refactor: simplify configuration and build code + leaner crate

This commit is contained in:
alexpasmantier 2025-01-25 01:23:42 +01:00 committed by Alexandre Pasmantier
parent c7109044f0
commit 5ba4ededc9
14 changed files with 113 additions and 2001 deletions

1520
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,24 +5,22 @@ edition = "2021"
description = "The revolution will be televised."
license = "MIT"
authors = ["Alexandre Pasmantier <alex.pasmant@gmail.com>"]
build = "build.rs"
repository = "https://github.com/alexpasmantier/television"
homepage = "https://github.com/alexpasmantier/television"
keywords = ["search", "fuzzy", "preview", "tui", "terminal"]
categories = [
"command-line-utilities",
"command-line-interface",
"concurrency",
"development-tools",
"command-line-utilities",
"command-line-interface",
"concurrency",
"development-tools",
]
include = [
"LICENSE",
"README.md",
"themes/**/*.toml",
"television/**",
"build.rs",
".config/config.toml",
"cable",
"LICENSE",
"README.md",
"themes/**/*.toml",
"television/**",
".config/config.toml",
"cable",
]
rust-version = "1.81"
@ -43,15 +41,14 @@ rustc-hash = "2.1"
syntect = { version = "5.2", default-features = false }
unicode-width = "0.2"
clap = { version = "4.5", default-features = false, features = [
"std",
"derive",
"cargo",
"string",
"std",
"derive",
"cargo",
"string",
] }
serde = { version = "1.0", features = ["derive"] }
ratatui = { version = "0.29", features = ["serde", "macros"] }
better-panic = "0.3"
config = "0.14"
signal-hook = "0.3"
human-panic = "2.0"
copypasta = "0.10"
@ -88,9 +85,6 @@ name = "tv"
name = "results_list_benchmark"
harness = false
[build-dependencies]
vergen-gix = { version = "1.0", features = ["build", "cargo", "rustc"] }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.28.1", features = ["serde", "use-dev-tty"] }

View File

@ -1,14 +0,0 @@
use std::error::Error;
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, RustcBuilder};
fn main() -> Result<(), Box<dyn Error>> {
let build = BuildBuilder::default().build_date(true).build()?;
let cargo = CargoBuilder::default().target_triple(true).build()?;
let rustc = RustcBuilder::default().semver(true).build()?;
Ok(Emitter::default()
.add_instructions(&build)?
.add_instructions(&cargo)?
.add_instructions(&rustc)?
.emit()?)
}

View File

