From 19e36b14a2ac68109085503e26511a882f63a2be Mon Sep 17 00:00:00 2001 From: Jerzy Wilczek Date: Thu, 5 Oct 2023 01:29:26 +0200 Subject: [PATCH 1/2] Add the configuration infrastructure. The only thing left now is to load and actually use the config and theme. --- Cargo.lock | 271 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 + src/config.rs | 105 +++++++++++++++++ src/config/color.rs | 117 +++++++++++++++++++ src/config/theme.rs | 255 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/main.rs | 15 +++ 7 files changed, 770 insertions(+), 1 deletion(-) create mode 100644 src/config.rs create mode 100644 src/config/color.rs create mode 100644 src/config/theme.rs diff --git a/Cargo.lock b/Cargo.lock index dae627c..edb5bc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,60 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + [[package]] name = "autocfg" version = "1.1.0" @@ -47,6 +101,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -84,12 +184,39 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -99,12 +226,39 @@ dependencies = [ "thread_local", ] +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "indoc" version = "2.0.3" @@ -124,12 +278,17 @@ dependencies = [ name = "jwtop" version = "0.1.2" dependencies = [ + "anyhow", + "clap", "crossterm", + "directories", "fuzzy-matcher", "ratatui", "regex", + "serde", "sysinfo", "systemstat", + "toml", ] [[package]] @@ -209,6 +368,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.1" @@ -227,7 +392,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", "windows-targets", ] @@ -273,6 +438,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -282,6 +456,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + [[package]] name = "regex" version = "1.9.4" @@ -343,6 +528,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + [[package]] name = "signal-hook" version = "0.3.17" @@ -379,6 +573,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strum" version = "0.25.0" @@ -440,6 +640,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "thiserror" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.7" @@ -467,6 +687,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.11" @@ -485,6 +739,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -578,3 +838,12 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index 0d9b3de..d04c7b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,14 @@ keywords = ["monitoring", "cli", "top", "tui"] categories = ["command-line-utilities"] [dependencies] +anyhow = "1.0.75" +clap = { version = "4.4.6", features = ["derive"] } crossterm = "0.27.0" +directories = "5.0.1" fuzzy-matcher = "0.3.7" regex = "1.9.3" +serde = { version = "1.0.188", features = ["derive"] } sysinfo = { version = "0.29.7", default-features = false } systemstat = "0.2.3" +toml = "0.8.2" tui = { package = "ratatui", version = "0.23.0" } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..73f98ff --- /dev/null +++ b/src/config.rs @@ -0,0 +1,105 @@ +mod color; +mod theme; + +use std::{ffi::OsString, path::PathBuf}; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; + +pub use color::RgbColor; +pub use theme::*; + +#[derive(clap::Parser)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + /// Dump the default config + #[arg(short, long)] + pub dump_config: bool, + + /// Dump a sample theme + #[arg(long, short('t'))] + pub dump_sample_theme: bool, + + /// The path to the config directory + #[arg(long)] + pub config_path: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct Config { + pub theme: Theme, +} + +impl Config { + pub fn load(cli: &Cli) -> Result { + let Some(config_dir_path) = config_path(cli) else { + return Ok(Default::default()); + }; + + let config_file_path = config_dir_path.join("config.toml"); + + if !config_file_path.exists() { + return Ok(Default::default()); + } + + let config = std::fs::read_to_string(&config_file_path).with_context(|| { + format!( + "Failed to read config from {}", + config_file_path.to_string_lossy() + ) + })?; + let config = toml::from_str::(&config).with_context(|| { + format!( + "Failed to parse config from {}", + config_file_path.to_string_lossy() + ) + })?; + + let Some(theme) = config.theme else { + return Ok(Self { + theme: Default::default(), + }); + }; + + let theme = Theme::load_from_file(&config_dir_path.join("themes").join(theme))?; + + Ok(Self { theme }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +struct RawConfig { + theme: Option, +} + +pub fn sample_config() -> String { + toml::to_string_pretty(&RawConfig { + theme: Some("theme name (has to correspond with a valid theme file located in /themes/.toml)".into()) + }).unwrap() +} + +pub fn config_path(cli: &Cli) -> Option { + if let Some(ref path) = cli.config_path { + if path.exists() { + return Some(path.clone()); + } + } + + if let Ok(path) = std::env::var("JWTOP_CONFIG_DIR") { + let path: PathBuf = path.into(); + if path.exists() { + return path.into(); + } + } + + let path = directories::ProjectDirs::from("org", "jw", "jwtop") + .unwrap() + .config_dir() + .to_path_buf(); + + if path.exists() { + return Some(path); + } + + None +} diff --git a/src/config/color.rs b/src/config/color.rs new file mode 100644 index 0000000..ff6e1f5 --- /dev/null +++ b/src/config/color.rs @@ -0,0 +1,117 @@ +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct RgbColor(pub u8, pub u8, pub u8); + +impl serde::Serialize for RgbColor { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&format!("#{:02x}{:02x}{:02x}", self.0, self.1, self.2)) + } +} + +struct RgbColorVisitor; + +impl<'de> serde::de::Visitor<'de> for RgbColorVisitor { + type Value = RgbColor; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string containing a hex color value looking like this: #rrggbb or like this: #RRGGBB") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if !v.is_ascii() { + return Err(E::custom( + "a hex color has to only contain ascii characters", + )); + } + + let v = v.as_bytes(); + + if v.len() != 7 { + return Err(E::custom("a hex color has to be 7 characters long")); + } + + if v[0] != b'#' { + return Err(E::custom("a hex color has to start with '#'")); + } + + let v = &v[1..]; + + if !v.iter().all(|c| c.is_ascii_hexdigit()) { + return Err(E::custom( + "the value part of a hex color has to only contain hexadecimal digits", + )); + } + + fn color(v: [u8; 2]) -> Result { + u8::from_str_radix( + std::str::from_utf8(&[v[0].to_ascii_lowercase(), v[1].to_ascii_lowercase()]) + .map_err(|e| E::custom(format!("unexpected error occurred: \"{}\"", e)))?, + 16, + ) + .map_err(|e| E::custom(format!("unexpected error occurred: \"{}\"", e))) + } + + let r = color([v[0], v[1]])?; + let g = color([v[2], v[3]])?; + let b = color([v[4], v[5]])?; + + Ok(RgbColor(r, g, b)) + } +} + +impl<'de> serde::Deserialize<'de> for RgbColor { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(RgbColorVisitor) + } +} + +impl From for tui::style::Color { + fn from(value: RgbColor) -> Self { + tui::style::Color::Rgb(value.0, value.1, value.2) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] + struct Wrapper { + color: RgbColor, + } + + #[test] + fn color_de() { + assert_eq!( + Wrapper { + color: RgbColor(0xaa, 0xbb, 0xcc) + }, + toml::from_str("color = \"#AABBCC\"").unwrap() + ); + assert_eq!( + Wrapper { + color: RgbColor(0xdd, 0xee, 0xff) + }, + toml::from_str("color = \"#ddeeff\"").unwrap() + ); + } + + #[test] + fn both_ways() { + let val = Wrapper { + color: RgbColor(12, 240, 144), + }; + assert_eq!( + val, + toml::from_str(&toml::to_string(&val).unwrap()).unwrap() + ) + } +} diff --git a/src/config/theme.rs b/src/config/theme.rs new file mode 100644 index 0000000..5d6f9e0 --- /dev/null +++ b/src/config/theme.rs @@ -0,0 +1,255 @@ +use std::path::Path; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use tui::style::Color; + +use super::RgbColor; + +// TODO: actually, this is kind of stupid. Most of this can be handled by using serde attributes. + +mod default_colors { + use crate::config::RgbColor; + + pub const RED: RgbColor = RgbColor(0xcc, 0x66, 0x66); + pub const GREEN: RgbColor = RgbColor(0xb5, 0xbd, 0x68); + + pub const YELLOW: RgbColor = RgbColor(0xf0, 0xc6, 0x74); + + pub const BLUE: RgbColor = RgbColor(0x81, 0xa2, 0xbe); + + pub const MAGENTA: RgbColor = RgbColor(0xb2, 0x94, 0xbb); + + pub const CYAN: RgbColor = RgbColor(0x8a, 0xbe, 0xb7); + + pub const BG: RgbColor = RgbColor(0x1d, 0x1f, 0x21); +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct RawTheme { + widget: Option, + plot: Option, + bars: Option, + table: Option, +} + +impl RawTheme { + fn sample_theme() -> Self { + Self { + widget: Some(RawWidgetTheme { + frame_color: Some(default_colors::CYAN), + title_color: Some(default_colors::CYAN), + background_color: Some(default_colors::BG), + }), + + plot: Some(RawPlotTheme { + axis_labels_color: Some(default_colors::CYAN), + plot_colors: vec![ + default_colors::BLUE, + default_colors::CYAN, + default_colors::GREEN, + default_colors::MAGENTA, + default_colors::RED, + default_colors::YELLOW, + ], + }), + + bars: Some(RawBarsTheme { + low_usage_color: Some(default_colors::GREEN), + medium_usage_color: Some(default_colors::YELLOW), + high_usage_color: Some(default_colors::RED), + }), + + table: Some(RawTableTheme { + header_color: Some(default_colors::BLUE), + row_color: Some(default_colors::BLUE), + }), + } + } +} + +pub fn sample_theme() -> String { + toml::to_string_pretty(&RawTheme::sample_theme()).unwrap() +} + +fn tui_color(raw: Option, default: Color) -> Color { + raw.map(|c| c.into()).unwrap_or(default) +} + +#[derive(Debug, Default, Clone)] +pub struct Theme { + pub widget: WidgetTheme, + pub plot: PlotTheme, + pub bars: BarsTheme, + pub table: TableTheme, +} + +impl From for Theme { + fn from(value: RawTheme) -> Self { + Self { + widget: value.widget.map(|t| t.into()).unwrap_or_default(), + plot: value.plot.map(|t| t.into()).unwrap_or_default(), + bars: value.bars.map(|t| t.into()).unwrap_or_default(), + table: value.table.map(|t| t.into()).unwrap_or_default(), + } + } +} + +impl Theme { + pub fn load_from_file(path: &Path) -> Result { + let file = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read theme from {}", path.to_string_lossy()))?; + + let raw: RawTheme = toml::from_str(&file).context("Failed to deserialize theme")?; + + Ok(raw.into()) + } +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] +struct RawWidgetTheme { + frame_color: Option, + title_color: Option, + background_color: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct WidgetTheme { + pub frame_color: Color, + pub title_color: Color, +} + +impl Default for WidgetTheme { + fn default() -> Self { + Self { + frame_color: Color::Cyan, + title_color: Color::Cyan, + } + } +} + +impl From for WidgetTheme { + fn from(value: RawWidgetTheme) -> Self { + let default = Self::default(); + + Self { + frame_color: tui_color(value.frame_color, default.frame_color), + title_color: tui_color(value.title_color, default.title_color), + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct RawPlotTheme { + axis_labels_color: Option, + plot_colors: Vec, +} + +#[derive(Debug, Clone)] +pub struct PlotTheme { + pub axis_labels_color: Color, + pub plot_colors: Vec, +} + +impl Default for PlotTheme { + fn default() -> Self { + Self { + axis_labels_color: Color::Cyan, + plot_colors: vec![ + Color::Blue, + Color::Cyan, + Color::Green, + Color::Magenta, + Color::Red, + Color::Yellow, + ], + } + } +} + +impl From for PlotTheme { + fn from(value: RawPlotTheme) -> Self { + let default = Self::default(); + + Self { + axis_labels_color: tui_color(value.axis_labels_color, default.axis_labels_color), + plot_colors: if !value.plot_colors.is_empty() { + value + .plot_colors + .into_iter() + .map(|c| c.into()) + .collect::>() + } else { + default.plot_colors + }, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] +struct RawBarsTheme { + low_usage_color: Option, + medium_usage_color: Option, + high_usage_color: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct BarsTheme { + pub low_usage_color: Color, + pub medium_usage_color: Color, + pub high_usage_color: Color, +} + +impl Default for BarsTheme { + fn default() -> Self { + Self { + low_usage_color: Color::Green, + medium_usage_color: Color::Yellow, + high_usage_color: Color::Red, + } + } +} + +impl From for BarsTheme { + fn from(value: RawBarsTheme) -> Self { + let default = Self::default(); + + Self { + low_usage_color: tui_color(value.low_usage_color, default.low_usage_color), + medium_usage_color: tui_color(value.medium_usage_color, default.medium_usage_color), + high_usage_color: tui_color(value.high_usage_color, default.high_usage_color), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] +struct RawTableTheme { + header_color: Option, + row_color: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct TableTheme { + pub header_color: Color, + pub row_color: Color, +} + +impl Default for TableTheme { + fn default() -> Self { + Self { + header_color: Color::Blue, + row_color: Color::Blue, + } + } +} + +impl From for TableTheme { + fn from(value: RawTableTheme) -> Self { + let default = Self::default(); + + Self { + header_color: tui_color(value.header_color, default.header_color), + row_color: tui_color(value.row_color, default.row_color), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 85ad8bf..c8297ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,3 +12,6 @@ pub mod tui; /// Event handler. pub mod handler; + +/// The app config +pub mod config; diff --git a/src/main.rs b/src/main.rs index 739e5dd..b9fd8d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use clap::Parser; use jwtop::app::{App, AppResult}; use jwtop::event::{Event, EventHandler}; use jwtop::handler::handle_key_events; @@ -9,6 +10,20 @@ use tui::Terminal; const TICK_RATE: u64 = 1000; fn main() -> AppResult<()> { + let cli = jwtop::config::Cli::parse(); + + if cli.dump_config { + println!("{}", jwtop::config::sample_config()); + return Ok(()); + } + + if cli.dump_sample_theme { + println!("{}", jwtop::config::sample_theme()); + return Ok(()); + } + + // TODO: load the config and actually use it in the app. + // Create an application. let mut app = App::new(); From 908580b7a3665b56659440ab30aa34c850db4af7 Mon Sep 17 00:00:00 2001 From: Jerzy Wilczek Date: Fri, 6 Oct 2023 01:13:40 +0200 Subject: [PATCH 2/2] Implement all of theming, in a better way. --- src/app.rs | 7 +- src/config.rs | 39 ++++-- src/config/color.rs | 42 +++--- src/config/theme.rs | 287 +++++++++++++++------------------------- src/main.rs | 10 +- src/ui.rs | 25 ++-- src/ui/chart_wrapper.rs | 52 +++++--- src/ui/cpus_bars.rs | 12 +- src/ui/disks.rs | 1 + src/ui/processes.rs | 11 +- 10 files changed, 234 insertions(+), 252 deletions(-) diff --git a/src/app.rs b/src/app.rs index 91dc8c9..a43b9de 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,7 +4,7 @@ use regex::Regex; use sysinfo::{CpuExt, Pid, Process, ProcessExt, System, SystemExt}; use systemstat::{BlockDeviceStats, Platform}; -use crate::ui::processes::Column; +use crate::{config::Config, ui::processes::Column}; /// Application result type. pub type AppResult = std::result::Result>; @@ -168,6 +168,7 @@ pub struct App { /// Is the application running? pub running: bool, pub input_state: InputState, + pub config: Config, pub cpu_history: Vec>, pub mem_history: VecDeque, @@ -185,9 +186,8 @@ pub struct App { } impl App { - #[allow(clippy::new_without_default)] /// Constructs a new instance of [`App`]. - pub fn new() -> Self { + pub fn new(config: Config) -> Self { let mut system = sysinfo::System::new(); let systemstat = systemstat::System::new(); system.refresh_cpu(); @@ -224,6 +224,7 @@ impl App { Self { running: true, input_state: Default::default(), + config, cpu_history, mem_history, mem_total, diff --git a/src/config.rs b/src/config.rs index 73f98ff..85759be 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,12 @@ mod color; mod theme; -use std::{ffi::OsString, path::PathBuf}; +use std::path::PathBuf; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; -pub use color::RgbColor; +pub use color::SerdeColor; pub use theme::*; #[derive(clap::Parser)] @@ -32,7 +32,7 @@ pub struct Config { impl Config { pub fn load(cli: &Cli) -> Result { - let Some(config_dir_path) = config_path(cli) else { + let Some(config_dir_path) = config_path(cli)? else { return Ok(Default::default()); }; @@ -61,7 +61,11 @@ impl Config { }); }; - let theme = Theme::load_from_file(&config_dir_path.join("themes").join(theme))?; + let theme = if theme == "default" { + Theme::default() + } else { + Theme::load_from_file(&config_dir_path.join("themes").join(format!("{theme}.toml")))? + }; Ok(Self { theme }) } @@ -69,26 +73,35 @@ impl Config { #[derive(Debug, Serialize, Deserialize, Clone, Default)] struct RawConfig { - theme: Option, + theme: Option, } pub fn sample_config() -> String { toml::to_string_pretty(&RawConfig { - theme: Some("theme name (has to correspond with a valid theme file located in /themes/.toml)".into()) - }).unwrap() + theme: Some("default".into()), + }) + .unwrap() } -pub fn config_path(cli: &Cli) -> Option { +pub fn config_path(cli: &Cli) -> Result> { if let Some(ref path) = cli.config_path { if path.exists() { - return Some(path.clone()); + return Ok(Some(path.clone())); + } else { + return Err(anyhow!( + "The config path passed in the cli option does not exist" + )); } } if let Ok(path) = std::env::var("JWTOP_CONFIG_DIR") { let path: PathBuf = path.into(); if path.exists() { - return path.into(); + return Ok(path.into()); + } else { + return Err(anyhow!( + "The config path passed in the environment variable does not exist" + )); } } @@ -98,8 +111,8 @@ pub fn config_path(cli: &Cli) -> Option { .to_path_buf(); if path.exists() { - return Some(path); + return Ok(Some(path)); } - None + Ok(None) } diff --git a/src/config/color.rs b/src/config/color.rs index ff6e1f5..2b66a7c 100644 --- a/src/config/color.rs +++ b/src/config/color.rs @@ -1,19 +1,35 @@ +use tui::style::Color; + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct RgbColor(pub u8, pub u8, pub u8); +pub struct SerdeColor(pub Color); + +impl std::ops::Deref for SerdeColor { + type Target = Color; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} -impl serde::Serialize for RgbColor { +impl serde::Serialize for SerdeColor { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { - serializer.serialize_str(&format!("#{:02x}{:02x}{:02x}", self.0, self.1, self.2)) + let Color::Rgb(r, g, b) = self.0 else { + return Err(serde::ser::Error::custom( + "only rgb colors are serializable", + )); + }; + + serializer.serialize_str(&format!("#{:02x}{:02x}{:02x}", r, g, b)) } } struct RgbColorVisitor; impl<'de> serde::de::Visitor<'de> for RgbColorVisitor { - type Value = RgbColor; + type Value = SerdeColor; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a string containing a hex color value looking like this: #rrggbb or like this: #RRGGBB") @@ -60,11 +76,11 @@ impl<'de> serde::de::Visitor<'de> for RgbColorVisitor { let g = color([v[2], v[3]])?; let b = color([v[4], v[5]])?; - Ok(RgbColor(r, g, b)) + Ok(SerdeColor(Color::Rgb(r, g, b))) } } -impl<'de> serde::Deserialize<'de> for RgbColor { +impl<'de> serde::Deserialize<'de> for SerdeColor { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -73,32 +89,26 @@ impl<'de> serde::Deserialize<'de> for RgbColor { } } -impl From for tui::style::Color { - fn from(value: RgbColor) -> Self { - tui::style::Color::Rgb(value.0, value.1, value.2) - } -} - #[cfg(test)] mod tests { use super::*; #[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] struct Wrapper { - color: RgbColor, + color: SerdeColor, } #[test] fn color_de() { assert_eq!( Wrapper { - color: RgbColor(0xaa, 0xbb, 0xcc) + color: SerdeColor(tui::style::Color::Rgb(0xaa, 0xbb, 0xcc)) }, toml::from_str("color = \"#AABBCC\"").unwrap() ); assert_eq!( Wrapper { - color: RgbColor(0xdd, 0xee, 0xff) + color: SerdeColor(tui::style::Color::Rgb(0xdd, 0xee, 0xff)) }, toml::from_str("color = \"#ddeeff\"").unwrap() ); @@ -107,7 +117,7 @@ mod tests { #[test] fn both_ways() { let val = Wrapper { - color: RgbColor(12, 240, 144), + color: SerdeColor(tui::style::Color::Rgb(12, 240, 144)), }; assert_eq!( val, diff --git a/src/config/theme.rs b/src/config/theme.rs index 5d6f9e0..1999b73 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -4,95 +4,67 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use tui::style::Color; -use super::RgbColor; - -// TODO: actually, this is kind of stupid. Most of this can be handled by using serde attributes. +use super::SerdeColor; mod default_colors { - use crate::config::RgbColor; - - pub const RED: RgbColor = RgbColor(0xcc, 0x66, 0x66); - pub const GREEN: RgbColor = RgbColor(0xb5, 0xbd, 0x68); - - pub const YELLOW: RgbColor = RgbColor(0xf0, 0xc6, 0x74); + use crate::config::SerdeColor; - pub const BLUE: RgbColor = RgbColor(0x81, 0xa2, 0xbe); + pub fn red() -> SerdeColor { + SerdeColor(tui::style::Color::Red) + } - pub const MAGENTA: RgbColor = RgbColor(0xb2, 0x94, 0xbb); + pub fn green() -> SerdeColor { + SerdeColor(tui::style::Color::Green) + } - pub const CYAN: RgbColor = RgbColor(0x8a, 0xbe, 0xb7); + pub fn yellow() -> SerdeColor { + SerdeColor(tui::style::Color::Yellow) + } - pub const BG: RgbColor = RgbColor(0x1d, 0x1f, 0x21); -} + pub fn blue() -> SerdeColor { + SerdeColor(tui::style::Color::Blue) + } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct RawTheme { - widget: Option, - plot: Option, - bars: Option, - table: Option, -} + pub fn _magenta() -> SerdeColor { + SerdeColor(tui::style::Color::Magenta) + } -impl RawTheme { - fn sample_theme() -> Self { - Self { - widget: Some(RawWidgetTheme { - frame_color: Some(default_colors::CYAN), - title_color: Some(default_colors::CYAN), - background_color: Some(default_colors::BG), - }), - - plot: Some(RawPlotTheme { - axis_labels_color: Some(default_colors::CYAN), - plot_colors: vec![ - default_colors::BLUE, - default_colors::CYAN, - default_colors::GREEN, - default_colors::MAGENTA, - default_colors::RED, - default_colors::YELLOW, - ], - }), - - bars: Some(RawBarsTheme { - low_usage_color: Some(default_colors::GREEN), - medium_usage_color: Some(default_colors::YELLOW), - high_usage_color: Some(default_colors::RED), - }), - - table: Some(RawTableTheme { - header_color: Some(default_colors::BLUE), - row_color: Some(default_colors::BLUE), - }), - } + pub fn cyan() -> SerdeColor { + SerdeColor(tui::style::Color::Cyan) } -} -pub fn sample_theme() -> String { - toml::to_string_pretty(&RawTheme::sample_theme()).unwrap() -} + pub fn plot() -> Vec { + vec![ + SerdeColor(tui::style::Color::Blue), + SerdeColor(tui::style::Color::Cyan), + SerdeColor(tui::style::Color::Green), + SerdeColor(tui::style::Color::Magenta), + SerdeColor(tui::style::Color::Red), + SerdeColor(tui::style::Color::Yellow), + ] + } -fn tui_color(raw: Option, default: Color) -> Color { - raw.map(|c| c.into()).unwrap_or(default) + pub const RED: SerdeColor = SerdeColor(tui::style::Color::Rgb(0xcc, 0x66, 0x66)); + pub const GREEN: SerdeColor = SerdeColor(tui::style::Color::Rgb(0xb5, 0xbd, 0x68)); + pub const YELLOW: SerdeColor = SerdeColor(tui::style::Color::Rgb(0xf0, 0xc6, 0x74)); + pub const BLUE: SerdeColor = SerdeColor(tui::style::Color::Rgb(0x81, 0xa2, 0xbe)); + pub const MAGENTA: SerdeColor = SerdeColor(tui::style::Color::Rgb(0xb2, 0x94, 0xbb)); + pub const CYAN: SerdeColor = SerdeColor(tui::style::Color::Rgb(0x8a, 0xbe, 0xb7)); } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Theme { + #[serde(default)] pub widget: WidgetTheme, + + #[serde(default)] pub plot: PlotTheme, + + #[serde(default)] pub bars: BarsTheme, - pub table: TableTheme, -} -impl From for Theme { - fn from(value: RawTheme) -> Self { - Self { - widget: value.widget.map(|t| t.into()).unwrap_or_default(), - plot: value.plot.map(|t| t.into()).unwrap_or_default(), - bars: value.bars.map(|t| t.into()).unwrap_or_default(), - table: value.table.map(|t| t.into()).unwrap_or_default(), - } - } + #[serde(default)] + pub table: TableTheme, } impl Theme { @@ -100,156 +72,111 @@ impl Theme { let file = std::fs::read_to_string(path) .with_context(|| format!("Failed to read theme from {}", path.to_string_lossy()))?; - let raw: RawTheme = toml::from_str(&file).context("Failed to deserialize theme")?; + let theme: Theme = toml::from_str(&file).context("Failed to deserialize theme")?; - Ok(raw.into()) + Ok(theme) } -} -#[derive(Debug, Clone, Copy, Deserialize, Serialize)] -struct RawWidgetTheme { - frame_color: Option, - title_color: Option, - background_color: Option, -} + pub fn sample_theme() -> Self { + Self { + widget: WidgetTheme { + frame_color: default_colors::CYAN, + title_color: default_colors::CYAN, + }, -#[derive(Debug, Clone, Copy)] -pub struct WidgetTheme { - pub frame_color: Color, - pub title_color: Color, -} + plot: PlotTheme { + axis_labels_color: default_colors::CYAN, + plot_colors: vec![ + default_colors::BLUE, + default_colors::CYAN, + default_colors::GREEN, + default_colors::MAGENTA, + default_colors::RED, + default_colors::YELLOW, + ], + }, -impl Default for WidgetTheme { - fn default() -> Self { - Self { - frame_color: Color::Cyan, - title_color: Color::Cyan, + bars: BarsTheme { + low_usage_color: default_colors::GREEN, + medium_usage_color: default_colors::YELLOW, + high_usage_color: default_colors::RED, + }, + + table: TableTheme { + header_color: default_colors::BLUE, + row_color: default_colors::BLUE, + }, } } } -impl From for WidgetTheme { - fn from(value: RawWidgetTheme) -> Self { - let default = Self::default(); +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct WidgetTheme { + #[serde(default = "default_colors::cyan")] + pub frame_color: SerdeColor, + #[serde(default = "default_colors::cyan")] + pub title_color: SerdeColor, +} +impl Default for WidgetTheme { + fn default() -> Self { Self { - frame_color: tui_color(value.frame_color, default.frame_color), - title_color: tui_color(value.title_color, default.title_color), + frame_color: SerdeColor(Color::Cyan), + title_color: SerdeColor(Color::Cyan), } } } -#[derive(Debug, Clone, Deserialize, Serialize)] -struct RawPlotTheme { - axis_labels_color: Option, - plot_colors: Vec, -} - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct PlotTheme { - pub axis_labels_color: Color, - pub plot_colors: Vec, + #[serde(default = "default_colors::cyan")] + pub axis_labels_color: SerdeColor, + #[serde(default = "default_colors::plot")] + pub plot_colors: Vec, } impl Default for PlotTheme { fn default() -> Self { Self { - axis_labels_color: Color::Cyan, - plot_colors: vec![ - Color::Blue, - Color::Cyan, - Color::Green, - Color::Magenta, - Color::Red, - Color::Yellow, - ], - } - } -} - -impl From for PlotTheme { - fn from(value: RawPlotTheme) -> Self { - let default = Self::default(); - - Self { - axis_labels_color: tui_color(value.axis_labels_color, default.axis_labels_color), - plot_colors: if !value.plot_colors.is_empty() { - value - .plot_colors - .into_iter() - .map(|c| c.into()) - .collect::>() - } else { - default.plot_colors - }, + axis_labels_color: SerdeColor(Color::Cyan), + plot_colors: default_colors::plot(), } } } -#[derive(Debug, Clone, Copy, Deserialize, Serialize)] -struct RawBarsTheme { - low_usage_color: Option, - medium_usage_color: Option, - high_usage_color: Option, -} - -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct BarsTheme { - pub low_usage_color: Color, - pub medium_usage_color: Color, - pub high_usage_color: Color, + #[serde(default = "default_colors::green")] + pub low_usage_color: SerdeColor, + #[serde(default = "default_colors::yellow")] + pub medium_usage_color: SerdeColor, + #[serde(default = "default_colors::red")] + pub high_usage_color: SerdeColor, } impl Default for BarsTheme { fn default() -> Self { Self { - low_usage_color: Color::Green, - medium_usage_color: Color::Yellow, - high_usage_color: Color::Red, - } - } -} - -impl From for BarsTheme { - fn from(value: RawBarsTheme) -> Self { - let default = Self::default(); - - Self { - low_usage_color: tui_color(value.low_usage_color, default.low_usage_color), - medium_usage_color: tui_color(value.medium_usage_color, default.medium_usage_color), - high_usage_color: tui_color(value.high_usage_color, default.high_usage_color), + low_usage_color: SerdeColor(Color::Green), + medium_usage_color: SerdeColor(Color::Yellow), + high_usage_color: SerdeColor(Color::Red), } } } -#[derive(Debug, Clone, Copy, Deserialize, Serialize)] -struct RawTableTheme { - header_color: Option, - row_color: Option, -} - -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct TableTheme { - pub header_color: Color, - pub row_color: Color, + #[serde(default = "default_colors::blue")] + pub header_color: SerdeColor, + #[serde(default = "default_colors::blue")] + pub row_color: SerdeColor, } impl Default for TableTheme { fn default() -> Self { Self { - header_color: Color::Blue, - row_color: Color::Blue, - } - } -} - -impl From for TableTheme { - fn from(value: RawTableTheme) -> Self { - let default = Self::default(); - - Self { - header_color: tui_color(value.header_color, default.header_color), - row_color: tui_color(value.row_color, default.row_color), + header_color: SerdeColor(Color::Blue), + row_color: SerdeColor(Color::Blue), } } } diff --git a/src/main.rs b/src/main.rs index b9fd8d3..3f58252 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,14 +18,18 @@ fn main() -> AppResult<()> { } if cli.dump_sample_theme { - println!("{}", jwtop::config::sample_theme()); + println!( + "{}", + toml::to_string_pretty(&jwtop::config::Theme::sample_theme()).unwrap() + ); + return Ok(()); } - // TODO: load the config and actually use it in the app. + let config = jwtop::config::Config::load(&cli)?; // Create an application. - let mut app = App::new(); + let mut app = App::new(config); // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stderr()); diff --git a/src/ui.rs b/src/ui.rs index 0fdebff..82af05c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -16,7 +16,8 @@ pub mod processes; /// Renders the user interface widgets. pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { - let style = Style::default().fg(Color::Cyan); + let block_style = Style::default().fg(*app.config.theme.widget.frame_color); + let title_style = Style::default().fg(*app.config.theme.widget.title_color); let block = Block::default() .borders(Borders::all()) .border_type(BorderType::Rounded); @@ -44,17 +45,18 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { &app.cpu_history, Box::new(|percentage, i| format!("cpu{i}: {percentage:.1}%")), [0.0, 100.0], + &app.config, ) - .style(style) - .block(block.clone().title("cpu")) + .style(block_style) + .block(block.clone().title(Line::styled("cpu", title_style))) .label_suffix('%'), cpus[0], ); frame.render_widget( CpusBars::new(app) - .style(style) - .block(block.clone().title("cpu")), + .style(block_style) + .block(block.clone().title(Line::styled("cpu", title_style))), cpus[1], ); @@ -63,22 +65,25 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { &[app.mem_history.clone()], Box::new(|used_mem, _| format!("used mem: {used_mem:.1}{}", app.mem_prefix.prefix())), [0.0, app.mem_total], + &app.config, ) - .style(style) - .block(block.clone().title("mem")) + .style(block_style) + .block(block.clone().title(Line::styled("mem", title_style))) .label_suffix(app.mem_prefix.prefix()), mem_and_disks[0], ); frame.render_widget( Disks::new(app) - .block(block.clone().title("disks")) - .style(style), + .block(block.clone().title(Line::styled("disks", title_style))) + .style(block_style), mem_and_disks[1], ); frame.render_widget( - Processes::new(app).block(block.title("procs")).style(style), + Processes::new(app) + .block(block.title(Line::styled("procs", title_style))) + .style(block_style), layout[2], ) } diff --git a/src/ui/chart_wrapper.rs b/src/ui/chart_wrapper.rs index d88f123..2889e5e 100644 --- a/src/ui/chart_wrapper.rs +++ b/src/ui/chart_wrapper.rs @@ -2,13 +2,16 @@ use std::collections::VecDeque; use tui::{ layout::{Alignment, Constraint}, - style::{Color, Style}, + style::Style, symbols::Marker, text::Span, widgets::{Axis, Block, Chart, Dataset, GraphType, Widget}, }; -use crate::app::HISTORY_LEN; +use crate::{ + app::HISTORY_LEN, + config::{Config, PlotTheme}, +}; pub struct ChartWrapper<'a, 'b> { data: Vec>, @@ -17,6 +20,7 @@ pub struct ChartWrapper<'a, 'b> { label_generator: Box String + 'a>, range: [f64; 2], label_suffix: Option, + theme: PlotTheme, } impl<'a, 'b> ChartWrapper<'a, 'b> { @@ -24,6 +28,7 @@ impl<'a, 'b> ChartWrapper<'a, 'b> { data: &[VecDeque], label_generator: Box String + 'a>, range: [f64; 2], + config: &Config, ) -> Self { let data = data .iter() @@ -42,6 +47,7 @@ impl<'a, 'b> ChartWrapper<'a, 'b> { label_generator, range, label_suffix: None, + theme: config.theme.plot.clone(), } } @@ -66,16 +72,7 @@ impl<'a, 'b> ChartWrapper<'a, 'b> { impl<'a, 'b> Widget for ChartWrapper<'a, 'b> { fn render(self, area: tui::layout::Rect, buf: &mut tui::buffer::Buffer) { - let colors = [ - Color::Blue, - Color::Cyan, - Color::Green, - Color::Magenta, - Color::Red, - Color::Yellow, - ] - .iter() - .cycle(); + let colors = self.theme.plot_colors.iter().cycle(); let datasets = self .data @@ -88,24 +85,41 @@ impl<'a, 'b> Widget for ChartWrapper<'a, 'b> { .graph_type(GraphType::Line) .marker(Marker::Braille) .name((self.label_generator)(data.last().unwrap().1, i)) - .style(Style::default().fg(color)) + .style(Style::default().fg(*color)) }) .collect(); let label_suffix = self.label_suffix.map(String::from).unwrap_or_default(); + let axis_label_style = Style::default().fg(*self.theme.axis_labels_color); + let mut chart = Chart::new(datasets) .x_axis(Axis::default().bounds([0.0, HISTORY_LEN as f64])) .y_axis( Axis::default() .bounds(self.range) .labels(vec![ - Span::raw(""), - Span::raw(format!("{:.0}{label_suffix}", self.range[1] / 5.0)), - Span::raw(format!("{:.0}{label_suffix}", self.range[1] * 2.0 / 5.0)), - Span::raw(format!("{:.0}{label_suffix}", self.range[1] * 3.0 / 5.0)), - Span::raw(format!("{:.0}{label_suffix}", self.range[1] * 4.0 / 5.0)), - Span::raw(format!("{:.0}{label_suffix}", self.range[1])), + Span::styled("", axis_label_style), + Span::styled( + format!("{:.0}{label_suffix}", self.range[1] / 5.0), + axis_label_style, + ), + Span::styled( + format!("{:.0}{label_suffix}", self.range[1] * 2.0 / 5.0), + axis_label_style, + ), + Span::styled( + format!("{:.0}{label_suffix}", self.range[1] * 3.0 / 5.0), + axis_label_style, + ), + Span::styled( + format!("{:.0}{label_suffix}", self.range[1] * 4.0 / 5.0), + axis_label_style, + ), + Span::styled( + format!("{:.0}{label_suffix}", self.range[1]), + axis_label_style, + ), ]) .labels_alignment(Alignment::Right), ) diff --git a/src/ui/cpus_bars.rs b/src/ui/cpus_bars.rs index 6cd80b1..fbd5147 100644 --- a/src/ui/cpus_bars.rs +++ b/src/ui/cpus_bars.rs @@ -1,15 +1,16 @@ use tui::{ layout::{Constraint, Direction, Layout}, - style::{Color, Style}, + style::Style, widgets::{Block, Borders, Gauge, Widget}, }; -use crate::app::App; +use crate::{app::App, config::BarsTheme}; pub struct CpusBars<'a> { cpus: Vec, style: Style, block: Option>, + theme: BarsTheme, } impl<'a> CpusBars<'a> { @@ -25,6 +26,7 @@ impl<'a> CpusBars<'a> { cpus, style: Default::default(), block: Default::default(), + theme: app.config.theme.bars, } } @@ -83,11 +85,11 @@ impl<'a> Widget for CpusBars<'a> { .enumerate() .for_each(|(i, (area, val))| { let color = if val < 50.0 { - Color::Green + *self.theme.low_usage_color } else if val < 80.0 { - Color::Yellow + *self.theme.medium_usage_color } else { - Color::Red + *self.theme.high_usage_color }; Gauge::default() diff --git a/src/ui/disks.rs b/src/ui/disks.rs index 7597c0b..70e3ef9 100644 --- a/src/ui/disks.rs +++ b/src/ui/disks.rs @@ -61,6 +61,7 @@ impl<'a, 'b> Disks<'a, 'b> { ) }), [0.0, max], + &app.config, ) .label_suffix('M'); diff --git a/src/ui/processes.rs b/src/ui/processes.rs index 561ddc6..fe3cd93 100644 --- a/src/ui/processes.rs +++ b/src/ui/processes.rs @@ -6,7 +6,10 @@ use tui::{ widgets::{block::Title, Block, Row, Table, Widget}, }; -use crate::app::{App, InputState, MemPrefix, ProcessInfo}; +use crate::{ + app::{App, InputState, MemPrefix, ProcessInfo}, + config::TableTheme, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SortDirection { @@ -129,6 +132,7 @@ pub struct Processes<'b> { processes: Vec, style: Style, block: Option>, + theme: TableTheme, sorting: InputState, } @@ -139,6 +143,7 @@ impl<'b> Processes<'b> { processes: app.processes.clone(), style: Default::default(), block: Default::default(), + theme: app.config.theme.table, sorting: app.input_state.clone(), } @@ -189,7 +194,7 @@ impl<'b> Widget for Processes<'b> { .iter() .map(|c| c.extract_data_as_string(&p)), ) - .style(Style::default().fg(tui::style::Color::Blue)) + .style(Style::default().fg(*self.theme.row_color)) })) .column_spacing(1) .widths(&[Constraint::Ratio(1, 6); 6]) @@ -207,7 +212,7 @@ impl<'b> Widget for Processes<'b> { ) .style( Style::default() - .fg(tui::style::Color::Blue) + .fg(*self.theme.header_color) .add_modifier(Modifier::BOLD), ), )