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
# ----------------------------------------------------------------------------
#
# 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"

View File

@ -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.

View File

@ -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<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.
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 {

View File

@ -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,39 +113,43 @@ const CONFIG_FILE_NAME: &str = "config.toml";
impl Config {
#[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 =
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() {
warn!("No config file found at {:?}", config_dir);
}
let mut cfg: Self = builder.build()?.try_deserialize()?;
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 (key, cmd) in default_bindings {
user_bindings.entry(*key).or_insert_with(|| cmd.clone());
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 {
@ -153,7 +157,12 @@ impl Config {
}
}
debug!("Config: {:?}", cfg);
Ok(cfg)
} else {
warn!("No config file found at {:?}", config_dir);
Ok(default_config)
}
}
}
@ -188,7 +197,7 @@ fn project_directory() -> Option<ProjectDirs> {
}
#[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 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
@ -196,7 +205,7 @@ impl<'de> Deserialize<'de> for KeyBindings {
D: Deserializer<'de>,
{
let parsed_map =
HashMap::<Mode, HashMap<String, Action>>::deserialize(
HashMap::<Mode, HashMap<Action, String>>::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(())
}

View File

@ -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,8 +42,8 @@ async fn main() -> Result<()> {
},
args.tick_rate,
args.frame_rate,
)?;
) {
Ok(mut app) => {
if let Some(entry) = app.run(stdout().is_terminal()).await? {
// print entry to stdout
stdout().flush()?;
@ -52,3 +52,9 @@ async fn main() -> Result<()> {
}
Ok(())
}
Err(err) => {
println!("{err:?}");
return Ok(());
}
}
}

View File

@ -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<UnboundedSender<Action>>,
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::<previewers::PreviewerConfig>::into(
self.config.previewers.clone(),

View File

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