@ -292,16 +292,7 @@ fn delimiter_parser(s: &str) -> Result<String, String> {
})
}
const VERSION_MESSAGE: &str = concat!(
env!("CARGO_PKG_VERSION"),
"\ntarget triple: ",
env!("VERGEN_CARGO_TARGET_TRIPLE"),
"\nbuild: ",
env!("VERGEN_RUSTC_SEMVER"),
" (",
env!("VERGEN_BUILD_DATE"),
")"
);
const VERSION_MESSAGE: &str = env!("CARGO_PKG_VERSION");
pub fn version() -> String {
let author = clap::crate_authors!();

View File

@ -29,10 +29,10 @@ impl Display for Binding {
}
#[derive(Clone, Debug, Default)]
pub struct KeyBindings(pub config::Map<Mode, config::Map<Action, Binding>>);
pub struct KeyBindings(pub FxHashMap<Mode, FxHashMap<Action, Binding>>);
impl Deref for KeyBindings {
type Target = config::Map<Mode, config::Map<Action, Binding>>;
type Target = FxHashMap<Mode, FxHashMap<Action, Binding>>;
fn deref(&self) -> &Self::Target {
&self.0
}
@ -44,6 +44,31 @@ impl DerefMut for KeyBindings {
}
}
pub fn merge_keybindings(
mut keybindings: KeyBindings,
new_keybindings: &KeyBindings,
) -> KeyBindings {
for (mode, bindings) in new_keybindings.iter() {
for (action, binding) in bindings {
match keybindings.get_mut(mode) {
Some(mode_bindings) => {
mode_bindings.insert(action.clone(), binding.clone());
}
None => {
keybindings.insert(
*mode,
[(action.clone(), binding.clone())]
.iter()
.cloned()
.collect(),
);
}
}
}
}
keybindings
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
pub enum SerializedBinding {
@ -61,37 +86,38 @@ impl<'de> Deserialize<'de> for KeyBindings {
FxHashMap<Action, SerializedBinding>,
>::deserialize(deserializer)?;
let keybindings = parsed_map
.into_iter()
.map(|(mode, inner_map)| {
let converted_inner_map = inner_map
.into_iter()
.map(|(cmd, binding)| {
(
cmd,
match binding {
SerializedBinding::SingleKey(key_str) => {
Binding::SingleKey(
parse_key(&key_str).unwrap(),
)
}
SerializedBinding::MultipleKeys(keys_str) => {
Binding::MultipleKeys(
let keybindings: FxHashMap<Mode, FxHashMap<Action, Binding>> =
parsed_map
.into_iter()
.map(|(mode, inner_map)| {
let converted_inner_map = inner_map
.into_iter()
.map(|(cmd, binding)| {
(
cmd,
match binding {
SerializedBinding::SingleKey(key_str) => {
Binding::SingleKey(
parse_key(&key_str).unwrap(),
)
}
SerializedBinding::MultipleKeys(
keys_str,
) => Binding::MultipleKeys(
keys_str
.iter()
.map(|key_str| {
parse_key(key_str).unwrap()
})
.collect(),
)
}
},
)
})
.collect();
(mode, converted_inner_map)
})
.collect();
),
},
)
})
.collect();
(mode, converted_inner_map)
})
.collect();
Ok(KeyBindings(keybindings))
}

View File

@ -1,14 +1,14 @@
#![allow(clippy::module_name_repetitions)]
use std::{env, path::PathBuf};
use color_eyre::Result;
use color_eyre::{eyre::Context, Result};
use directories::ProjectDirs;
use keybindings::merge_keybindings;
pub use keybindings::{parse_key, Binding, KeyBindings};
use lazy_static::lazy_static;
use previewers::PreviewersConfig;
use serde::Deserialize;
use shell_integration::ShellIntegrationConfig;
use styles::Styles;
pub use themes::Theme;
use tracing::{debug, warn};
use ui::UiConfig;
@ -16,18 +16,18 @@ use ui::UiConfig;
mod keybindings;
mod previewers;
mod shell_integration;
mod styles;
mod themes;
mod ui;
const CONFIG: &str = include_str!("../.config/config.toml");
const DEFAULT_CONFIG: &str = include_str!("../../.config/config.toml");
#[allow(dead_code, clippy::module_name_repetitions)]
#[derive(Clone, Debug, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct AppConfig {
#[serde(default)]
#[serde(default = "get_data_dir")]
pub data_dir: PathBuf,
#[serde(default)]
#[serde(default = "get_config_dir")]
pub config_dir: PathBuf,
#[serde(default = "default_frame_rate")]
pub frame_rate: f64,
@ -36,19 +36,23 @@ pub struct AppConfig {
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
/// General application configuration
#[allow(clippy::struct_field_names)]
#[serde(default, flatten)]
pub config: AppConfig,
/// Keybindings configuration
#[serde(default)]
pub keybindings: KeyBindings,
#[serde(default)]
pub styles: Styles,
/// UI configuration
#[serde(default)]
pub ui: UiConfig,
/// Previewers configuration
#[serde(default)]
pub previewers: PreviewersConfig,
/// Shell integration configuration
#[serde(default)]
pub shell_integration: ShellIntegrationConfig,
}
@ -77,68 +81,40 @@ impl Config {
#[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
pub fn new() -> Result<Self> {
// Load the default_config values as base defaults
let default_config: Config =
toml::from_str(CONFIG).expect("default config should be valid");
let default_config: Config = toml::from_str(DEFAULT_CONFIG)
.wrap_err("Error parsing default config")?;
// initialize the config builder
let data_dir = get_data_dir();
let config_dir = get_config_dir();
std::fs::create_dir_all(&config_dir)
.expect("Failed creating configuration directory");
std::fs::create_dir_all(&data_dir)
.expect("Failed creating data directory");
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("frame_rate", default_config.config.frame_rate)?
.set_default("tick_rate", default_config.config.tick_rate)?
.set_default("ui", default_config.ui.clone())?
.set_default("previewers", default_config.previewers.clone())?
.set_default("theme", default_config.ui.theme.clone())?
.set_default(
"shell_integration",
default_config.shell_integration.clone(),
)?;
// Load the user's config file
let source = config::File::from(config_dir.join(CONFIG_FILE_NAME))
.format(config::FileFormat::Toml)
.required(false);
builder = builder.add_source(source);
if config_dir.join(CONFIG_FILE_NAME).is_file() {
debug!("Found config file at {:?}", config_dir);
let mut cfg: Self = builder.build()?.try_deserialize().unwrap();
//.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.clone());
}
}
let path = config_dir.join(CONFIG_FILE_NAME);
let contents = std::fs::read_to_string(&path)?;
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);
}
}
let cfg: Config = toml::from_str(&contents)
.wrap_err(format!("error parsing config: {path:?}"))?;
// merge keybindings with default keybindings
let keybindings = merge_keybindings(
default_config.keybindings,
&cfg.keybindings,
);
let cfg = Config { keybindings, ..cfg };
debug!("Config: {:?}", cfg);
Ok(cfg)
} else {
warn!("No config file found at {:?}, creating default configuration file at that location.", config_dir);
// create the default configuration file in the user's config directory
std::fs::write(config_dir.join(CONFIG_FILE_NAME), CONFIG)?;
std::fs::write(config_dir.join(CONFIG_FILE_NAME), DEFAULT_CONFIG)?;
Ok(default_config)
}
}

