initial implementation of customizable styles for tui

This commit is contained in:
Andrew Cherry 2024-02-27 08:51:39 +00:00 committed by Ellie Huxtable
parent 22a9b497ad
commit dd587201ca
7 changed files with 175 additions and 20 deletions

44
Cargo.lock generated
View File

@ -249,6 +249,7 @@ dependencies = [
"parse_duration",
"pretty_assertions",
"rand",
"ratatui",
"regex",
"reqwest",
"rmp",
@ -563,6 +564,15 @@ version = "0.3.0"
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"
@ -720,6 +730,20 @@ dependencies = [
"windows-sys 0.48.0",
]
[[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"
@ -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",
@ -3465,6 +3491,12 @@ dependencies = [
"syn 1.0.109",
]
[[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"
@ -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",

View File

@ -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"

View File

@ -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 }

View File

@ -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,
@ -382,6 +484,9 @@ pub struct Settings {
#[serde(default)]
pub stats: Stats,
#[serde(default)]
pub styles: Styles,
#[serde(default)]
pub sync: Sync,

View File

@ -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 }

View File

@ -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);

View File

@ -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 {