mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-29 14:21:43 +00:00
751 lines
22 KiB
Rust
751 lines
22 KiB
Rust
use crate::cli::parse_source_entry_delimiter;
|
|
use crate::{
|
|
config::{Binding, KeyBindings, ui},
|
|
features::Features,
|
|
screen::layout::{InputPosition, Orientation},
|
|
};
|
|
use anyhow::Result;
|
|
use rustc_hash::FxHashMap;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_with::{OneOrMany, serde_as};
|
|
use std::fmt::{self, Display, Formatter};
|
|
use std::hash::{Hash, Hasher};
|
|
use string_pipeline::MultiTemplate;
|
|
use which::which;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum Template {
|
|
StringPipeline(MultiTemplate),
|
|
Raw(String),
|
|
}
|
|
|
|
impl Template {
|
|
pub fn raw(&self) -> &str {
|
|
match self {
|
|
Template::StringPipeline(template) => template.template_string(),
|
|
Template::Raw(raw) => raw,
|
|
}
|
|
}
|
|
|
|
pub fn parse(template: &str) -> Result<Self, String> {
|
|
match MultiTemplate::parse(template) {
|
|
Ok(multi_template) => Ok(Template::StringPipeline(multi_template)),
|
|
Err(_) => Ok(Template::Raw(template.to_string())),
|
|
}
|
|
}
|
|
|
|
pub fn format(&self, input: &str) -> Result<String> {
|
|
match self {
|
|
Template::StringPipeline(template) => {
|
|
template.format(input).map_err(|e| {
|
|
anyhow::anyhow!(
|
|
"Failed to format template '{}' with '{}': {}",
|
|
self.raw(),
|
|
input,
|
|
e
|
|
)
|
|
})
|
|
}
|
|
Template::Raw(raw) => Ok(raw.replace("{}", input)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for Template {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
write!(f, "{}", self.raw())
|
|
}
|
|
}
|
|
|
|
impl PartialEq for Template {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.raw() == other.raw()
|
|
&& matches!(
|
|
(self, other),
|
|
(Template::StringPipeline(_), Template::StringPipeline(_))
|
|
| (Template::Raw(_), Template::Raw(_))
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Eq for Template {}
|
|
|
|
impl Hash for Template {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
self.raw().hash(state);
|
|
}
|
|
}
|
|
|
|
impl Serialize for Template {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
serializer.serialize_str(self.raw())
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for Template {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let raw = String::deserialize(deserializer)?;
|
|
Template::parse(&raw).map_err(serde::de::Error::custom)
|
|
}
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct CommandSpec {
|
|
#[serde(rename = "command")]
|
|
#[serde_as(as = "OneOrMany<_>")]
|
|
pub inner: Vec<Template>,
|
|
#[serde(default)]
|
|
pub interactive: bool,
|
|
#[serde(default)]
|
|
pub env: FxHashMap<String, String>,
|
|
}
|
|
|
|
impl Display for CommandSpec {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
write!(
|
|
f,
|
|
"[{}]",
|
|
self.inner
|
|
.iter()
|
|
.map(Template::raw)
|
|
.collect::<Vec<_>>()
|
|
.join(";")
|
|
)
|
|
}
|
|
}
|
|
|
|
impl From<Template> for CommandSpec {
|
|
fn from(template: Template) -> Self {
|
|
Self::new(vec![template], false, FxHashMap::default())
|
|
}
|
|
}
|
|
|
|
impl CommandSpec {
|
|
pub fn new(
|
|
inner: Vec<Template>,
|
|
interactive: bool,
|
|
env: FxHashMap<String, String>,
|
|
) -> Self {
|
|
Self {
|
|
inner,
|
|
interactive,
|
|
env,
|
|
}
|
|
}
|
|
|
|
pub fn command_count(&self) -> usize {
|
|
self.inner.len()
|
|
}
|
|
|
|
pub fn has_multiple_commands(&self) -> bool {
|
|
self.inner.len() > 1
|
|
}
|
|
|
|
/// This wraps back to the first command in a circular manner.
|
|
///
|
|
/// # Panics
|
|
/// If the command spec does not contain any commands.
|
|
pub fn get_nth(&self, index: usize) -> &Template {
|
|
&self.inner[index % self.inner.len()]
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct ChannelKeyBindings {
|
|
/// Optional channel specific shortcut that, when pressed, switches directly to this channel.
|
|
#[serde(default)]
|
|
pub shortcut: Option<Binding>,
|
|
/// Regular action -> binding mappings living at channel level.
|
|
#[serde(flatten)]
|
|
#[serde(default)]
|
|
pub bindings: KeyBindings,
|
|
}
|
|
|
|
impl ChannelKeyBindings {
|
|
pub fn channel_shortcut(&self) -> Option<&Binding> {
|
|
self.shortcut.as_ref()
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct HistoryConfig {
|
|
/// Whether to use global history for this channel (overrides global setting)
|
|
#[serde(default)]
|
|
pub global_mode: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct ChannelPrototype {
|
|
pub metadata: Metadata,
|
|
pub source: SourceSpec,
|
|
#[serde(default)]
|
|
pub preview: Option<PreviewSpec>,
|
|
#[serde(default)]
|
|
pub ui: Option<UiSpec>,
|
|
#[serde(default)]
|
|
pub keybindings: Option<ChannelKeyBindings>,
|
|
/// Watch interval in seconds for automatic reloading (0 = disabled)
|
|
#[serde(default)]
|
|
pub watch: f64,
|
|
#[serde(default)]
|
|
pub history: HistoryConfig,
|
|
// actions: Vec<Action>,
|
|
}
|
|
|
|
impl ChannelPrototype {
|
|
pub fn new(name: &str, command: &str) -> Self {
|
|
Self {
|
|
metadata: Metadata {
|
|
name: name.to_string(),
|
|
description: None,
|
|
requirements: vec![],
|
|
},
|
|
source: SourceSpec {
|
|
command: CommandSpec {
|
|
inner: vec![
|
|
Template::parse(command)
|
|
.expect("Failed to parse command"),
|
|
],
|
|
interactive: false,
|
|
env: FxHashMap::default(),
|
|
},
|
|
entry_delimiter: None,
|
|
ansi: false,
|
|
display: None,
|
|
output: None,
|
|
},
|
|
preview: None,
|
|
ui: None,
|
|
keybindings: None,
|
|
watch: 0.0,
|
|
history: HistoryConfig::default(),
|
|
}
|
|
}
|
|
|
|
pub fn stdin(
|
|
preview: Option<PreviewSpec>,
|
|
entry_delimiter: Option<char>,
|
|
) -> Self {
|
|
Self {
|
|
metadata: Metadata {
|
|
name: "stdin".to_string(),
|
|
description: Some(
|
|
"A channel that reads from stdin".to_string(),
|
|
),
|
|
requirements: vec![],
|
|
},
|
|
source: SourceSpec {
|
|
command: CommandSpec {
|
|
inner: vec![Template::parse("cat").unwrap()],
|
|
interactive: false,
|
|
env: FxHashMap::default(),
|
|
},
|
|
ansi: false,
|
|
entry_delimiter,
|
|
display: None,
|
|
output: None,
|
|
},
|
|
preview,
|
|
ui: None,
|
|
keybindings: None,
|
|
watch: 0.0,
|
|
history: HistoryConfig::default(),
|
|
}
|
|
}
|
|
|
|
pub fn with_preview(mut self, preview: Option<PreviewSpec>) -> Self {
|
|
self.preview = preview;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Display for ChannelPrototype {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
write!(f, "{}", self.metadata.name)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct Metadata {
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
#[serde(default)]
|
|
pub requirements: Vec<BinaryRequirement>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
|
#[serde(transparent)]
|
|
pub struct BinaryRequirement {
|
|
pub bin_name: String,
|
|
#[serde(skip)]
|
|
met: bool,
|
|
}
|
|
|
|
impl BinaryRequirement {
|
|
pub fn new(bin_name: &str) -> Self {
|
|
Self {
|
|
bin_name: bin_name.to_string(),
|
|
met: false,
|
|
}
|
|
}
|
|
|
|
/// Check if the required binary is available in the system's PATH.
|
|
///
|
|
/// This method updates the requirement's state in place to reflect whether the binary was
|
|
/// found.
|
|
pub fn init(&mut self) {
|
|
self.met = which(&self.bin_name).is_ok();
|
|
}
|
|
|
|
/// Whether the requirement is available in the system's PATH.
|
|
///
|
|
/// This should be called after `init()`.
|
|
pub fn is_met(&self) -> bool {
|
|
self.met
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct SourceSpec {
|
|
#[serde(flatten)]
|
|
pub command: CommandSpec,
|
|
#[serde(deserialize_with = "deserialize_entry_delimiter", default)]
|
|
pub entry_delimiter: Option<char>,
|
|
#[serde(default)]
|
|
pub ansi: bool,
|
|
#[serde(default)]
|
|
pub display: Option<Template>,
|
|
#[serde(default)]
|
|
pub output: Option<Template>,
|
|
}
|
|
|
|
/// Just a helper function to adapt cli parsing to serde deserialization.
|
|
fn deserialize_entry_delimiter<'de, D>(
|
|
deserializer: D,
|
|
) -> Result<Option<char>, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
if let Ok(Some(delimiter)) = Option::<String>::deserialize(deserializer) {
|
|
parse_source_entry_delimiter(&delimiter)
|
|
.map(Some)
|
|
.map_err(serde::de::Error::custom)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct PreviewSpec {
|
|
#[serde(flatten)]
|
|
pub command: CommandSpec,
|
|
#[serde(default)]
|
|
pub offset: Option<Template>,
|
|
#[serde(default)]
|
|
pub cached: bool,
|
|
}
|
|
|
|
impl PreviewSpec {
|
|
pub fn new(command: CommandSpec, offset: Option<Template>) -> Self {
|
|
Self {
|
|
command,
|
|
offset,
|
|
cached: false,
|
|
}
|
|
}
|
|
|
|
pub fn from_str_command(command: &str) -> Self {
|
|
Self {
|
|
command: CommandSpec {
|
|
inner: vec![
|
|
Template::parse(command)
|
|
.expect("Failed to parse preview command"),
|
|
],
|
|
interactive: false,
|
|
env: FxHashMap::default(),
|
|
},
|
|
offset: None,
|
|
cached: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct UiSpec {
|
|
#[serde(default)]
|
|
pub ui_scale: Option<u16>,
|
|
#[serde(default)]
|
|
pub features: Option<Features>,
|
|
// `layout` is clearer for the user but collides with the overall `Layout` type.
|
|
#[serde(rename = "layout", alias = "orientation", default)]
|
|
pub orientation: Option<Orientation>,
|
|
#[serde(default)]
|
|
pub input_bar_position: Option<InputPosition>,
|
|
#[serde(default)]
|
|
pub input_header: Option<Template>,
|
|
#[serde(default)]
|
|
pub input_prompt: Option<String>,
|
|
// Feature-specific configurations
|
|
#[serde(default)]
|
|
pub preview_panel: Option<ui::PreviewPanelConfig>,
|
|
#[serde(default)]
|
|
pub status_bar: Option<ui::StatusBarConfig>,
|
|
#[serde(default)]
|
|
pub help_panel: Option<ui::HelpPanelConfig>,
|
|
#[serde(default)]
|
|
pub remote_control: Option<ui::RemoteControlConfig>,
|
|
}
|
|
|
|
pub const DEFAULT_PROTOTYPE_NAME: &str = "files";
|
|
|
|
impl From<&crate::config::UiConfig> for UiSpec {
|
|
fn from(config: &crate::config::UiConfig) -> Self {
|
|
UiSpec {
|
|
ui_scale: Some(config.ui_scale),
|
|
features: Some(config.features.clone()),
|
|
orientation: Some(config.orientation),
|
|
input_bar_position: Some(config.input_bar_position),
|
|
input_header: config.input_header.clone(),
|
|
input_prompt: Some(config.input_prompt.clone()),
|
|
preview_panel: Some(config.preview_panel.clone()),
|
|
status_bar: Some(config.status_bar.clone()),
|
|
help_panel: Some(config.help_panel.clone()),
|
|
remote_control: Some(config.remote_control.clone()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{action::Action, event::Key};
|
|
|
|
use super::*;
|
|
use toml::from_str;
|
|
|
|
#[test]
|
|
fn test_command_spec_get_nth() {
|
|
let command_spec = CommandSpec {
|
|
inner: vec![
|
|
Template::parse("cmd1").unwrap(),
|
|
Template::parse("cmd2").unwrap(),
|
|
Template::parse("cmd3").unwrap(),
|
|
],
|
|
interactive: false,
|
|
env: FxHashMap::default(),
|
|
};
|
|
|
|
assert_eq!(command_spec.get_nth(0).raw(), "cmd1");
|
|
assert_eq!(command_spec.get_nth(1).raw(), "cmd2");
|
|
assert_eq!(command_spec.get_nth(2).raw(), "cmd3");
|
|
assert_eq!(command_spec.get_nth(3).raw(), "cmd1"); // wraps around
|
|
}
|
|
|
|
#[test]
|
|
fn test_template_serialization() {
|
|
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
|
struct TestStruct {
|
|
template: Template,
|
|
}
|
|
let raw_1 = r#"template = "Hello, {}""#;
|
|
let raw_2 = r#"template = "Hello, World""#;
|
|
let raw_3 = r#"template = "docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}'""#;
|
|
|
|
let test_1: TestStruct = from_str(raw_1).unwrap();
|
|
let test_2: TestStruct = from_str(raw_2).unwrap();
|
|
let test_3: TestStruct = from_str(raw_3).unwrap();
|
|
|
|
assert_eq!(
|
|
test_1.template,
|
|
Template::StringPipeline(
|
|
MultiTemplate::parse("Hello, {}").unwrap()
|
|
)
|
|
);
|
|
assert_eq!(
|
|
test_2.template,
|
|
Template::StringPipeline(
|
|
MultiTemplate::parse("Hello, World").unwrap()
|
|
)
|
|
);
|
|
assert_eq!(
|
|
test_3.template,
|
|
Template::Raw(
|
|
"docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}'"
|
|
.to_string()
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_channel_prototype_deserialization() {
|
|
let toml_data = r#"
|
|
[metadata]
|
|
name = "files"
|
|
description = "A channel to select files and directories"
|
|
requirements = ["fd", "bat"]
|
|
|
|
[source]
|
|
command = "fd -t f"
|
|
interactive = false
|
|
env = {}
|
|
display = "{split:/:-1}" # only show the last path segment ('/a/b/c' -> 'c')
|
|
output = "{}" # output the full path
|
|
unknown_field = "ignored" # should be ignored
|
|
|
|
[preview]
|
|
command = "bat -n --color=always {}"
|
|
env = { "BAT_THEME" = "ansi" }
|
|
interactive = false
|
|
offset = "3" # why not
|
|
|
|
[ui]
|
|
layout = "landscape"
|
|
ui_scale = 100
|
|
input_bar_position = "bottom"
|
|
input_header = "Input: {}"
|
|
|
|
[ui.features]
|
|
preview_panel = { enabled = true, visible = true }
|
|
|
|
[ui.preview_panel]
|
|
size = 66
|
|
header = "Preview: {}"
|
|
footer = "Press 'q' to quit"
|
|
|
|
[keybindings]
|
|
esc = "quit"
|
|
ctrl-c = "quit"
|
|
down = "select_next_entry"
|
|
ctrl-n = "select_next_entry"
|
|
ctrl-j = "select_next_entry"
|
|
up = "select_prev_entry"
|
|
ctrl-p = "select_prev_entry"
|
|
ctrl-k = "select_prev_entry"
|
|
enter = "confirm_selection"
|
|
"#;
|
|
|
|
let prototype: ChannelPrototype = from_str(toml_data).unwrap();
|
|
|
|
assert_eq!(prototype.metadata.name, "files");
|
|
assert_eq!(
|
|
prototype.metadata.description,
|
|
Some("A channel to select files and directories".to_string())
|
|
);
|
|
assert_eq!(
|
|
format!("{}", prototype.source.command.inner[0]),
|
|
"fd -t f"
|
|
);
|
|
|
|
assert!(!prototype.source.command.interactive);
|
|
assert_eq!(prototype.source.display.unwrap().raw(), "{split:/:-1}");
|
|
assert_eq!(prototype.source.output.unwrap().raw(), "{}");
|
|
|
|
let preview = prototype.preview.as_ref().unwrap();
|
|
assert_eq!(
|
|
format!("{}", preview.command.inner[0]),
|
|
"bat -n --color=always {}"
|
|
);
|
|
assert!(!preview.command.interactive);
|
|
assert_eq!(
|
|
preview.command.env.get("BAT_THEME"),
|
|
Some(&"ansi".to_string())
|
|
);
|
|
assert_eq!(preview.offset.as_ref().unwrap().raw(), "3");
|
|
|
|
let ui = prototype.ui.unwrap();
|
|
assert_eq!(ui.orientation, Some(Orientation::Landscape));
|
|
assert_eq!(ui.ui_scale, Some(100));
|
|
assert!(ui.features.is_some());
|
|
let features = ui.features.as_ref().unwrap();
|
|
assert!(features.preview_panel.enabled);
|
|
assert_eq!(ui.input_bar_position, Some(InputPosition::Bottom));
|
|
assert_eq!(ui.preview_panel.as_ref().unwrap().size, 66);
|
|
assert_eq!(ui.input_header.as_ref().unwrap().raw(), "Input: {}");
|
|
assert_eq!(
|
|
ui.preview_panel
|
|
.as_ref()
|
|
.unwrap()
|
|
.header
|
|
.as_ref()
|
|
.unwrap()
|
|
.raw(),
|
|
"Preview: {}"
|
|
);
|
|
assert_eq!(
|
|
ui.preview_panel
|
|
.as_ref()
|
|
.unwrap()
|
|
.footer
|
|
.as_ref()
|
|
.unwrap()
|
|
.raw(),
|
|
"Press 'q' to quit"
|
|
);
|
|
|
|
let keybindings = prototype.keybindings.unwrap();
|
|
assert_eq!(keybindings.bindings.get(&Key::Esc), Some(&Action::Quit));
|
|
assert_eq!(
|
|
keybindings.bindings.get(&Key::Ctrl('c')),
|
|
Some(&Action::Quit)
|
|
);
|
|
assert_eq!(
|
|
keybindings.bindings.get(&Key::Down),
|
|
Some(&Action::SelectNextEntry)
|
|
);
|
|
assert_eq!(
|
|
keybindings.bindings.get(&Key::Ctrl('n')),
|
|
Some(&Action::SelectNextEntry)
|
|
);
|
|
assert_eq!(
|
|
keybindings.bindings.get(&Key::Ctrl('j')),
|
|
Some(&Action::SelectNextEntry)
|
|
);
|
|
assert_eq!(
|
|
keybindings.bindings.get(&Key::Up),
|
|
Some(&Action::SelectPrevEntry)
|
|
);
|
|
assert_eq!(
|
|
keybindings.bindings.get(&Key::Ctrl('p')),
|
|
Some(&Action::SelectPrevEntry)
|
|
);
|
|
assert_eq!(
|
|
keybindings.bindings.get(&Key::Ctrl('k')),
|
|
Some(&Action::SelectPrevEntry)
|
|
);
|
|
assert_eq!(
|
|
keybindings.bindings.get(&Key::Enter),
|
|
Some(&Action::ConfirmSelection)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_channel_prototype_deserialization_multiple_commands() {
|
|
let toml_data = r#"
|
|
[metadata]
|
|
name = "files"
|
|
description = "A channel to select files and directories"
|
|
requirements = ["fd", "bat"]
|
|
|
|
[source]
|
|
command = ["fd -t f", "fd -t f --hidden"]
|
|
output = "{}" # output the full path
|
|
"#;
|
|
|
|
let prototype: ChannelPrototype = from_str(toml_data).unwrap();
|
|
|
|
assert_eq!(prototype.metadata.name, "files");
|
|
assert_eq!(
|
|
prototype.metadata.description,
|
|
Some("A channel to select files and directories".to_string())
|
|
);
|
|
assert_eq!(
|
|
prototype
|
|
.source
|
|
.command
|
|
.inner
|
|
.iter()
|
|
.map(Template::raw)
|
|
.collect::<Vec<_>>(),
|
|
vec!["fd -t f", "fd -t f --hidden"]
|
|
);
|
|
assert!(!prototype.source.command.interactive);
|
|
assert!(prototype.source.command.env.is_empty());
|
|
assert_eq!(prototype.source.output.unwrap().raw(), "{}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_channel_prototype_deserialization_bare_minimum() {
|
|
let toml_data = r#"
|
|
[metadata]
|
|
name = "files"
|
|
description = "A channel to select files and directories"
|
|
requirements = ["fd"]
|
|
|
|
[source]
|
|
command = "fd -t f"
|
|
"#;
|
|
|
|
let prototype: ChannelPrototype = from_str(toml_data).unwrap();
|
|
|
|
assert_eq!(prototype.metadata.name, "files");
|
|
assert_eq!(
|
|
prototype.metadata.description,
|
|
Some("A channel to select files and directories".to_string())
|
|
);
|
|
assert_eq!(
|
|
format!("{}", prototype.source.command.inner[0]),
|
|
"fd -t f"
|
|
);
|
|
assert!(!prototype.source.command.interactive);
|
|
assert!(prototype.source.command.env.is_empty());
|
|
assert!(prototype.source.display.is_none());
|
|
assert!(prototype.source.output.is_none());
|
|
assert!(prototype.preview.is_none());
|
|
assert!(prototype.ui.is_none());
|
|
assert!(prototype.keybindings.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_channel_prototype_deserialization_partial_ui_options() {
|
|
let toml_data = r#"
|
|
[metadata]
|
|
name = "files"
|
|
description = "A channel to select files and directories"
|
|
requirements = ["fd"]
|
|
|
|
[source]
|
|
command = "fd -t f"
|
|
|
|
[ui]
|
|
layout = "landscape"
|
|
ui_scale = 40
|
|
|
|
[ui.preview_panel]
|
|
footer = "Press 'q' to quit"
|
|
"#;
|
|
|
|
let prototype: ChannelPrototype = from_str(toml_data).unwrap();
|
|
|
|
assert_eq!(prototype.metadata.name, "files");
|
|
assert_eq!(
|
|
prototype.metadata.description,
|
|
Some("A channel to select files and directories".to_string())
|
|
);
|
|
assert_eq!(
|
|
format!("{}", prototype.source.command.inner[0]),
|
|
"fd -t f"
|
|
);
|
|
assert!(!prototype.source.command.interactive);
|
|
assert!(prototype.source.command.env.is_empty());
|
|
assert!(prototype.source.display.is_none());
|
|
assert!(prototype.source.output.is_none());
|
|
|
|
let ui = prototype.ui.unwrap();
|
|
assert_eq!(ui.orientation, Some(Orientation::Landscape));
|
|
assert_eq!(ui.ui_scale, Some(40));
|
|
assert!(ui.features.is_none());
|
|
assert!(ui.input_bar_position.is_none());
|
|
assert!(ui.preview_panel.is_some());
|
|
assert!(ui.input_header.is_none());
|
|
assert_eq!(
|
|
ui.preview_panel
|
|
.as_ref()
|
|
.unwrap()
|
|
.footer
|
|
.as_ref()
|
|
.unwrap()
|
|
.raw(),
|
|
"Press 'q' to quit"
|
|
);
|
|
assert!(ui.status_bar.is_none());
|
|
assert!(ui.help_panel.is_none());
|
|
assert!(ui.remote_control.is_none());
|
|
}
|
|
}
|