diff --git a/.config/config.toml b/.config/config.toml index fae8300..d0c4b06 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -35,51 +35,50 @@ theme = "Visual Studio Dark+" # Keybindings # ---------------------------------------------------------------------------- # -# Channel mode keybindings +# Channel mode +# ------------------------ [keybindings.Channel] # Quit the application -esc = "Quit" +Quit = "esc" # Scrolling through entries -down = "SelectNextEntry" -ctrl-n = "SelectNextEntry" -up = "SelectPrevEntry" -ctrl-p = "SelectPrevEntry" +SelectNextEntry = "down" +SelectPrevEntry = "up" # Scrolling the preview pane -ctrl-d = "ScrollPreviewHalfPageDown" -ctrl-u = "ScrollPreviewHalfPageUp" +ScrollPreviewHalfPageDown = "ctrl-d" +ScrollPreviewHalfPageUp = "ctrl-u" # Select an entry -enter = "SelectEntry" +SelectEntry = "enter" # Copy the selected entry to the clipboard -ctrl-y = "CopyEntryToClipboard" +CopyEntryToClipboard = "ctrl-y" # Toggle the remote control mode -ctrl-r = "ToggleRemoteControl" +ToggleRemoteControl = "ctrl-r" # Toggle the send to channel mode -ctrl-s = "ToggleSendToChannel" +ToggleSendToChannel = "ctrl-s" -# Remote control mode keybindings + +# Remote control mode +# ------------------------------- [keybindings.RemoteControl] # Quit the application -esc = "Quit" +Quit = "esc" # Scrolling through entries -down = "SelectNextEntry" -up = "SelectPrevEntry" -ctrl-n = "SelectNextEntry" -ctrl-p = "SelectPrevEntry" +SelectNextEntry = "down" +SelectPrevEntry = "up" # Select an entry -enter = "SelectEntry" +SelectEntry = "enter" # Toggle the remote control mode -ctrl-r = "ToggleRemoteControl" +ToggleRemoteControl = "ctrl-r" -# Send to channel mode keybindings + +# Send to channel mode +# -------------------------------- [keybindings.SendToChannel] # Quit the application -esc = "Quit" +Quit = "esc" # Scrolling through entries -down = "SelectNextEntry" -up = "SelectPrevEntry" -ctrl-n = "SelectNextEntry" -ctrl-p = "SelectPrevEntry" +SelectNextEntry = "down" +SelectPrevEntry = "up" # Select an entry -enter = "SelectEntry" +SelectEntry = "enter" # Toggle the send to channel mode -ctrl-s = "ToggleSendToChannel" +ToggleSendToChannel = "ctrl-s" diff --git a/crates/television/action.rs b/crates/television/action.rs index d6c85da..baddb66 100644 --- a/crates/television/action.rs +++ b/crates/television/action.rs @@ -2,7 +2,9 @@ use serde::{Deserialize, Serialize}; use strum::Display; /// The different actions that can be performed by the application. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display)] +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, Hash, +)] pub enum Action { // input actions /// Add a character to the input buffer. diff --git a/crates/television/app.rs b/crates/television/app.rs index d668cce..a0f1f63 100644 --- a/crates/television/app.rs +++ b/crates/television/app.rs @@ -1,9 +1,12 @@ +use std::collections::HashMap; use std::sync::Arc; use color_eyre::Result; +use derive_deref::Deref; use tokio::sync::{mpsc, Mutex}; use tracing::{debug, info}; +use crate::config::KeyBindings; use crate::television::{Mode, Television}; use crate::{ action::Action, @@ -14,10 +17,28 @@ use crate::{ use television_channels::channels::TelevisionChannel; use television_channels::entry::Entry; +#[derive(Deref, Default)] +pub struct Keymap(pub HashMap>); + +impl From<&KeyBindings> for Keymap { + fn from(keybindings: &KeyBindings) -> Self { + let mut keymap = HashMap::new(); + for (mode, bindings) in keybindings.iter() { + let mut mode_keymap = HashMap::new(); + for (action, key) in bindings { + mode_keymap.insert(*key, action.clone()); + } + keymap.insert(*mode, mode_keymap); + } + Self(keymap) + } +} + /// The main application struct that holds the state of the application. pub struct App { /// The configuration of the application. config: Config, + keymap: Keymap, // maybe move these two into config instead of passing them // via the cli? tick_rate: f64, @@ -51,14 +72,17 @@ impl App { let (_, event_rx) = mpsc::unbounded_channel(); let (event_abort_tx, _) = mpsc::unbounded_channel(); let television = Arc::new(Mutex::new(Television::new(channel))); + let config = Config::new()?; + let keymap = Keymap::from(&config.keybindings); Ok(Self { + config, + keymap, tick_rate, frame_rate, television, should_quit: false, should_suspend: false, - config: Config::new()?, action_tx, action_rx, event_rx, @@ -159,8 +183,7 @@ impl App { _ => {} } // get action based on keybindings - self.config - .keybindings + self.keymap .get(&self.television.lock().await.mode) .and_then(|keymap| keymap.get(&keycode).cloned()) .unwrap_or(if let Key::Char(c) = keycode { diff --git a/crates/television/config.rs b/crates/television/config.rs index 7ab603d..c3318b5 100644 --- a/crates/television/config.rs +++ b/crates/television/config.rs @@ -6,7 +6,7 @@ use crate::{ event::{convert_raw_event_to_key, Key}, television::Mode, }; -use color_eyre::Result; +use color_eyre::{eyre::Context, Result}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use derive_deref::{Deref, DerefMut}; use directories::ProjectDirs; @@ -113,47 +113,56 @@ const CONFIG_FILE_NAME: &str = "config.toml"; impl Config { #[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)] - pub fn new() -> Result { + pub fn new() -> Result { + // Load the default_config values as base defaults let default_config: Config = toml::from_str(CONFIG).expect("default config should be valid"); + // initialize the config builder let data_dir = get_data_dir(); let config_dir = get_config_dir(); let mut builder = config::Config::builder() .set_default("data_dir", data_dir.to_str().unwrap())? .set_default("config_dir", config_dir.to_str().unwrap())?; - // Load the default_config values as base defaults - builder = builder.add_source(config::File::from_str( - CONFIG, - config::FileFormat::Toml, - )); - + // Load the user's config file let source = config::File::from(config_dir.join(CONFIG_FILE_NAME)) .format(config::FileFormat::Toml) .required(false); builder = builder.add_source(source); - if !config_dir.join(CONFIG_FILE_NAME).is_file() { + if config_dir.join(CONFIG_FILE_NAME).is_file() { + debug!("Found config file at {:?}", config_dir); + let mut cfg: Self = + builder.build()?.try_deserialize().with_context(|| { + format!( + "Error parsing config file at {:?}", + config_dir.join(CONFIG_FILE_NAME) + ) + })?; + + for (mode, default_bindings) in default_config.keybindings.iter() { + let user_bindings = cfg.keybindings.entry(*mode).or_default(); + for (command, key) in default_bindings { + user_bindings + .entry(command.clone()) + .or_insert_with(|| *key); + } + } + + for (mode, default_styles) in default_config.styles.iter() { + let user_styles = cfg.styles.entry(*mode).or_default(); + for (style_key, style) in default_styles { + user_styles.entry(style_key.clone()).or_insert(*style); + } + } + + debug!("Config: {:?}", cfg); + Ok(cfg) + } else { warn!("No config file found at {:?}", config_dir); + Ok(default_config) } - - let mut cfg: Self = builder.build()?.try_deserialize()?; - - for (mode, default_bindings) in default_config.keybindings.iter() { - let user_bindings = cfg.keybindings.entry(*mode).or_default(); - for (key, cmd) in default_bindings { - user_bindings.entry(*key).or_insert_with(|| cmd.clone()); - } - } - for (mode, default_styles) in default_config.styles.iter() { - let user_styles = cfg.styles.entry(*mode).or_default(); - for (style_key, style) in default_styles { - user_styles.entry(style_key.clone()).or_insert(*style); - } - } - - Ok(cfg) } } @@ -188,7 +197,7 @@ fn project_directory() -> Option { } #[derive(Clone, Debug, Default, Deref, DerefMut)] -pub struct KeyBindings(pub HashMap>); +pub struct KeyBindings(pub config::Map>); impl<'de> Deserialize<'de> for KeyBindings { fn deserialize(deserializer: D) -> Result @@ -196,7 +205,7 @@ impl<'de> Deserialize<'de> for KeyBindings { D: Deserializer<'de>, { let parsed_map = - HashMap::>::deserialize( + HashMap::>::deserialize( deserializer, )?; @@ -205,7 +214,7 @@ impl<'de> Deserialize<'de> for KeyBindings { .map(|(mode, inner_map)| { let converted_inner_map = inner_map .into_iter() - .map(|(key_str, cmd)| (parse_key(&key_str).unwrap(), cmd)) + .map(|(cmd, key_str)| (cmd, parse_key(&key_str).unwrap())) .collect(); (mode, converted_inner_map) }) @@ -573,9 +582,8 @@ mod tests { c.keybindings .get(&Mode::Channel) .unwrap() - .get(&parse_key("esc").unwrap()) - .unwrap(), - &Action::Quit + .get(&Action::Quit), + Some(&parse_key("esc").unwrap()) ); Ok(()) } diff --git a/crates/television/main.rs b/crates/television/main.rs index b0c545c..2eb14a8 100644 --- a/crates/television/main.rs +++ b/crates/television/main.rs @@ -30,7 +30,7 @@ async fn main() -> Result<()> { let args = Cli::parse(); - let mut app: App = App::new( + match App::new( { if is_readable_stdin() { debug!("Using stdin channel"); @@ -42,13 +42,19 @@ async fn main() -> Result<()> { }, args.tick_rate, args.frame_rate, - )?; - - if let Some(entry) = app.run(stdout().is_terminal()).await? { - // print entry to stdout - stdout().flush()?; - info!("{:?}", entry); - writeln!(stdout(), "{}", entry.stdout_repr())?; + ) { + Ok(mut app) => { + if let Some(entry) = app.run(stdout().is_terminal()).await? { + // print entry to stdout + stdout().flush()?; + info!("{:?}", entry); + writeln!(stdout(), "{}", entry.stdout_repr())?; + } + Ok(()) + } + Err(err) => { + println!("{err:?}"); + return Ok(()); + } } - Ok(()) } diff --git a/crates/television/television.rs b/crates/television/television.rs index cf114c6..5c1ae82 100644 --- a/crates/television/television.rs +++ b/crates/television/television.rs @@ -1,3 +1,4 @@ +use crate::app::Keymap; use crate::picker::Picker; use crate::ui::input::actions::InputActionHandler; use crate::ui::layout::{Dimensions, Layout}; @@ -32,6 +33,7 @@ pub enum Mode { pub struct Television { action_tx: Option>, pub config: Config, + pub keymap: Keymap, pub(crate) channel: TelevisionChannel, pub(crate) remote_control: TelevisionChannel, pub mode: Mode, @@ -62,6 +64,7 @@ impl Television { Self { action_tx: None, config: Config::default(), + keymap: Keymap::default(), channel, remote_control: TelevisionChannel::RemoteControl( RemoteControl::default(), @@ -228,6 +231,7 @@ impl Television { /// * `Result<()>` - An Ok result or an error. pub fn register_config_handler(&mut self, config: Config) -> Result<()> { self.config = config; + self.keymap = Keymap::from(&self.config.keybindings); let previewer_config = std::convert::Into::::into( self.config.previewers.clone(), diff --git a/crates/television/ui/keymap.rs b/crates/television/ui/keymap.rs index 4ca34d0..795af30 100644 --- a/crates/television/ui/keymap.rs +++ b/crates/television/ui/keymap.rs @@ -211,8 +211,7 @@ impl Television { /// A reference to the keymap for the current mode. fn keymap_for_mode(&self) -> Result<&HashMap> { let keymap = self - .config - .keybindings + .keymap .get(&self.mode) .ok_or_eyre("No keybindings found for the current Mode")?; Ok(keymap)