mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 11:35:25 +00:00
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:
parent
4f0daec63d
commit
06a4feb9f2
@ -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"
|
||||||
|
@ -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.
|
||||||
|
@ -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 {
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
@ -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(())
|
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user