mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-23 02:20:03 +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)]
|
#![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 color_eyre::{eyre::Context, Result};
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
||||||
use derive_deref::{Deref, DerefMut};
|
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
pub use keybindings::KeyBindings;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use previewers::PreviewersConfig;
|
||||||
use serde::{de::Deserializer, Deserialize};
|
use serde::Deserialize;
|
||||||
use television_previewers::previewers::{self, PreviewerConfig};
|
use styles::Styles;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
use ui::UiConfig;
|
||||||
|
|
||||||
|
mod keybindings;
|
||||||
|
mod previewers;
|
||||||
|
mod styles;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
const CONFIG: &str = include_str!("../../.config/config.toml");
|
const CONFIG: &str = include_str!("../../.config/config.toml");
|
||||||
|
|
||||||
@ -27,56 +27,6 @@ pub struct AppConfig {
|
|||||||
pub config_dir: PathBuf,
|
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)]
|
#[allow(dead_code)]
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@ -87,6 +37,7 @@ pub struct Config {
|
|||||||
pub keybindings: KeyBindings,
|
pub keybindings: KeyBindings,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub styles: Styles,
|
pub styles: Styles,
|
||||||
|
#[serde(default)]
|
||||||
pub ui: UiConfig,
|
pub ui: UiConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub previewers: PreviewersConfig,
|
pub previewers: PreviewersConfig,
|
||||||
@ -123,7 +74,9 @@ impl Config {
|
|||||||
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())?
|
||||||
|
.set_default("ui", UiConfig::default())?
|
||||||
|
.set_default("previewers", PreviewersConfig::default())?;
|
||||||
|
|
||||||
// Load the user's config file
|
// Load the user's config file
|
||||||
let source = config::File::from(config_dir.join(CONFIG_FILE_NAME))
|
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"))
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::action::Action;
|
||||||
#[test]
|
use crate::config::keybindings::parse_key;
|
||||||
fn test_parse_style_default() {
|
use crate::television::Mode;
|
||||||
let style = parse_style("");
|
use pretty_assertions::assert_eq;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config() -> Result<()> {
|
fn test_config() -> Result<()> {
|
||||||
@ -587,89 +169,4 @@ mod tests {
|
|||||||
);
|
);
|
||||||
Ok(())
|
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