use crate::app::ExternalMsg; use crate::app::HelpMenuLine; use crate::app::NodeFilter; use crate::app::NodeSorter; use crate::app::NodeSorterApplicable; use crate::ui::Border; use crate::ui::BorderType; use crate::ui::Constraint; use crate::ui::Layout; use crate::ui::Style; use indexmap::IndexSet; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Action { #[serde(default)] pub help: Option, #[serde(default)] pub messages: Vec, } impl Action { pub fn sanitized(self, read_only: bool) -> Option { if self.messages.is_empty() { None } else if read_only { if self.messages.iter().all(ExternalMsg::is_read_only) { Some(self) } else { None } } else { Some(self) } } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct NodeTypeConfig { #[serde(default)] pub style: Style, #[serde(default)] pub meta: HashMap, } impl NodeTypeConfig { pub fn extend(mut self, other: &Self) -> Self { self.style = self.style.extend(&other.style); self.meta.extend(other.meta.to_owned()); self } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct NodeTypesConfig { #[serde(default)] pub directory: NodeTypeConfig, #[serde(default)] pub file: NodeTypeConfig, #[serde(default)] pub symlink: NodeTypeConfig, #[serde(default)] pub mime_essence: HashMap>, #[serde(default)] pub extension: HashMap, #[serde(default)] pub special: HashMap, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct UiConfig { #[serde(default)] pub prefix: Option, #[serde(default)] pub suffix: Option, #[serde(default)] pub style: Style, } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct UiElement { #[serde(default)] pub format: Option, #[serde(default)] pub style: Style, } impl UiElement { pub fn extend(mut self, other: &Self) -> Self { self.format = other.format.to_owned().or(self.format); self.style = self.style.extend(&other.style); self } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TableRowConfig { #[serde(default)] pub cols: Option>, #[serde(default)] pub style: Style, #[serde(default)] pub height: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TableConfig { #[serde(default)] pub header: TableRowConfig, #[serde(default)] pub row: TableRowConfig, #[serde(default)] pub style: Style, #[serde(default)] pub tree: Option<(UiElement, UiElement, UiElement)>, #[serde(default)] pub col_spacing: Option, #[serde(default)] pub col_widths: Option>, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LogsConfig { #[serde(default)] pub info: UiElement, #[serde(default)] pub success: UiElement, #[serde(default)] pub warning: UiElement, #[serde(default)] pub error: UiElement, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SortDirectionIdentifiersUi { #[serde(default)] pub forward: UiElement, #[serde(default)] pub reverse: UiElement, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SortAndFilterUi { #[serde(default)] pub separator: UiElement, #[serde(default)] pub default_identifier: UiElement, #[serde(default)] pub sort_direction_identifiers: SortDirectionIdentifiersUi, #[serde(default)] pub sorter_identifiers: HashMap, #[serde(default)] pub filter_identifiers: HashMap, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PanelUi { #[serde(default)] pub default: PanelUiConfig, #[serde(default)] pub table: PanelUiConfig, #[serde(default)] pub sort_and_filter: PanelUiConfig, #[serde(default)] pub selection: PanelUiConfig, #[serde(default)] pub input_and_logs: PanelUiConfig, #[serde(default)] pub help_menu: PanelUiConfig, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct GeneralConfig { #[serde(default)] pub disable_debug_error_mode: bool, #[serde(default)] pub enable_mouse: bool, #[serde(default)] pub show_hidden: bool, #[serde(default)] pub read_only: bool, #[serde(default)] pub enable_recover_mode: bool, #[serde(default)] pub hide_remaps_in_help_menu: bool, #[serde(default)] pub enforce_bounded_index_navigation: bool, #[serde(default)] pub prompt: UiElement, #[serde(default)] pub logs: LogsConfig, #[serde(default)] pub table: TableConfig, #[serde(default)] pub default_ui: UiConfig, #[serde(default)] pub focus_ui: UiConfig, #[serde(default)] pub selection_ui: UiConfig, #[serde(default)] pub focus_selection_ui: UiConfig, #[serde(default)] pub sort_and_filter_ui: SortAndFilterUi, #[serde(default)] pub panel_ui: PanelUi, #[serde(default)] pub initial_sorting: Option>, #[serde(default)] pub initial_mode: Option, #[serde(default)] pub initial_layout: Option, #[serde(default)] pub start_fifo: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct KeyBindings { #[serde(default)] pub on_key: BTreeMap, #[serde(default)] pub on_alphabet: Option, #[serde(default)] pub on_number: Option, #[serde(default)] pub on_alphanumeric: Option, #[serde(default)] pub on_special_character: Option, #[serde(default)] pub on_character: Option, #[serde(default)] pub on_navigation: Option, #[serde(default)] pub on_function: Option, #[serde(default)] pub default: Option, // Checklist for adding new field: // - [ ] Update App::handle_key // - [ ] Update KeyBindings::sanitized // - [ ] Update Mode::help_menu // - [ ] Update configure-key-bindings.md // - [ ] Update debug-key-bindings.md } impl KeyBindings { pub fn sanitized(mut self, read_only: bool) -> Self { if read_only { self.on_key = self .on_key .into_iter() .filter_map(|(k, a)| a.sanitized(read_only).map(|a| (k, a))) .collect(); self.on_alphabet = self.on_alphabet.and_then(|a| a.sanitized(read_only)); self.on_number = self.on_number.and_then(|a| a.sanitized(read_only)); self.on_alphanumeric = self.on_alphanumeric.and_then(|a| a.sanitized(read_only)); self.on_special_character = self .on_special_character .and_then(|a| a.sanitized(read_only)); self.on_character = self.on_character.and_then(|a| a.sanitized(read_only)); self.on_navigation = self.on_navigation.and_then(|a| a.sanitized(read_only)); self.on_function = self.on_function.and_then(|a| a.sanitized(read_only)); self.default = self.default.and_then(|a| a.sanitized(read_only)); }; self } } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Mode { #[serde(default)] pub name: String, #[serde(default)] pub help: Option, #[serde(default)] pub extra_help: Option, #[serde(default)] pub key_bindings: KeyBindings, #[serde(default)] pub layout: Option, } impl Mode { pub fn sanitized(mut self, read_only: bool) -> Self { self.key_bindings = self.key_bindings.sanitized(read_only); self } pub fn help_menu(&self) -> Vec { let extra_help_lines = self.extra_help.clone().map(|e| { e.lines() .map(|l| HelpMenuLine::Paragraph(l.into())) .collect::>() }); self.help .clone() .map(|h| { h.lines() .map(|l| HelpMenuLine::Paragraph(l.into())) .collect() }) .unwrap_or_else(|| { let lines = extra_help_lines .unwrap_or_default() .into_iter() .chain(self.key_bindings.on_key.iter().filter_map(|(k, a)| { let remaps = self .key_bindings .on_key .iter() .filter_map(|(rk, ra)| { if rk == k { None } else if a == ra { Some(rk.clone()) } else { None } }) .collect::>(); a.help .clone() .map(|h| HelpMenuLine::KeyMap(k.into(), remaps, h)) })) .chain( self.key_bindings .on_alphabet .iter() .map(|a| ("[a-Z]", a.help.clone())) .filter_map(|(k, mh)| { mh.map(|h| HelpMenuLine::KeyMap(k.into(), vec![], h)) }), ) .chain( self.key_bindings .on_number .iter() .map(|a| ("[0-9]", a.help.clone())) .filter_map(|(k, mh)| { mh.map(|h| HelpMenuLine::KeyMap(k.into(), vec![], h)) }), ) .chain( self.key_bindings .on_alphanumeric .iter() .map(|a| ("[0-Z]", a.help.clone())) .filter_map(|(k, mh)| { mh.map(|h| HelpMenuLine::KeyMap(k.into(), vec![], h)) }), ) .chain( self.key_bindings .on_special_character .iter() .map(|a| ("[^0-Z]", a.help.clone())) .filter_map(|(k, mh)| { mh.map(|h| HelpMenuLine::KeyMap(k.into(), vec![], h)) }), ) .chain( self.key_bindings .on_character .iter() .map(|a| ("[*]", a.help.clone())) .filter_map(|(k, mh)| { mh.map(|h| HelpMenuLine::KeyMap(k.into(), vec![], h)) }), ) .chain( self.key_bindings .on_navigation .iter() .map(|a| ("[nav]", a.help.clone())) .filter_map(|(k, mh)| { mh.map(|h| HelpMenuLine::KeyMap(k.into(), vec![], h)) }), ) .chain( self.key_bindings .on_function .iter() .map(|a| ("[f1-f12]", a.help.clone())) .filter_map(|(k, mh)| { mh.map(|h| HelpMenuLine::KeyMap(k.into(), vec![], h)) }), ) .chain( self.key_bindings .default .iter() .map(|a| ("[default]", a.help.clone())) .filter_map(|(k, mh)| { mh.map(|h| HelpMenuLine::KeyMap(k.into(), vec![], h)) }), ); let mut remapped = HashSet::new(); let mut result = vec![]; for line in lines { match line { HelpMenuLine::Paragraph(p) => { result.push(HelpMenuLine::Paragraph(p)) } HelpMenuLine::KeyMap(k, r, d) => { if !remapped.contains(&k) { for k in r.iter() { remapped.insert(k.clone()); } result.push(HelpMenuLine::KeyMap(k, r, d)); } } } } result }) } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct BuiltinModesConfig { #[serde(default)] pub default: Mode, #[serde(default)] pub debug_error: Mode, #[serde(default)] pub recover: Mode, #[serde(default)] pub selection_ops: Mode, #[serde(default)] pub create: Mode, #[serde(default)] pub create_directory: Mode, #[serde(default)] pub create_file: Mode, #[serde(default)] pub number: Mode, #[serde(default)] pub go_to: Mode, #[serde(default)] pub rename: Mode, #[serde(default)] pub duplicate_as: Mode, #[serde(default)] pub delete: Mode, #[serde(default)] pub action: Mode, #[serde(default)] pub search: Mode, #[serde(default)] pub filter: Mode, #[serde(default)] pub relative_path_does_match_regex: Mode, #[serde(default)] pub relative_path_does_not_match_regex: Mode, #[serde(default)] pub sort: Mode, #[serde(default)] pub switch_layout: Mode, #[serde(default)] pub quit: Mode, } impl BuiltinModesConfig { pub fn get(&self, name: &str) -> Option<&Mode> { match name { "default" => Some(&self.default), "debug_error" => Some(&self.debug_error), "recover" => Some(&self.recover), "selection ops" => Some(&self.selection_ops), "selection_ops" => Some(&self.selection_ops), "create" => Some(&self.create), "create file" => Some(&self.create_file), "create_file" => Some(&self.create_file), "create directory" => Some(&self.create_directory), "create_directory" => Some(&self.create_directory), "number" => Some(&self.number), "go to" => Some(&self.go_to), "go_to" => Some(&self.go_to), "rename" => Some(&self.rename), "duplicate as" => Some(&self.duplicate_as), "duplicate_as" => Some(&self.duplicate_as), "delete" => Some(&self.delete), "action" => Some(&self.action), "search" => Some(&self.search), "sort" => Some(&self.sort), "filter" => Some(&self.filter), "relative_path_does_match_regex" => { Some(&self.relative_path_does_match_regex) } "relative path does match regex" => { Some(&self.relative_path_does_match_regex) } "relative_path_does_not_match_regex" => { Some(&self.relative_path_does_not_match_regex) } "relative path does not match regex" => { Some(&self.relative_path_does_not_match_regex) } "switch layout" => Some(&self.switch_layout), "switch_layout" => Some(&self.switch_layout), "quit" => Some(&self.quit), _ => None, } } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ModesConfig { #[serde(default)] pub builtin: BuiltinModesConfig, #[serde(default)] pub custom: HashMap, } impl ModesConfig { pub fn get(&self, name: &str) -> Option<&Mode> { self.builtin.get(name).or_else(|| self.custom.get(name)) } } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct PanelUiConfig { #[serde(default)] pub title: UiElement, #[serde(default)] pub style: Style, #[serde(default)] pub borders: Option>, #[serde(default)] pub border_type: Option, #[serde(default)] pub border_style: Style, } impl PanelUiConfig { pub fn extend(mut self, other: &Self) -> Self { self.title = self.title.extend(&other.title); self.style = self.style.extend(&other.style); self.borders = other.borders.to_owned().or(self.borders); self.border_type = other.border_type.to_owned().or(self.border_type); self.border_style = self.border_style.extend(&other.border_style); self } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct BuiltinLayoutsConfig { #[serde(default)] pub default: Layout, #[serde(default)] pub no_help: Layout, #[serde(default)] pub no_selection: Layout, #[serde(default)] pub no_help_no_selection: Layout, } impl BuiltinLayoutsConfig { pub fn get(&self, name: &str) -> Option<&Layout> { match name { "default" => Some(&self.default), "no_help" => Some(&self.no_help), "no help" => Some(&self.no_help), "no_selection" => Some(&self.no_selection), "no selection" => Some(&self.no_selection), "no_help_no_selection" => Some(&self.no_help_no_selection), "no help no selection" => Some(&self.no_help_no_selection), _ => None, } } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LayoutsConfig { #[serde(default)] pub builtin: BuiltinLayoutsConfig, #[serde(default)] pub custom: HashMap, } impl LayoutsConfig { pub fn get_builtin(&self, name: &str) -> Option<&Layout> { self.builtin.get(name) } pub fn get_custom(&self, name: &str) -> Option<&Layout> { self.custom.get(name) } pub fn get(&self, name: &str) -> Option<&Layout> { self.get_builtin(name).or_else(|| self.get_custom(name)) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { #[serde(default)] pub general: GeneralConfig, #[serde(default)] pub node_types: NodeTypesConfig, #[serde(default)] pub layouts: LayoutsConfig, #[serde(default)] pub modes: ModesConfig, }