summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Cherry <andrew@xyncro.com>2024-02-27 08:51:39 +0000
committerEllie Huxtable <ellie@elliehuxtable.com>2024-02-28 13:11:05 +0000
commitdd587201ca3de0bbf90707d4a39123a825f9fcdf (patch)
tree0d72b123306ad2d41178d0fc8ea8743e1051f7ec
parent22a9b497adea8869eba67dd153000d211d758382 (diff)
initial implementation of customizable styles for tui
-rw-r--r--Cargo.lock44
-rw-r--r--Cargo.toml1
-rw-r--r--atuin-client/Cargo.toml1
-rw-r--r--atuin-client/src/settings.rs107
-rw-r--r--atuin/Cargo.toml2
-rw-r--r--atuin/src/command/client/search/history_list.rs25
-rw-r--r--atuin/src/command/client/search/interactive.rs15
7 files changed, 175 insertions, 20 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 89f252cf..ba679364 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index f1057382..08c38222 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 {