View File

@ -1,7 +1,4 @@
use std::collections::HashMap;
use crate::preview::{previewers, PreviewerConfig};
use config::ValueKind;
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize, Default)]
@ -20,26 +17,11 @@ impl From<PreviewersConfig> for PreviewerConfig {
}
}
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("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)]
#[serde(default)]
pub struct FilePreviewerConfig {
//pub max_file_size: u64,
pub theme: String,
@ -54,19 +36,5 @@ impl Default for FilePreviewerConfig {
}
}
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())
}
}

View File

@ -1,5 +1,3 @@
use std::collections::HashMap;
use rustc_hash::FxHashMap;
use serde::Deserialize;
@ -7,20 +5,3 @@ use serde::Deserialize;
pub struct ShellIntegrationConfig {
pub commands: FxHashMap<String, String>,
}
impl From<ShellIntegrationConfig> for config::ValueKind {
fn from(val: ShellIntegrationConfig) -> Self {
let mut m = HashMap::new();
m.insert(
String::from("commands"),
config::ValueKind::Table(
val.commands
.into_iter()
.map(|(k, v)| (k, config::ValueKind::String(v).into()))
.collect(),
)
.into(),
);
config::ValueKind::Table(m)
}
}

View File

@ -1,207 +0,0 @@
use crate::screen::mode::Mode;
use ratatui::prelude::{Color, Modifier, Style};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Deserializer};
use std::ops::{Deref, DerefMut};
#[derive(Clone, Debug, Default)]
pub struct Styles(pub FxHashMap<Mode, FxHashMap<String, Style>>);
impl Deref for Styles {
type Target = FxHashMap<Mode, FxHashMap<String, Style>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Styles {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'de> Deserialize<'de> for Styles {
fn deserialize<D>(deserializer: D) -> color_eyre::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let parsed_map =
FxHashMap::<Mode, FxHashMap<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("");
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);
}
}

View File

@ -1,6 +1,3 @@
use std::collections::HashMap;
use config::ValueKind;
use serde::Deserialize;
use crate::screen::layout::{InputPosition, PreviewTitlePosition};
@ -10,6 +7,7 @@ use super::themes::DEFAULT_THEME;
const DEFAULT_UI_SCALE: u16 = 100;
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
pub struct UiConfig {
pub use_nerd_font_icons: bool,
pub ui_scale: u16,
@ -34,39 +32,3 @@ impl Default for UiConfig {
}
}
}
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(),
);
m.insert(
String::from("show_help_bar"),
ValueKind::Boolean(val.show_help_bar).into(),
);
m.insert(
String::from("show_preview_panel"),
ValueKind::Boolean(val.show_preview_panel).into(),
);
m.insert(
String::from("input_position"),
ValueKind::String(val.input_bar_position.to_string()).into(),
);
m.insert(
String::from("preview_title_position"),
match val.preview_title_position {
Some(pos) => ValueKind::String(pos.to_string()),
None => ValueKind::Nil,
}
.into(),
);
m.insert(String::from("theme"), ValueKind::String(val.theme).into());
ValueKind::Table(m)
}
}

