diff options
author | Andrew Cherry <andrew@xyncro.com> | 2024-02-27 08:51:39 +0000 |
---|---|---|
committer | Ellie Huxtable <ellie@elliehuxtable.com> | 2024-02-28 13:11:05 +0000 |
commit | dd587201ca3de0bbf90707d4a39123a825f9fcdf (patch) | |
tree | 0d72b123306ad2d41178d0fc8ea8743e1051f7ec | |
parent | 22a9b497adea8869eba67dd153000d211d758382 (diff) |
initial implementation of customizable styles for tui
-rw-r--r-- | Cargo.lock | 44 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | atuin-client/Cargo.toml | 1 | ||||
-rw-r--r-- | atuin-client/src/settings.rs | 107 | ||||
-rw-r--r-- | atuin/Cargo.toml | 2 | ||||
-rw-r--r-- | atuin/src/command/client/search/history_list.rs | 25 | ||||
-rw-r--r-- | atuin/src/command/client/search/interactive.rs | 15 |
7 files changed, 175 insertions, 20 deletions
@@ -249,6 +249,7 @@ dependencies = [ "parse_duration", "pretty_assertions", "rand", + "ratatui", "regex", "reqwest", "rmp", @@ -564,6 +565,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + +[[package]] name = "cc" version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -721,6 +731,20 @@ dependencies = [ ] [[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "serde", + "static_assertions", +] + +[[package]] name = "config" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2581,17 +2605,19 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb" +checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8" dependencies = [ "bitflags 2.4.2", "cassowary", + "compact_str", "crossterm", "indoc", "itertools", "lru", "paste", + "serde", "stability", "strum", "unicode-segmentation", @@ -3466,6 +3492,12 @@ dependencies = [ ] [[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] name = "str-buf" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3496,18 +3528,18 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "strum" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" dependencies = [ "heck", "proc-macro2", @@ -47,6 +47,7 @@ typed-builder = "0.18.0" pretty_assertions = "1.3.0" thiserror = "1.0" rustix = {version = "0.38.30", features=["process", "fs"]} +ratatui = { version = "0.26", features = ["serde"] } [workspace.dependencies.reqwest] version = "0.11" diff --git a/atuin-client/Cargo.toml b/atuin-client/Cargo.toml index 51227044..a4a1de37 100644 --- a/atuin-client/Cargo.toml +++ b/atuin-client/Cargo.toml @@ -54,6 +54,7 @@ futures = "0.3" crypto_secretbox = "0.1.1" generic-array = { version = "0.14", features = ["serde"] } serde_with = "3.5.1" +ratatui = { workspace = true } # encryption rusty_paseto = { version = "0.6.0", default-features = false } diff --git a/atuin-client/src/settings.rs b/atuin-client/src/settings.rs index 9f2afd04..179666f2 100644 --- a/atuin-client/src/settings.rs +++ b/atuin-client/src/settings.rs @@ -15,9 +15,10 @@ use config::{ use eyre::{bail, eyre, Context, Error, Result}; use fs_err::{create_dir_all, File}; use parse_duration::parse; +use ratatui::style::{Color, Stylize}; use regex::RegexSet; use semver::Version; -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; use serde_with::DeserializeFromStr; use time::{ format_description::{well_known::Rfc3339, FormatItem}, @@ -321,6 +322,107 @@ impl Default for Stats { } } +#[derive(Clone, Debug, Default, Deserialize)] +pub struct Styles { + #[serde(default, deserialize_with = "Variants::deserialize_style")] + pub command: Option<ratatui::style::Style>, + #[serde(default, deserialize_with = "Variants::deserialize_style")] + pub command_selected: Option<ratatui::style::Style>, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum Variants { + Color(Color), + Components(Components), +} + +impl Variants { + fn deserialize_style<'de, D>(deserializer: D) -> Result<Option<ratatui::style::Style>, D::Error> + where + D: Deserializer<'de>, + { + let variants: Option<Variants> = Deserialize::deserialize(deserializer)?; + let style: Option<ratatui::style::Style> = variants.map(|variants| variants.into()); + + Ok(style) + } +} + +impl From<Variants> for ratatui::style::Style { + fn from(value: Variants) -> ratatui::style::Style { + match value { + Variants::Components(complex_style) => complex_style.into(), + Variants::Color(color) => color.into(), + } + } +} + +#[derive(Debug, Default, Deserialize)] +pub struct Components { + // Colors + #[serde(default)] + pub foreground: Option<Color>, + #[serde(default)] + pub background: Option<Color>, + #[serde(default)] + pub underline: Option<Color>, + + // Modifiers + #[serde(default)] + pub bold: Option<bool>, + #[serde(default)] + pub crossed_out: Option<bool>, + #[serde(default)] + pub italic: Option<bool>, + #[serde(default)] + pub underlined: Option<bool>, +} + +impl From<Components> for ratatui::style::Style { + fn from(value: Components) -> ratatui::style::Style { + let mut style = ratatui::style::Style::default(); + + if let Some(color) = value.foreground { + style = style.fg(color); + }; + + if let Some(color) = value.background { + style = style.bg(color); + } + + if let Some(color) = value.underline { + style = style.underline_color(color); + } + + style = match value.bold { + Some(true) => style.bold(), + Some(_) => style.not_bold(), + _ => style, + }; + + style = match value.crossed_out { + Some(true) => style.crossed_out(), + Some(_) => style.not_crossed_out(), + _ => style, + }; + + style = match value.italic { + Some(true) => style.italic(), + Some(_) => style.not_italic(), + _ => style, + }; + + style = match value.underlined { + Some(true) => style.underlined(), + Some(_) => style.not_underlined(), + _ => style, + }; + + style + } +} + #[derive(Clone, Debug, Deserialize, Default)] pub struct Sync { pub records: bool, @@ -383,6 +485,9 @@ pub struct Settings { pub stats: Stats, #[serde(default)] + pub styles: Styles, + + #[serde(default)] pub sync: Sync, #[serde(default)] diff --git a/atuin/Cargo.toml b/atuin/Cargo.toml index 4d04c67e..4143b72f 100644 --- a/atuin/Cargo.toml +++ b/atuin/Cargo.toml @@ -74,7 +74,7 @@ tiny-bip39 = "1" futures-util = "0.3" fuzzy-matcher = "0.3.7" colored = "2.0.4" -ratatui = "0.25" +ratatui = { workspace = true } tracing = "0.1" cli-clipboard = { version = "0.4.0", optional = true } uuid = { workspace = true } diff --git a/atuin/src/command/client/search/history_list.rs b/atuin/src/command/client/search/history_list.rs index e27d0ce2..880b2c7a 100644 --- a/atuin/src/command/client/search/history_list.rs +++ b/atuin/src/command/client/search/history_list.rs @@ -1,11 +1,11 @@ use std::time::Duration; -use atuin_client::history::History; +use atuin_client::{history::History, settings::Styles}; use atuin_common::utils::Escapable as _; use ratatui::{ buffer::Buffer, layout::Rect, - style::{Color, Modifier, Style}, + style::{Color, Modifier, Style, Stylize}, widgets::{Block, StatefulWidget, Widget}, }; use time::OffsetDateTime; @@ -19,6 +19,7 @@ pub struct HistoryList<'a> { /// Apply an alternative highlighting to the selected row alternate_highlight: bool, now: &'a dyn Fn() -> OffsetDateTime, + styles: &'a Styles, } #[derive(Default)] @@ -70,6 +71,7 @@ impl<'a> StatefulWidget for HistoryList<'a> { inverted: self.inverted, alternate_highlight: self.alternate_highlight, now: &self.now, + styles: self.styles, }; for item in self.history.iter().skip(state.offset).take(end - start) { @@ -91,6 +93,7 @@ impl<'a> HistoryList<'a> { inverted: bool, alternate_highlight: bool, now: &'a dyn Fn() -> OffsetDateTime, + styles: &'a Styles, ) -> Self { Self { history, @@ -98,6 +101,7 @@ impl<'a> HistoryList<'a> { inverted, alternate_highlight, now, + styles, } } @@ -130,6 +134,7 @@ struct DrawState<'a> { inverted: bool, alternate_highlight: bool, now: &'a dyn Fn() -> OffsetDateTime, + styles: &'a Styles, } // longest line prefix I could come up with @@ -183,12 +188,16 @@ impl DrawState<'_> { } fn command(&mut self, h: &History) { - let mut style = Style::default(); - if !self.alternate_highlight && (self.y as usize + self.state.offset == self.state.selected) - { - // if not applying alternative highlighting to the whole row, color the command - style = style.fg(Color::Red).add_modifier(Modifier::BOLD); - } + let alternate_highlight = self.alternate_highlight; + let selected = self.y as usize + self.state.offset == self.state.selected; + + let style = if !alternate_highlight && selected { + self.styles + .command_selected + .unwrap_or_else(|| Style::default().fg(Color::Red).bold()) + } else { + self.styles.command.unwrap_or_default() + }; for section in h.command.escape_control().split_ascii_whitespace() { self.draw(" ", style); diff --git a/atuin/src/command/client/search/interactive.rs b/atuin/src/command/client/search/interactive.rs index 514b42b5..7b057d26 100644 --- a/atuin/src/command/client/search/interactive.rs +++ b/atuin/src/command/client/search/interactive.rs @@ -22,7 +22,7 @@ use unicode_width::UnicodeWidthStr; use atuin_client::{ database::{current_context, Database}, history::{store::HistoryStore, History, HistoryStats}, - settings::{CursorStyle, ExitMode, FilterMode, KeymapMode, SearchMode, Settings}, + settings::{CursorStyle, ExitMode, FilterMode, KeymapMode, SearchMode, Settings, Styles}, }; use super::{ @@ -557,7 +557,7 @@ impl State { // TODO: this should be split so that we have one interactive search container that is // EITHER a search box or an inspector. But I'm not doing that now, way too much atm. // also allocate less 🙈 - let titles = TAB_TITLES.iter().copied().map(Line::from).collect(); + let titles: Vec<_> = TAB_TITLES.iter().copied().map(Line::from).collect(); let tabs = Tabs::new(titles) .block(Block::default().borders(Borders::NONE)) @@ -596,8 +596,13 @@ impl State { match self.tab_index { 0 => { - let results_list = - Self::build_results_list(style, results, self.keymap_mode, &self.now); + let results_list = Self::build_results_list( + style, + results, + self.keymap_mode, + &self.now, + &settings.styles, + ); f.render_stateful_widget(results_list, results_list_chunk, &mut self.results_state); } @@ -718,12 +723,14 @@ impl State { results: &'a [History], keymap_mode: KeymapMode, now: &'a dyn Fn() -> OffsetDateTime, + styles: &'a Styles, ) -> HistoryList<'a> { let results_list = HistoryList::new( results, style.invert, keymap_mode == KeymapMode::VimNormal, now, + styles, ); if style.compact { |