fix(config): swap out default keymaps with user defined ones instead of stacking (#26)

* fix(config): swap out default keymaps with user defined ones instead of stacking

* fix default configuration fallback
This commit is contained in:
Alexandre Pasmantier 2024-11-15 15:16:00 +01:00 committed by GitHub
parent 4f0daec63d
commit 06a4feb9f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 116 additions and 75 deletions

View File

@ -35,51 +35,50 @@ theme = "Visual Studio Dark+"
# Keybindings # Keybindings
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# #
# Channel mode keybindings # Channel mode
# ------------------------
[keybindings.Channel] [keybindings.Channel]
# Quit the application # Quit the application
esc = "Quit" Quit = "esc"
# Scrolling through entries # Scrolling through entries
down = "SelectNextEntry" SelectNextEntry = "down"
ctrl-n = "SelectNextEntry" SelectPrevEntry = "up"
up = "SelectPrevEntry"
ctrl-p = "SelectPrevEntry"
# Scrolling the preview pane # Scrolling the preview pane
ctrl-d = "ScrollPreviewHalfPageDown" ScrollPreviewHalfPageDown = "ctrl-d"
ctrl-u = "ScrollPreviewHalfPageUp" ScrollPreviewHalfPageUp = "ctrl-u"
# Select an entry # Select an entry
enter = "SelectEntry" SelectEntry = "enter"
# Copy the selected entry to the clipboard # Copy the selected entry to the clipboard
ctrl-y = "CopyEntryToClipboard" CopyEntryToClipboard = "ctrl-y"
# Toggle the remote control mode # Toggle the remote control mode
ctrl-r = "ToggleRemoteControl" ToggleRemoteControl = "ctrl-r"
# Toggle the send to channel mode # Toggle the send to channel mode
ctrl-s = "ToggleSendToChannel" ToggleSendToChannel = "ctrl-s"
# Remote control mode keybindings
# Remote control mode
# -------------------------------
[keybindings.RemoteControl] [keybindings.RemoteControl]
# Quit the application # Quit the application
esc = "Quit" Quit = "esc"
# Scrolling through entries # Scrolling through entries
down = "SelectNextEntry" SelectNextEntry = "down"
up = "SelectPrevEntry" SelectPrevEntry = "up"
ctrl-n = "SelectNextEntry"
ctrl-p = "SelectPrevEntry"
# Select an entry # Select an entry
enter = "SelectEntry" SelectEntry = "enter"
# Toggle the remote control mode # Toggle the remote control mode
ctrl-r = "ToggleRemoteControl" ToggleRemoteControl = "ctrl-r"
# Send to channel mode keybindings
# Send to channel mode
# --------------------------------
[keybindings.SendToChannel] [keybindings.SendToChannel]
# Quit the application # Quit the application
esc = "Quit" Quit = "esc"
# Scrolling through entries # Scrolling through entries
down = "SelectNextEntry" SelectNextEntry = "down"
up = "SelectPrevEntry" SelectPrevEntry = "up"
ctrl-n = "SelectNextEntry"
ctrl-p = "SelectPrevEntry"
# Select an entry # Select an entry
enter = "SelectEntry" SelectEntry = "enter"
# Toggle the send to channel mode # Toggle the send to channel mode
ctrl-s = "ToggleSendToChannel" ToggleSendToChannel = "ctrl-s"

View File

@ -2,7 +2,9 @@ use serde::{Deserialize, Serialize};
use strum::Display; use strum::Display;
/// The different actions that can be performed by the application. /// 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 { pub enum Action {
// input actions // input actions
/// Add a character to the input buffer. /// Add a character to the input buffer.

View File

@ -1,9 +1,12 @@
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use color_eyre::Result; use color_eyre::Result;
use derive_deref::Deref;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tracing::{debug, info}; use tracing::{debug, info};
use crate::config::KeyBindings;
use crate::television::{Mode, Television}; use crate::television::{Mode, Television};
use crate::{ use crate::{
action::Action, action::Action,
@ -14,10 +17,28 @@ use crate::{
use television_channels::channels::TelevisionChannel; use television_channels::channels::TelevisionChannel;
use television_channels::entry::Entry; use television_channels::entry::Entry;
#[derive(Deref, Default)]
pub struct Keymap(pub HashMap<Mode, HashMap<Key, Action>>);
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. /// The main application struct that holds the state of the application.
pub struct App { pub struct App {
/// The configuration of the application. /// The configuration of the application.
config: Config, config: Config,
keymap: Keymap,
// maybe move these two into config instead of passing them // maybe move these two into config instead of passing them
// via the cli? // via the cli?
tick_rate: f64, tick_rate: f64,
@ -51,14 +72,17 @@ impl App {
let (_, event_rx) = mpsc::unbounded_channel(); let (_, event_rx) = mpsc::unbounded_channel();
let (event_abort_tx, _) = mpsc::unbounded_channel(); let (event_abort_tx, _) = mpsc::unbounded_channel();
let television = Arc::new(Mutex::new(Television::new(channel))); let television = Arc::new(Mutex::new(Television::new(channel)));
let config = Config::new()?;
let keymap = Keymap::from(&config.keybindings);
Ok(Self { Ok(Self {
config,
keymap,
tick_rate, tick_rate,
frame_rate, frame_rate,
television, television,
should_quit: false, should_quit: false,
should_suspend: false, should_suspend: false,
config: Config::new()?,
action_tx, action_tx,
action_rx, action_rx,
event_rx, event_rx,
@ -159,8 +183,7 @@ impl App {
_ => {} _ => {}
} }
// get action based on keybindings // get action based on keybindings
self.config self.keymap
.keybindings
.get(&self.television.lock().await.mode) .get(&self.television.lock().await.mode)
.and_then(|keymap| keymap.get(&keycode).cloned()) .and_then(|keymap| keymap.get(&keycode).cloned())
.unwrap_or(if let Key::Char(c) = keycode { .unwrap_or(if let Key::Char(c) = keycode {

View File

@ -6,7 +6,7 @@ use crate::{
event::{convert_raw_event_to_key, Key}, event::{convert_raw_event_to_key, Key},
television::Mode, television::Mode,
}; };
use color_eyre::Result; use color_eyre::{eyre::Context, Result};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use derive_deref::{Deref, DerefMut}; use derive_deref::{Deref, DerefMut};
use directories::ProjectDirs; use directories::ProjectDirs;
@ -113,47 +113,56 @@ const CONFIG_FILE_NAME: &str = "config.toml";
impl Config { impl Config {
#[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)] #[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
pub fn new() -> Result<Self, config::ConfigError> { pub fn new() -> Result<Self> {
// Load the default_config values as base defaults
let default_config: Config = let default_config: Config =
toml::from_str(CONFIG).expect("default config should be valid"); toml::from_str(CONFIG).expect("default config should be valid");
// initialize the config builder
let data_dir = get_data_dir(); let data_dir = get_data_dir();
let config_dir = get_config_dir(); let config_dir = get_config_dir();
let mut builder = config::Config::builder() let mut builder = config::Config::builder()
.set_default("data_dir", data_dir.to_str().unwrap())? .set_default("data_dir", data_dir.to_str().unwrap())?
.set_default("config_dir", config_dir.to_str().unwrap())?; .set_default("config_dir", config_dir.to_str().unwrap())?;
// Load the default_config values as base defaults // Load the user's config file
builder = builder.add_source(config::File::from_str(
CONFIG,
config::FileFormat::Toml,
));
let source = config::File::from(config_dir.join(CONFIG_FILE_NAME)) let source = config::File::from(config_dir.join(CONFIG_FILE_NAME))
.format(config::FileFormat::Toml) .format(config::FileFormat::Toml)
.required(false); .required(false);
builder = builder.add_source(source); 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); 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<ProjectDirs> {
} }
#[derive(Clone, Debug, Default, Deref, DerefMut)] #[derive(Clone, Debug, Default, Deref, DerefMut)]
pub struct KeyBindings(pub HashMap<Mode, HashMap<Key, Action>>); pub struct KeyBindings(pub config::Map<Mode, config::Map<Action, Key>>);
impl<'de> Deserialize<'de> for KeyBindings { impl<'de> Deserialize<'de> for KeyBindings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
@ -196,7 +205,7 @@ impl<'de> Deserialize<'de> for KeyBindings {
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let parsed_map = let parsed_map =
HashMap::<Mode, HashMap<String, Action>>::deserialize( HashMap::<Mode, HashMap<Action, String>>::deserialize(
deserializer, deserializer,
)?; )?;
@ -205,7 +214,7 @@ impl<'de> Deserialize<'de> for KeyBindings {
.map(|(mode, inner_map)| { .map(|(mode, inner_map)| {
let converted_inner_map = inner_map let converted_inner_map = inner_map
.into_iter() .into_iter()
.map(|(key_str, cmd)| (parse_key(&key_str).unwrap(), cmd)) .map(|(cmd, key_str)| (cmd, parse_key(&key_str).unwrap()))
.collect(); .collect();
(mode, converted_inner_map) (mode, converted_inner_map)
}) })
@ -573,9 +582,8 @@ mod tests {
c.keybindings c.keybindings
.get(&Mode::Channel) .get(&Mode::Channel)
.unwrap() .unwrap()
.get(&parse_key("esc").unwrap()) .get(&Action::Quit),
.unwrap(), Some(&parse_key("esc").unwrap())
&Action::Quit
); );
Ok(()) Ok(())
} }

View File

@ -30,7 +30,7 @@ async fn main() -> Result<()> {
let args = Cli::parse(); let args = Cli::parse();
let mut app: App = App::new( match App::new(
{ {
if is_readable_stdin() { if is_readable_stdin() {
debug!("Using stdin channel"); debug!("Using stdin channel");
@ -42,13 +42,19 @@ async fn main() -> Result<()> {
}, },
args.tick_rate, args.tick_rate,
args.frame_rate, args.frame_rate,
)?; ) {
Ok(mut app) => {
if let Some(entry) = app.run(stdout().is_terminal()).await? { if let Some(entry) = app.run(stdout().is_terminal()).await? {
// print entry to stdout // print entry to stdout
stdout().flush()?; stdout().flush()?;
info!("{:?}", entry); info!("{:?}", entry);
writeln!(stdout(), "{}", entry.stdout_repr())?; writeln!(stdout(), "{}", entry.stdout_repr())?;
}
Ok(())
}
Err(err) => {
println!("{err:?}");
return Ok(());
}
} }
Ok(())
} }

View File

@ -1,3 +1,4 @@
use crate::app::Keymap;
use crate::picker::Picker; use crate::picker::Picker;
use crate::ui::input::actions::InputActionHandler; use crate::ui::input::actions::InputActionHandler;
use crate::ui::layout::{Dimensions, Layout}; use crate::ui::layout::{Dimensions, Layout};
@ -32,6 +33,7 @@ pub enum Mode {
pub struct Television { pub struct Television {
action_tx: Option<UnboundedSender<Action>>, action_tx: Option<UnboundedSender<Action>>,
pub config: Config, pub config: Config,
pub keymap: Keymap,
pub(crate) channel: TelevisionChannel, pub(crate) channel: TelevisionChannel,
pub(crate) remote_control: TelevisionChannel, pub(crate) remote_control: TelevisionChannel,
pub mode: Mode, pub mode: Mode,
@ -62,6 +64,7 @@ impl Television {
Self { Self {
action_tx: None, action_tx: None,
config: Config::default(), config: Config::default(),
keymap: Keymap::default(),
channel, channel,
remote_control: TelevisionChannel::RemoteControl( remote_control: TelevisionChannel::RemoteControl(
RemoteControl::default(), RemoteControl::default(),
@ -228,6 +231,7 @@ impl Television {
/// * `Result<()>` - An Ok result or an error. /// * `Result<()>` - An Ok result or an error.
pub fn register_config_handler(&mut self, config: Config) -> Result<()> { pub fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config; self.config = config;
self.keymap = Keymap::from(&self.config.keybindings);
let previewer_config = let previewer_config =
std::convert::Into::<previewers::PreviewerConfig>::into( std::convert::Into::<previewers::PreviewerConfig>::into(
self.config.previewers.clone(), self.config.previewers.clone(),

View File

@ -211,8 +211,7 @@ impl Television {
/// A reference to the keymap for the current mode. /// A reference to the keymap for the current mode.
fn keymap_for_mode(&self) -> Result<&HashMap<Key, Action>> { fn keymap_for_mode(&self) -> Result<&HashMap<Key, Action>> {
let keymap = self let keymap = self
.config .keymap
.keybindings
.get(&self.mode) .get(&self.mode)
.ok_or_eyre("No keybindings found for the current Mode")?; .ok_or_eyre("No keybindings found for the current Mode")?;
Ok(keymap) Ok(keymap)