View File

@ -1,14 +1,14 @@
use color_eyre::Result;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use crate::config;
use crate::config::get_data_dir;
lazy_static::lazy_static! {
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
}
pub fn init() -> Result<()> {
let directory = config::get_data_dir();
let directory = get_data_dir();
std::fs::create_dir_all(directory.clone())?;
let log_path = directory.join(LOG_FILE.clone());
let log_file = std::fs::File::create(log_path)?;

View File

@ -40,40 +40,6 @@ pub fn build_metadata_table<'a>(
)),
]);
let target_triple_row = Row::new(vec![
Cell::from(Span::styled(
"target triple: ",
Style::default().fg(colorscheme.help.metadata_field_name_fg),
)),
Cell::from(Span::styled(
&app_metadata.build.target_triple,
Style::default().fg(colorscheme.help.metadata_field_value_fg),
)),
]);
let build_row = Row::new(vec![
Cell::from(Span::styled(
"build: ",
Style::default().fg(colorscheme.help.metadata_field_name_fg),
)),
Cell::from(Span::styled(
&app_metadata.build.rustc_version,
Style::default().fg(colorscheme.help.metadata_field_value_fg),
)),
Cell::from(Span::styled(
" (",
Style::default().fg(colorscheme.help.metadata_field_name_fg),
)),
Cell::from(Span::styled(
&app_metadata.build.build_date,
Style::default().fg(colorscheme.help.metadata_field_value_fg),
)),
Cell::from(Span::styled(
")",
Style::default().fg(colorscheme.help.metadata_field_name_fg),
)),
]);
let current_dir_row = Row::new(vec![
Cell::from(Span::styled(
"current directory: ",
@ -115,8 +81,6 @@ pub fn build_metadata_table<'a>(
Table::new(
vec![
version_row,
target_triple_row,
build_row,
current_dir_row,
current_channel_row,
current_mode_row,

View File

@ -21,7 +21,7 @@ use crate::screen::preview::draw_preview_content_block;
use crate::screen::remote_control::draw_remote_control;
use crate::screen::results::draw_results_list;
use crate::screen::spinner::{Spinner, SpinnerState};
use crate::utils::metadata::{AppMetadata, BuildMetadata};
use crate::utils::metadata::AppMetadata;
use crate::utils::strings::EMPTY_STRING;
use crate::{cable::load_cable_channels, keymap::Keymap};
use color_eyre::Result;
@ -75,11 +75,6 @@ impl Television {
let app_metadata = AppMetadata::new(
env!("CARGO_PKG_VERSION").to_string(),
BuildMetadata::new(
env!("VERGEN_RUSTC_SEMVER").to_string(),
env!("VERGEN_BUILD_DATE").to_string(),
env!("VERGEN_CARGO_TARGET_TRIPLE").to_string(),
),
std::env::current_dir()
.expect("Could not get current directory")
.to_string_lossy()

View File

@ -1,38 +1,12 @@
pub struct BuildMetadata {
pub rustc_version: String,
pub build_date: String,
pub target_triple: String,
}
impl BuildMetadata {
pub fn new(
rustc_version: String,
build_date: String,
target_triple: String,
) -> Self {
Self {
rustc_version,
build_date,
target_triple,
}
}
}
pub struct AppMetadata {
pub version: String,
pub build: BuildMetadata,
pub current_directory: String,
}
impl AppMetadata {
pub fn new(
version: String,
build: BuildMetadata,
current_directory: String,
) -> Self {
pub fn new(version: String, current_directory: String) -> Self {
Self {
version,
build,
current_directory,
}
}