mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-22 01:50:00 +00:00
refactor(configuration): modularize code and better handling of default options (#32)
* fix(config): set ui default configuration values * refactor(configuration): modularize code and better handling of default options
This commit is contained in:
parent
d0f023cf18
commit
aa2f2609a4
@ -1,20 +1,20 @@
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
use std::{collections::HashMap, env, path::PathBuf};
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
event::{convert_raw_event_to_key, Key},
|
||||
television::Mode,
|
||||
};
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use directories::ProjectDirs;
|
||||
pub use keybindings::KeyBindings;
|
||||
use lazy_static::lazy_static;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use serde::{de::Deserializer, Deserialize};
|
||||
use television_previewers::previewers::{self, PreviewerConfig};
|
||||
use previewers::PreviewersConfig;
|
||||
use serde::Deserialize;
|
||||
use styles::Styles;
|
||||
use tracing::{debug, warn};
|
||||
use ui::UiConfig;
|
||||
|
||||
mod keybindings;
|
||||
mod previewers;
|
||||
mod styles;
|
||||
mod ui;
|
||||
|
||||
const CONFIG: &str = include_str!("../../.config/config.toml");
|
||||
|
||||
@ -27,56 +27,6 @@ pub struct AppConfig {
|
||||
pub config_dir: PathBuf,
|
||||
}
|
||||
|
||||
const DEFAULT_UI_SCALE: u16 = 90;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct UiConfig {
|
||||
pub use_nerd_font_icons: bool,
|
||||
pub ui_scale: u16,
|
||||
}
|
||||
|
||||
impl Default for UiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
use_nerd_font_icons: false,
|
||||
ui_scale: DEFAULT_UI_SCALE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct PreviewersConfig {
|
||||
#[serde(default)]
|
||||
pub basic: BasicPreviewerConfig,
|
||||
#[serde(default)]
|
||||
pub directory: DirectoryPreviewerConfig,
|
||||
pub file: FilePreviewerConfig,
|
||||
#[serde(default)]
|
||||
pub env_var: EnvVarPreviewerConfig,
|
||||
}
|
||||
|
||||
impl From<PreviewersConfig> for PreviewerConfig {
|
||||
fn from(val: PreviewersConfig) -> Self {
|
||||
PreviewerConfig::default()
|
||||
.file(previewers::FilePreviewerConfig::new(val.file.theme.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct BasicPreviewerConfig {}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct DirectoryPreviewerConfig {}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct FilePreviewerConfig {
|
||||
//pub max_file_size: u64,
|
||||
pub theme: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct EnvVarPreviewerConfig {}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct Config {
|
||||
@ -87,6 +37,7 @@ pub struct Config {
|
||||
pub keybindings: KeyBindings,
|
||||
#[serde(default)]
|
||||
pub styles: Styles,
|
||||
#[serde(default)]
|
||||
pub ui: UiConfig,
|
||||
#[serde(default)]
|
||||
pub previewers: PreviewersConfig,
|
||||
@ -123,7 +74,9 @@ impl Config {
|
||||
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())?;
|
||||
.set_default("config_dir", config_dir.to_str().unwrap())?
|
||||
.set_default("ui", UiConfig::default())?
|
||||
.set_default("previewers", PreviewersConfig::default())?;
|
||||
|
||||
// Load the user's config file
|
||||
let source = config::File::from(config_dir.join(CONFIG_FILE_NAME))
|
||||
@ -196,384 +149,13 @@ fn project_directory() -> Option<ProjectDirs> {
|
||||
ProjectDirs::from("com", "", env!("CARGO_PKG_NAME"))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
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>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let parsed_map =
|
||||
HashMap::<Mode, HashMap<Action, String>>::deserialize(
|
||||
deserializer,
|
||||
)?;
|
||||
|
||||
let keybindings = parsed_map
|
||||
.into_iter()
|
||||
.map(|(mode, inner_map)| {
|
||||
let converted_inner_map = inner_map
|
||||
.into_iter()
|
||||
.map(|(cmd, key_str)| (cmd, parse_key(&key_str).unwrap()))
|
||||
.collect();
|
||||
(mode, converted_inner_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(KeyBindings(keybindings))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
|
||||
let raw_lower = raw.to_ascii_lowercase();
|
||||
let (remaining, modifiers) = extract_modifiers(&raw_lower);
|
||||
parse_key_code_with_modifiers(remaining, modifiers)
|
||||
}
|
||||
|
||||
fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
|
||||
let mut modifiers = KeyModifiers::empty();
|
||||
let mut current = raw;
|
||||
|
||||
loop {
|
||||
match current {
|
||||
rest if rest.starts_with("ctrl-") => {
|
||||
modifiers.insert(KeyModifiers::CONTROL);
|
||||
current = &rest[5..];
|
||||
}
|
||||
rest if rest.starts_with("alt-") => {
|
||||
modifiers.insert(KeyModifiers::ALT);
|
||||
current = &rest[4..];
|
||||
}
|
||||
rest if rest.starts_with("shift-") => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
current = &rest[6..];
|
||||
}
|
||||
_ => break, // break out of the loop if no known prefix is detected
|
||||
};
|
||||
}
|
||||
|
||||
(current, modifiers)
|
||||
}
|
||||
|
||||
fn parse_key_code_with_modifiers(
|
||||
raw: &str,
|
||||
mut modifiers: KeyModifiers,
|
||||
) -> Result<KeyEvent, String> {
|
||||
let c = match raw {
|
||||
"esc" => KeyCode::Esc,
|
||||
"enter" => KeyCode::Enter,
|
||||
"left" => KeyCode::Left,
|
||||
"right" => KeyCode::Right,
|
||||
"up" => KeyCode::Up,
|
||||
"down" => KeyCode::Down,
|
||||
"home" => KeyCode::Home,
|
||||
"end" => KeyCode::End,
|
||||
"pageup" => KeyCode::PageUp,
|
||||
"pagedown" => KeyCode::PageDown,
|
||||
"backtab" => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
KeyCode::BackTab
|
||||
}
|
||||
"backspace" => KeyCode::Backspace,
|
||||
"delete" => KeyCode::Delete,
|
||||
"insert" => KeyCode::Insert,
|
||||
"f1" => KeyCode::F(1),
|
||||
"f2" => KeyCode::F(2),
|
||||
"f3" => KeyCode::F(3),
|
||||
"f4" => KeyCode::F(4),
|
||||
"f5" => KeyCode::F(5),
|
||||
"f6" => KeyCode::F(6),
|
||||
"f7" => KeyCode::F(7),
|
||||
"f8" => KeyCode::F(8),
|
||||
"f9" => KeyCode::F(9),
|
||||
"f10" => KeyCode::F(10),
|
||||
"f11" => KeyCode::F(11),
|
||||
"f12" => KeyCode::F(12),
|
||||
"space" => KeyCode::Char(' '),
|
||||
"hyphen" | "minus" => KeyCode::Char('-'),
|
||||
"tab" => KeyCode::Tab,
|
||||
c if c.len() == 1 => {
|
||||
let mut c = c.chars().next().unwrap();
|
||||
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||
c = c.to_ascii_uppercase();
|
||||
}
|
||||
KeyCode::Char(c)
|
||||
}
|
||||
_ => return Err(format!("Unable to parse {raw}")),
|
||||
};
|
||||
Ok(KeyEvent::new(c, modifiers))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
|
||||
let char;
|
||||
let key_code = match key_event.code {
|
||||
KeyCode::Backspace => "backspace",
|
||||
KeyCode::Enter => "enter",
|
||||
KeyCode::Left => "left",
|
||||
KeyCode::Right => "right",
|
||||
KeyCode::Up => "up",
|
||||
KeyCode::Down => "down",
|
||||
KeyCode::Home => "home",
|
||||
KeyCode::End => "end",
|
||||
KeyCode::PageUp => "pageup",
|
||||
KeyCode::PageDown => "pagedown",
|
||||
KeyCode::Tab => "tab",
|
||||
KeyCode::BackTab => "backtab",
|
||||
KeyCode::Delete => "delete",
|
||||
KeyCode::Insert => "insert",
|
||||
KeyCode::F(c) => {
|
||||
char = format!("f({c})");
|
||||
&char
|
||||
}
|
||||
KeyCode::Char(' ') => "space",
|
||||
KeyCode::Char(c) => {
|
||||
char = c.to_string();
|
||||
&char
|
||||
}
|
||||
KeyCode::Esc => "esc",
|
||||
KeyCode::Null
|
||||
| KeyCode::CapsLock
|
||||
| KeyCode::Menu
|
||||
| KeyCode::ScrollLock
|
||||
| KeyCode::Media(_)
|
||||
| KeyCode::NumLock
|
||||
| KeyCode::PrintScreen
|
||||
| KeyCode::Pause
|
||||
| KeyCode::KeypadBegin
|
||||
| KeyCode::Modifier(_) => "",
|
||||
};
|
||||
|
||||
let mut modifiers = Vec::with_capacity(3);
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
modifiers.push("ctrl");
|
||||
}
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
|
||||
modifiers.push("shift");
|
||||
}
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::ALT) {
|
||||
modifiers.push("alt");
|
||||
}
|
||||
|
||||
let mut key = modifiers.join("-");
|
||||
|
||||
if !key.is_empty() {
|
||||
key.push('-');
|
||||
}
|
||||
key.push_str(key_code);
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
pub fn parse_key(raw: &str) -> Result<Key, String> {
|
||||
if raw.chars().filter(|c| *c == '>').count()
|
||||
!= raw.chars().filter(|c| *c == '<').count()
|
||||
{
|
||||
return Err(format!("Unable to parse `{raw}`"));
|
||||
}
|
||||
let raw = if raw.contains("><") {
|
||||
raw
|
||||
} else {
|
||||
let raw = raw.strip_prefix('<').unwrap_or(raw);
|
||||
let raw = raw.strip_suffix('>').unwrap_or(raw);
|
||||
raw
|
||||
};
|
||||
let key_event = parse_key_event(raw)?;
|
||||
Ok(convert_raw_event_to_key(key_event))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
|
||||
|
||||
impl<'de> Deserialize<'de> for Styles {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let parsed_map =
|
||||
HashMap::<Mode, HashMap<String, String>>::deserialize(
|
||||
deserializer,
|
||||
)?;
|
||||
|
||||
let styles = parsed_map
|
||||
.into_iter()
|
||||
.map(|(mode, inner_map)| {
|
||||
let converted_inner_map = inner_map
|
||||
.into_iter()
|
||||
.map(|(str, style)| (str, parse_style(&style)))
|
||||
.collect();
|
||||
(mode, converted_inner_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Styles(styles))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_style(line: &str) -> Style {
|
||||
let (foreground, background) =
|
||||
line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
|
||||
let foreground = process_color_string(foreground);
|
||||
let background = process_color_string(&background.replace("on ", ""));
|
||||
|
||||
let mut style = Style::default();
|
||||
if let Some(fg) = parse_color(&foreground.0) {
|
||||
style = style.fg(fg);
|
||||
}
|
||||
if let Some(bg) = parse_color(&background.0) {
|
||||
style = style.bg(bg);
|
||||
}
|
||||
style = style.add_modifier(foreground.1 | background.1);
|
||||
style
|
||||
}
|
||||
|
||||
fn process_color_string(color_str: &str) -> (String, Modifier) {
|
||||
let color = color_str
|
||||
.replace("grey", "gray")
|
||||
.replace("bright ", "")
|
||||
.replace("bold ", "")
|
||||
.replace("underline ", "")
|
||||
.replace("inverse ", "");
|
||||
|
||||
let mut modifiers = Modifier::empty();
|
||||
if color_str.contains("underline") {
|
||||
modifiers |= Modifier::UNDERLINED;
|
||||
}
|
||||
if color_str.contains("bold") {
|
||||
modifiers |= Modifier::BOLD;
|
||||
}
|
||||
if color_str.contains("inverse") {
|
||||
modifiers |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
(color, modifiers)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn parse_color(s: &str) -> Option<Color> {
|
||||
let s = s.trim_start();
|
||||
let s = s.trim_end();
|
||||
if s.contains("bright color") {
|
||||
let s = s.trim_start_matches("bright ");
|
||||
let c = s
|
||||
.trim_start_matches("color")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c.wrapping_shl(8)))
|
||||
} else if s.contains("color") {
|
||||
let c = s
|
||||
.trim_start_matches("color")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c))
|
||||
} else if s.contains("gray") {
|
||||
let c = 232
|
||||
+ s.trim_start_matches("gray")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c))
|
||||
} else if s.contains("rgb") {
|
||||
let red =
|
||||
(s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let green =
|
||||
(s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let blue =
|
||||
(s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let c = 16 + red * 36 + green * 6 + blue;
|
||||
Some(Color::Indexed(c))
|
||||
} else if s == "bold black" {
|
||||
Some(Color::Indexed(8))
|
||||
} else if s == "bold red" {
|
||||
Some(Color::Indexed(9))
|
||||
} else if s == "bold green" {
|
||||
Some(Color::Indexed(10))
|
||||
} else if s == "bold yellow" {
|
||||
Some(Color::Indexed(11))
|
||||
} else if s == "bold blue" {
|
||||
Some(Color::Indexed(12))
|
||||
} else if s == "bold magenta" {
|
||||
Some(Color::Indexed(13))
|
||||
} else if s == "bold cyan" {
|
||||
Some(Color::Indexed(14))
|
||||
} else if s == "bold white" {
|
||||
Some(Color::Indexed(15))
|
||||
} else if s == "black" {
|
||||
Some(Color::Indexed(0))
|
||||
} else if s == "red" {
|
||||
Some(Color::Indexed(1))
|
||||
} else if s == "green" {
|
||||
Some(Color::Indexed(2))
|
||||
} else if s == "yellow" {
|
||||
Some(Color::Indexed(3))
|
||||
} else if s == "blue" {
|
||||
Some(Color::Indexed(4))
|
||||
} else if s == "magenta" {
|
||||
Some(Color::Indexed(5))
|
||||
} else if s == "cyan" {
|
||||
Some(Color::Indexed(6))
|
||||
} else if s == "white" {
|
||||
Some(Color::Indexed(7))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_default() {
|
||||
let style = parse_style("");
|
||||
assert_eq!(style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_foreground() {
|
||||
let style = parse_style("red");
|
||||
assert_eq!(style.fg, Some(Color::Indexed(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_background() {
|
||||
let style = parse_style("on blue");
|
||||
assert_eq!(style.bg, Some(Color::Indexed(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_modifiers() {
|
||||
let style = parse_style("underline red on blue");
|
||||
assert_eq!(style.fg, Some(Color::Indexed(1)));
|
||||
assert_eq!(style.bg, Some(Color::Indexed(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_color_string() {
|
||||
let (color, modifiers) =
|
||||
process_color_string("underline bold inverse gray");
|
||||
assert_eq!(color, "gray");
|
||||
assert!(modifiers.contains(Modifier::UNDERLINED));
|
||||
assert!(modifiers.contains(Modifier::BOLD));
|
||||
assert!(modifiers.contains(Modifier::REVERSED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_color_rgb() {
|
||||
let color = parse_color("rgb123");
|
||||
let expected = 16 + 36 + 2 * 6 + 3;
|
||||
assert_eq!(color, Some(Color::Indexed(expected)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_color_unknown() {
|
||||
let color = parse_color("unknown");
|
||||
assert_eq!(color, None);
|
||||
}
|
||||
use crate::action::Action;
|
||||
use crate::config::keybindings::parse_key;
|
||||
use crate::television::Mode;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_config() -> Result<()> {
|
||||
@ -587,89 +169,4 @@ mod tests {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_keys() {
|
||||
assert_eq!(
|
||||
parse_key_event("a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("esc").unwrap(),
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_modifiers() {
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("alt-enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("shift-esc").unwrap(),
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_modifiers() {
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-alt-a").unwrap(),
|
||||
KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-shift-enter").unwrap(),
|
||||
KeyEvent::new(
|
||||
KeyCode::Enter,
|
||||
KeyModifiers::CONTROL | KeyModifiers::SHIFT
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_multiple_modifiers() {
|
||||
assert_eq!(
|
||||
key_event_to_string(&KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
)),
|
||||
"ctrl-alt-a".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_keys() {
|
||||
assert!(parse_key_event("invalid-key").is_err());
|
||||
assert!(parse_key_event("ctrl-invalid-key").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_insensitivity() {
|
||||
assert_eq!(
|
||||
parse_key_event("CTRL-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("AlT-eNtEr").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
286
crates/television/config/keybindings.rs
Normal file
286
crates/television/config/keybindings.rs
Normal file
@ -0,0 +1,286 @@
|
||||
use crate::action::Action;
|
||||
use crate::event::{convert_raw_event_to_key, Key};
|
||||
use crate::television::Mode;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct KeyBindings(pub config::Map<Mode, config::Map<Action, Key>>);
|
||||
|
||||
impl<'de> Deserialize<'de> for KeyBindings {
|
||||
fn deserialize<D>(deserializer: D) -> color_eyre::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let parsed_map =
|
||||
HashMap::<Mode, HashMap<Action, String>>::deserialize(
|
||||
deserializer,
|
||||
)?;
|
||||
|
||||
let keybindings = parsed_map
|
||||
.into_iter()
|
||||
.map(|(mode, inner_map)| {
|
||||
let converted_inner_map = inner_map
|
||||
.into_iter()
|
||||
.map(|(cmd, key_str)| (cmd, parse_key(&key_str).unwrap()))
|
||||
.collect();
|
||||
(mode, converted_inner_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(KeyBindings(keybindings))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_key_event(raw: &str) -> color_eyre::Result<KeyEvent, String> {
|
||||
let raw_lower = raw.to_ascii_lowercase();
|
||||
let (remaining, modifiers) = extract_modifiers(&raw_lower);
|
||||
parse_key_code_with_modifiers(remaining, modifiers)
|
||||
}
|
||||
|
||||
fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
|
||||
let mut modifiers = KeyModifiers::empty();
|
||||
let mut current = raw;
|
||||
|
||||
loop {
|
||||
match current {
|
||||
rest if rest.starts_with("ctrl-") => {
|
||||
modifiers.insert(KeyModifiers::CONTROL);
|
||||
current = &rest[5..];
|
||||
}
|
||||
rest if rest.starts_with("alt-") => {
|
||||
modifiers.insert(KeyModifiers::ALT);
|
||||
current = &rest[4..];
|
||||
}
|
||||
rest if rest.starts_with("shift-") => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
current = &rest[6..];
|
||||
}
|
||||
_ => break, // break out of the loop if no known prefix is detected
|
||||
};
|
||||
}
|
||||
|
||||
(current, modifiers)
|
||||
}
|
||||
|
||||
fn parse_key_code_with_modifiers(
|
||||
raw: &str,
|
||||
mut modifiers: KeyModifiers,
|
||||
) -> color_eyre::Result<KeyEvent, String> {
|
||||
let c = match raw {
|
||||
"esc" => KeyCode::Esc,
|
||||
"enter" => KeyCode::Enter,
|
||||
"left" => KeyCode::Left,
|
||||
"right" => KeyCode::Right,
|
||||
"up" => KeyCode::Up,
|
||||
"down" => KeyCode::Down,
|
||||
"home" => KeyCode::Home,
|
||||
"end" => KeyCode::End,
|
||||
"pageup" => KeyCode::PageUp,
|
||||
"pagedown" => KeyCode::PageDown,
|
||||
"backtab" => {
|
||||
modifiers.insert(KeyModifiers::SHIFT);
|
||||
KeyCode::BackTab
|
||||
}
|
||||
"backspace" => KeyCode::Backspace,
|
||||
"delete" => KeyCode::Delete,
|
||||
"insert" => KeyCode::Insert,
|
||||
"f1" => KeyCode::F(1),
|
||||
"f2" => KeyCode::F(2),
|
||||
"f3" => KeyCode::F(3),
|
||||
"f4" => KeyCode::F(4),
|
||||
"f5" => KeyCode::F(5),
|
||||
"f6" => KeyCode::F(6),
|
||||
"f7" => KeyCode::F(7),
|
||||
"f8" => KeyCode::F(8),
|
||||
"f9" => KeyCode::F(9),
|
||||
"f10" => KeyCode::F(10),
|
||||
"f11" => KeyCode::F(11),
|
||||
"f12" => KeyCode::F(12),
|
||||
"space" => KeyCode::Char(' '),
|
||||
"hyphen" | "minus" => KeyCode::Char('-'),
|
||||
"tab" => KeyCode::Tab,
|
||||
c if c.len() == 1 => {
|
||||
let mut c = c.chars().next().unwrap();
|
||||
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||
c = c.to_ascii_uppercase();
|
||||
}
|
||||
KeyCode::Char(c)
|
||||
}
|
||||
_ => return Err(format!("Unable to parse {raw}")),
|
||||
};
|
||||
Ok(KeyEvent::new(c, modifiers))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
|
||||
let char;
|
||||
let key_code = match key_event.code {
|
||||
KeyCode::Backspace => "backspace",
|
||||
KeyCode::Enter => "enter",
|
||||
KeyCode::Left => "left",
|
||||
KeyCode::Right => "right",
|
||||
KeyCode::Up => "up",
|
||||
KeyCode::Down => "down",
|
||||
KeyCode::Home => "home",
|
||||
KeyCode::End => "end",
|
||||
KeyCode::PageUp => "pageup",
|
||||
KeyCode::PageDown => "pagedown",
|
||||
KeyCode::Tab => "tab",
|
||||
KeyCode::BackTab => "backtab",
|
||||
KeyCode::Delete => "delete",
|
||||
KeyCode::Insert => "insert",
|
||||
KeyCode::F(c) => {
|
||||
char = format!("f({c})");
|
||||
&char
|
||||
}
|
||||
KeyCode::Char(' ') => "space",
|
||||
KeyCode::Char(c) => {
|
||||
char = c.to_string();
|
||||
&char
|
||||
}
|
||||
KeyCode::Esc => "esc",
|
||||
KeyCode::Null
|
||||
| KeyCode::CapsLock
|
||||
| KeyCode::Menu
|
||||
| KeyCode::ScrollLock
|
||||
| KeyCode::Media(_)
|
||||
| KeyCode::NumLock
|
||||
| KeyCode::PrintScreen
|
||||
| KeyCode::Pause
|
||||
| KeyCode::KeypadBegin
|
||||
| KeyCode::Modifier(_) => "",
|
||||
};
|
||||
|
||||
let mut modifiers = Vec::with_capacity(3);
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
modifiers.push("ctrl");
|
||||
}
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
|
||||
modifiers.push("shift");
|
||||
}
|
||||
|
||||
if key_event.modifiers.intersects(KeyModifiers::ALT) {
|
||||
modifiers.push("alt");
|
||||
}
|
||||
|
||||
let mut key = modifiers.join("-");
|
||||
|
||||
if !key.is_empty() {
|
||||
key.push('-');
|
||||
}
|
||||
key.push_str(key_code);
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
pub fn parse_key(raw: &str) -> color_eyre::Result<Key, String> {
|
||||
if raw.chars().filter(|c| *c == '>').count()
|
||||
!= raw.chars().filter(|c| *c == '<').count()
|
||||
{
|
||||
return Err(format!("Unable to parse `{raw}`"));
|
||||
}
|
||||
let raw = if raw.contains("><") {
|
||||
raw
|
||||
} else {
|
||||
let raw = raw.strip_prefix('<').unwrap_or(raw);
|
||||
let raw = raw.strip_suffix('>').unwrap_or(raw);
|
||||
raw
|
||||
};
|
||||
let key_event = parse_key_event(raw)?;
|
||||
Ok(convert_raw_event_to_key(key_event))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simple_keys() {
|
||||
assert_eq!(
|
||||
parse_key_event("a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("esc").unwrap(),
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_modifiers() {
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("alt-enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("shift-esc").unwrap(),
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_modifiers() {
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-alt-a").unwrap(),
|
||||
KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("ctrl-shift-enter").unwrap(),
|
||||
KeyEvent::new(
|
||||
KeyCode::Enter,
|
||||
KeyModifiers::CONTROL | KeyModifiers::SHIFT
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_multiple_modifiers() {
|
||||
assert_eq!(
|
||||
key_event_to_string(&KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
)),
|
||||
"ctrl-alt-a".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_keys() {
|
||||
assert!(parse_key_event("invalid-key").is_err());
|
||||
assert!(parse_key_event("ctrl-invalid-key").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_insensitivity() {
|
||||
assert_eq!(
|
||||
parse_key_event("CTRL-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key_event("AlT-eNtEr").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
|
||||
);
|
||||
}
|
||||
}
|
75
crates/television/config/previewers.rs
Normal file
75
crates/television/config/previewers.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use config::ValueKind;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use television_previewers::previewers;
|
||||
use television_previewers::previewers::PreviewerConfig;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct PreviewersConfig {
|
||||
#[serde(default)]
|
||||
pub basic: BasicPreviewerConfig,
|
||||
#[serde(default)]
|
||||
pub directory: DirectoryPreviewerConfig,
|
||||
pub file: FilePreviewerConfig,
|
||||
#[serde(default)]
|
||||
pub env_var: EnvVarPreviewerConfig,
|
||||
}
|
||||
|
||||
impl From<PreviewersConfig> for PreviewerConfig {
|
||||
fn from(val: PreviewersConfig) -> Self {
|
||||
PreviewerConfig::default()
|
||||
.file(previewers::FilePreviewerConfig::new(val.file.theme.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PreviewersConfig> for ValueKind {
|
||||
fn from(val: PreviewersConfig) -> Self {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(String::from("basic"), val.basic.into());
|
||||
m.insert(String::from("directory"), val.directory.into());
|
||||
m.insert(String::from("file"), val.file.into());
|
||||
m.insert(String::from("env_var"), val.env_var.into());
|
||||
ValueKind::Table(m)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct BasicPreviewerConfig {}
|
||||
|
||||
impl From<BasicPreviewerConfig> for ValueKind {
|
||||
fn from(_val: BasicPreviewerConfig) -> Self {
|
||||
ValueKind::Table(HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct DirectoryPreviewerConfig {}
|
||||
|
||||
impl From<DirectoryPreviewerConfig> for ValueKind {
|
||||
fn from(_val: DirectoryPreviewerConfig) -> Self {
|
||||
ValueKind::Table(HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct FilePreviewerConfig {
|
||||
//pub max_file_size: u64,
|
||||
pub theme: String,
|
||||
}
|
||||
|
||||
impl From<FilePreviewerConfig> for ValueKind {
|
||||
fn from(val: FilePreviewerConfig) -> Self {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(String::from("theme"), ValueKind::String(val.theme).into());
|
||||
ValueKind::Table(m)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct EnvVarPreviewerConfig {}
|
||||
|
||||
impl From<EnvVarPreviewerConfig> for ValueKind {
|
||||
fn from(_val: EnvVarPreviewerConfig) -> Self {
|
||||
ValueKind::Table(HashMap::new())
|
||||
}
|
||||
}
|
194
crates/television/config/styles.rs
Normal file
194
crates/television/config/styles.rs
Normal file
@ -0,0 +1,194 @@
|
||||
use crate::television::Mode;
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use ratatui::prelude::{Color, Modifier, Style};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
|
||||
|
||||
impl<'de> Deserialize<'de> for Styles {
|
||||
fn deserialize<D>(deserializer: D) -> color_eyre::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let parsed_map =
|
||||
HashMap::<Mode, HashMap<String, String>>::deserialize(
|
||||
deserializer,
|
||||
)?;
|
||||
|
||||
let styles = parsed_map
|
||||
.into_iter()
|
||||
.map(|(mode, inner_map)| {
|
||||
let converted_inner_map = inner_map
|
||||
.into_iter()
|
||||
.map(|(str, style)| (str, parse_style(&style)))
|
||||
.collect();
|
||||
(mode, converted_inner_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Styles(styles))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_style(line: &str) -> Style {
|
||||
let (foreground, background) =
|
||||
line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
|
||||
let foreground = process_color_string(foreground);
|
||||
let background = process_color_string(&background.replace("on ", ""));
|
||||
|
||||
let mut style = Style::default();
|
||||
if let Some(fg) = parse_color(&foreground.0) {
|
||||
style = style.fg(fg);
|
||||
}
|
||||
if let Some(bg) = parse_color(&background.0) {
|
||||
style = style.bg(bg);
|
||||
}
|
||||
style = style.add_modifier(foreground.1 | background.1);
|
||||
style
|
||||
}
|
||||
|
||||
pub fn process_color_string(color_str: &str) -> (String, Modifier) {
|
||||
let color = color_str
|
||||
.replace("grey", "gray")
|
||||
.replace("bright ", "")
|
||||
.replace("bold ", "")
|
||||
.replace("underline ", "")
|
||||
.replace("inverse ", "");
|
||||
|
||||
let mut modifiers = Modifier::empty();
|
||||
if color_str.contains("underline") {
|
||||
modifiers |= Modifier::UNDERLINED;
|
||||
}
|
||||
if color_str.contains("bold") {
|
||||
modifiers |= Modifier::BOLD;
|
||||
}
|
||||
if color_str.contains("inverse") {
|
||||
modifiers |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
(color, modifiers)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn parse_color(s: &str) -> Option<Color> {
|
||||
let s = s.trim_start();
|
||||
let s = s.trim_end();
|
||||
if s.contains("bright color") {
|
||||
let s = s.trim_start_matches("bright ");
|
||||
let c = s
|
||||
.trim_start_matches("color")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c.wrapping_shl(8)))
|
||||
} else if s.contains("color") {
|
||||
let c = s
|
||||
.trim_start_matches("color")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c))
|
||||
} else if s.contains("gray") {
|
||||
let c = 232
|
||||
+ s.trim_start_matches("gray")
|
||||
.parse::<u8>()
|
||||
.unwrap_or_default();
|
||||
Some(Color::Indexed(c))
|
||||
} else if s.contains("rgb") {
|
||||
let red =
|
||||
(s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let green =
|
||||
(s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let blue =
|
||||
(s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
|
||||
let c = 16 + red * 36 + green * 6 + blue;
|
||||
Some(Color::Indexed(c))
|
||||
} else if s == "bold black" {
|
||||
Some(Color::Indexed(8))
|
||||
} else if s == "bold red" {
|
||||
Some(Color::Indexed(9))
|
||||
} else if s == "bold green" {
|
||||
Some(Color::Indexed(10))
|
||||
} else if s == "bold yellow" {
|
||||
Some(Color::Indexed(11))
|
||||
} else if s == "bold blue" {
|
||||
Some(Color::Indexed(12))
|
||||
} else if s == "bold magenta" {
|
||||
Some(Color::Indexed(13))
|
||||
} else if s == "bold cyan" {
|
||||
Some(Color::Indexed(14))
|
||||
} else if s == "bold white" {
|
||||
Some(Color::Indexed(15))
|
||||
} else if s == "black" {
|
||||
Some(Color::Indexed(0))
|
||||
} else if s == "red" {
|
||||
Some(Color::Indexed(1))
|
||||
} else if s == "green" {
|
||||
Some(Color::Indexed(2))
|
||||
} else if s == "yellow" {
|
||||
Some(Color::Indexed(3))
|
||||
} else if s == "blue" {
|
||||
Some(Color::Indexed(4))
|
||||
} else if s == "magenta" {
|
||||
Some(Color::Indexed(5))
|
||||
} else if s == "cyan" {
|
||||
Some(Color::Indexed(6))
|
||||
} else if s == "white" {
|
||||
Some(Color::Indexed(7))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_default() {
|
||||
let style = parse_style("");
|
||||
pretty_assertions::assert_eq!(style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_foreground() {
|
||||
let style = parse_style("red");
|
||||
pretty_assertions::assert_eq!(style.fg, Some(Color::Indexed(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_background() {
|
||||
let style = parse_style("on blue");
|
||||
pretty_assertions::assert_eq!(style.bg, Some(Color::Indexed(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_style_modifiers() {
|
||||
let style = parse_style("underline red on blue");
|
||||
pretty_assertions::assert_eq!(style.fg, Some(Color::Indexed(1)));
|
||||
pretty_assertions::assert_eq!(style.bg, Some(Color::Indexed(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_color_string() {
|
||||
let (color, modifiers) =
|
||||
process_color_string("underline bold inverse gray");
|
||||
pretty_assertions::assert_eq!(color, "gray");
|
||||
assert!(modifiers.contains(Modifier::UNDERLINED));
|
||||
assert!(modifiers.contains(Modifier::BOLD));
|
||||
assert!(modifiers.contains(Modifier::REVERSED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_color_rgb() {
|
||||
let color = parse_color("rgb123");
|
||||
let expected = 16 + 36 + 2 * 6 + 3;
|
||||
pretty_assertions::assert_eq!(color, Some(Color::Indexed(expected)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_color_unknown() {
|
||||
let color = parse_color("unknown");
|
||||
pretty_assertions::assert_eq!(color, None);
|
||||
}
|
||||
}
|
35
crates/television/config/ui.rs
Normal file
35
crates/television/config/ui.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use config::ValueKind;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const DEFAULT_UI_SCALE: u16 = 90;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct UiConfig {
|
||||
pub use_nerd_font_icons: bool,
|
||||
pub ui_scale: u16,
|
||||
}
|
||||
|
||||
impl Default for UiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
use_nerd_font_icons: false,
|
||||
ui_scale: DEFAULT_UI_SCALE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UiConfig> for ValueKind {
|
||||
fn from(val: UiConfig) -> Self {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(
|
||||
String::from("use_nerd_font_icons"),
|
||||
ValueKind::Boolean(val.use_nerd_font_icons).into(),
|
||||
);
|
||||
m.insert(
|
||||
String::from("ui_scale"),
|
||||
ValueKind::U64(val.ui_scale.into()).into(),
|
||||
);
|
||||
ValueKind::Table(m)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user