diff --git a/.github/workflows/validate_schema.yml b/.github/workflows/validate_schema.yml index d14d7f66..34e9f197 100644 --- a/.github/workflows/validate_schema.yml +++ b/.github/workflows/validate_schema.yml @@ -49,7 +49,9 @@ jobs: - name: Test nightly validates on valid sample configs run: | python3 scripts/schema/validator.py -s ./schema/nightly/bottom.json -f ./sample_configs/default_config.toml + python3 scripts/schema/validator.py --uncomment -s ./schema/nightly/bottom.json -f ./sample_configs/default_config.toml python3 scripts/schema/validator.py -s ./schema/nightly/bottom.json -f ./sample_configs/demo_config.toml + - name: Test nightly catches on a bad sample config run: | diff --git a/Cargo.lock b/Cargo.lock index 1d72c7e8..bd0b7409 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,7 @@ dependencies = [ "serde", "serde_json", "starship-battery", + "strum", "sysctl", "sysinfo", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 0edfa260..1d3d0920 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ nvidia = ["nvml-wrapper"] gpu = ["nvidia"] zfs = [] logging = ["fern", "log", "time/local-offset"] -generate_schema = ["schemars", "serde_json"] +generate_schema = ["schemars", "serde_json", "strum"] deploy = ["battery", "gpu", "zfs"] default = ["deploy"] @@ -85,18 +85,14 @@ concat-string = "1.0.1" crossterm = "0.27.0" ctrlc = { version = "3.4.4", features = ["termination"] } dirs = "5.0.1" -fern = { version = "0.6.2", optional = true } hashbrown = "0.14.5" humantime = "2.1.0" indexmap = "2.2.6" indoc = "2.0.5" itertools = "0.13.0" -log = { version = "0.4.21", optional = true } nvml-wrapper = { version = "0.10.0", optional = true, features = ["legacy-functions"] } regex = "1.10.4" -schemars = { version = "0.8.21", optional = true } serde = { version = "1.0.203", features = ["derive"] } -serde_json = { version = "1.0.117", optional = true } starship-battery = { version = "0.8.3", optional = true } sysinfo = "=0.30.12" thiserror = "1.0.61" @@ -107,6 +103,15 @@ unicode-ellipsis = "0.1.4" unicode-segmentation = "1.11.0" unicode-width = "0.1.12" +# Used for logging. +fern = { version = "0.6.2", optional = true } +log = { version = "0.4.21", optional = true } + +# These are just used for for schema generation. +schemars = { version = "0.8.21", optional = true } +serde_json = { version = "1.0.117", optional = true } +strum = { version = "0.26", features = ["derive"], optional = true } + [target.'cfg(unix)'.dependencies] libc = "0.2.155" diff --git a/sample_configs/default_config.toml b/sample_configs/default_config.toml index 4855b008..f5ab351f 100644 --- a/sample_configs/default_config.toml +++ b/sample_configs/default_config.toml @@ -27,13 +27,13 @@ #whole_word = false # Whether to make process searching use regex by default. #regex = false -# Defaults to Celsius. Temperature is one of: -#temperature_type = "k" -#temperature_type = "f" +# The temperature unit. One of the following, defaults to "c" for Celsius: #temperature_type = "c" -#temperature_type = "kelvin" -#temperature_type = "fahrenheit" -#temperature_type = "celsius" +##temperature_type = "k" +##temperature_type = "f" +##temperature_type = "kelvin" +##temperature_type = "fahrenheit" +##temperature_type = "celsius" # The default time interval (in milliseconds). #default_time_value = "60s" # The time delta on each zoom in/out action (in milliseconds). @@ -84,7 +84,7 @@ #[processes] # The columns shown by the process widget. The following columns are supported: # PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU% -#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMEM%", "GPU%"] +#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"] # CPU widget configuration #[cpu] diff --git a/schema/nightly/bottom.json b/schema/nightly/bottom.json index ac3ec53f..22eecb4c 100644 --- a/schema/nightly/bottom.json +++ b/schema/nightly/bottom.json @@ -87,6 +87,7 @@ }, "definitions": { "ColoursConfig": { + "description": "Colour configuration.", "type": "object", "properties": { "all_cpu_color": { @@ -242,7 +243,7 @@ } }, "CpuConfig": { - "description": "Process column settings.", + "description": "CPU column settings.", "type": "object", "properties": { "default": { @@ -611,23 +612,33 @@ } } }, - "ProcWidgetColumn": { - "description": "A hacky workaround for now.", + "ProcColumn": { + "description": "A column in the process widget.", "type": "string", "enum": [ - "PidOrCount", - "ProcNameOrCommand", - "Cpu", + "PID", + "Count", + "Name", + "Command", + "CPU%", "Mem", - "ReadPerSecond", - "WritePerSecond", - "TotalRead", - "TotalWrite", - "User", + "Mem%", + "R/s", + "Read", + "Rps", + "W/s", + "Write", + "Wps", + "T.Read", + "TWrite", + "T.Write", + "TRead", "State", + "User", "Time", - "GpuMem", - "GpuUtil" + "GMem", + "GMem%", + "GPU%" ] }, "ProcessesConfig": { @@ -638,7 +649,7 @@ "description": "A list of process widget columns.", "type": "array", "items": { - "$ref": "#/definitions/ProcWidgetColumn" + "$ref": "#/definitions/ProcColumn" } } } diff --git a/scripts/schema/validator.py b/scripts/schema/validator.py index d537f928..7a2902d1 100644 --- a/scripts/schema/validator.py +++ b/scripts/schema/validator.py @@ -5,6 +5,8 @@ import argparse import toml import jsonschema_rs +import re +import traceback def main(): @@ -17,6 +19,12 @@ def main(): parser.add_argument( "-s", "--schema", type=str, required=True, help="The schema to use." ) + parser.add_argument( + "--uncomment", + required=False, + action="store_true", + help="Uncomment the settings inside the file.", + ) parser.add_argument( "--should_fail", required=False, @@ -28,22 +36,38 @@ def main(): file = args.file schema = args.schema should_fail = args.should_fail + uncomment = args.uncomment with open(file) as f, open(schema) as s: try: validator = jsonschema_rs.JSONSchema.from_str(s.read()) except: - print("Coudln't create validator.") + print("Couldn't create validator.") exit() - is_valid = validator.is_valid(toml.load(f)) - if is_valid: + if uncomment: + read_file = f.read() + read_file = re.sub(r"^#([a-zA-Z\[])", r"\1", read_file, flags=re.MULTILINE) + read_file = re.sub( + r"^#(\s\s+)([a-zA-Z\[])", r"\2", read_file, flags=re.MULTILINE + ) + print(f"uncommented file: \n{read_file}") + + toml_str = toml.loads(read_file) + else: + toml_str = toml.load(f) + + try: + validator.validate(toml_str) if should_fail: - print("Fail!") + print("Fail! Should have errored.") exit(1) else: print("All good!") - else: + except jsonschema_rs.ValidationError as err: + print(f"Caught error: `{err}`") + print(traceback.format_exc()) + if should_fail: print("Caught error, good!") else: diff --git a/src/constants.rs b/src/constants.rs index 797de3f9..d9aa9af8 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -545,13 +545,13 @@ pub const CONFIG_TEXT: &str = r#"# This is a default config file for bottom. Al #whole_word = false # Whether to make process searching use regex by default. #regex = false -# Defaults to Celsius. Temperature is one of: -#temperature_type = "k" -#temperature_type = "f" +# The temperature unit. One of the following, defaults to "c" for Celsius: #temperature_type = "c" -#temperature_type = "kelvin" -#temperature_type = "fahrenheit" -#temperature_type = "celsius" +##temperature_type = "k" +##temperature_type = "f" +##temperature_type = "kelvin" +##temperature_type = "fahrenheit" +##temperature_type = "celsius" # The default time interval (in milliseconds). #default_time_value = "60s" # The time delta on each zoom in/out action (in milliseconds). @@ -606,7 +606,7 @@ pub const CONFIG_TEXT: &str = r#"# This is a default config file for bottom. Al #[processes] # The columns shown by the process widget. The following columns are supported: # PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU% -#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMEM%", "GPU%"] +#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"] # CPU widget configuration #[cpu] diff --git a/src/main.rs b/src/main.rs index 4f6e4c9f..cb30aed9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -278,28 +278,47 @@ fn create_collection_thread( }) } +#[cfg(feature = "generate_schema")] +fn generate_schema() -> anyhow::Result<()> { + let mut schema = schemars::schema_for!(crate::options::config::ConfigV1); + { + use itertools::Itertools; + use strum::VariantArray; + + let proc_columns = schema.definitions.get_mut("ProcColumn").unwrap(); + match proc_columns { + schemars::schema::Schema::Object(proc_columns) => { + let enums = proc_columns.enum_values.as_mut().unwrap(); + *enums = options::config::process::ProcColumn::VARIANTS + .iter() + .flat_map(|var| var.get_schema_names()) + .map(|v| serde_json::Value::String(v.to_string())) + .dedup() + .collect(); + } + _ => anyhow::bail!("missing proc columns definition"), + } + } + + let metadata = schema.schema.metadata.as_mut().unwrap(); + metadata.id = Some( + "https://github.com/ClementTsang/bottom/blob/main/schema/nightly/bottom.json".to_string(), + ); + metadata.description = + Some("https://clementtsang.github.io/bottom/nightly/configuration/config-file".to_string()); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); + + Ok(()) +} + fn main() -> anyhow::Result<()> { // let _profiler = dhat::Profiler::new_heap(); let args = args::get_args(); #[cfg(feature = "generate_schema")] - { - if args.other.generate_schema { - let mut schema = schemars::schema_for!(crate::options::config::ConfigV1); - let metadata = schema.schema.metadata.as_mut().unwrap(); - metadata.id = Some( - "https://github.com/ClementTsang/bottom/blob/main/schema/nightly/bottom.json" - .to_string(), - ); - metadata.description = Some( - "https://clementtsang.github.io/bottom/nightly/configuration/config-file" - .to_string(), - ); - println!("{}", serde_json::to_string_pretty(&schema).unwrap()); - - return Ok(()); - } + if args.other.generate_schema { + return generate_schema(); } #[cfg(feature = "logging")] diff --git a/src/options.rs b/src/options.rs index f45699d1..b2020054 100644 --- a/src/options.rs +++ b/src/options.rs @@ -183,7 +183,9 @@ pub fn init_app( if cfg.columns.is_empty() { None } else { - Some(IndexSet::from_iter(cfg.columns.clone())) + Some(IndexSet::from_iter( + cfg.columns.iter().map(ProcWidgetColumn::from), + )) } }) }; diff --git a/src/options/colours.rs b/src/options/colours.rs index 1a30dfab..0f5edfba 100644 --- a/src/options/colours.rs +++ b/src/options/colours.rs @@ -2,9 +2,11 @@ use std::borrow::Cow; use serde::{Deserialize, Serialize}; +/// Colour configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] pub struct ColoursConfig { + // TODO: Make these an enum instead. pub table_header_color: Option>, pub all_cpu_color: Option>, pub avg_cpu_color: Option>, diff --git a/src/options/config/cpu.rs b/src/options/config/cpu.rs index 240b070d..ff471768 100644 --- a/src/options/config/cpu.rs +++ b/src/options/config/cpu.rs @@ -12,7 +12,7 @@ pub enum CpuDefault { Average, } -/// Process column settings. +/// CPU column settings. #[derive(Clone, Debug, Default, Deserialize)] #[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] pub struct CpuConfig { diff --git a/src/options/config/process.rs b/src/options/config/process.rs index 3a98a912..978c1bac 100644 --- a/src/options/config/process.rs +++ b/src/options/config/process.rs @@ -8,12 +8,121 @@ use crate::widgets::ProcWidgetColumn; pub struct ProcessesConfig { /// A list of process widget columns. #[serde(default)] - pub columns: Vec, + pub columns: Vec, +} + +/// A column in the process widget. +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "generate_schema", + derive(schemars::JsonSchema, strum::VariantArray) +)] +pub enum ProcColumn { + Pid, + Count, + Name, + Command, + CpuPercent, + Mem, + MemPercent, + Read, + Write, + TotalRead, + TotalWrite, + State, + User, + Time, + #[cfg(feature = "gpu")] + GpuMem, + #[cfg(feature = "gpu")] + GpuPercent, +} + +impl ProcColumn { + /// An ugly hack to generate the JSON schema. + #[cfg(feature = "generate_schema")] + pub fn get_schema_names(&self) -> &[&'static str] { + match self { + ProcColumn::Pid => &["PID"], + ProcColumn::Count => &["Count"], + ProcColumn::Name => &["Name"], + ProcColumn::Command => &["Command"], + ProcColumn::CpuPercent => &["CPU%"], + ProcColumn::Mem => &["Mem"], + ProcColumn::MemPercent => &["Mem%"], + ProcColumn::Read => &["R/s", "Read", "Rps"], + ProcColumn::Write => &["W/s", "Write", "Wps"], + ProcColumn::TotalRead => &["T.Read", "TWrite"], + ProcColumn::TotalWrite => &["T.Write", "TRead"], + ProcColumn::State => &["State"], + ProcColumn::User => &["User"], + ProcColumn::Time => &["Time"], + #[cfg(feature = "gpu")] + ProcColumn::GpuMem => &["GMem", "GMem%"], + #[cfg(feature = "gpu")] + ProcColumn::GpuPercent => &["GPU%"], + } + } +} + +impl<'de> Deserialize<'de> for ProcColumn { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?.to_lowercase(); + match value.as_str() { + "cpu%" => Ok(ProcColumn::CpuPercent), + "mem" => Ok(ProcColumn::Mem), + "mem%" => Ok(ProcColumn::Mem), + "pid" => Ok(ProcColumn::Pid), + "count" => Ok(ProcColumn::Count), + "name" => Ok(ProcColumn::Name), + "command" => Ok(ProcColumn::Command), + "read" | "r/s" | "rps" => Ok(ProcColumn::Read), + "write" | "w/s" | "wps" => Ok(ProcColumn::Write), + "tread" | "t.read" => Ok(ProcColumn::TotalRead), + "twrite" | "t.write" => Ok(ProcColumn::TotalWrite), + "state" => Ok(ProcColumn::State), + "user" => Ok(ProcColumn::User), + "time" => Ok(ProcColumn::Time), + #[cfg(feature = "gpu")] + "gmem" | "gmem%" => Ok(ProcColumn::GpuMem), + #[cfg(feature = "gpu")] + "gpu%" => Ok(ProcColumn::GpuPercent), + _ => Err(serde::de::Error::custom("doesn't match any column type")), + } + } +} + +impl From<&ProcColumn> for ProcWidgetColumn { + fn from(value: &ProcColumn) -> Self { + match value { + ProcColumn::Pid => ProcWidgetColumn::PidOrCount, + ProcColumn::Count => ProcWidgetColumn::PidOrCount, + ProcColumn::Name => ProcWidgetColumn::ProcNameOrCommand, + ProcColumn::Command => ProcWidgetColumn::ProcNameOrCommand, + ProcColumn::CpuPercent => ProcWidgetColumn::Cpu, + ProcColumn::Mem => ProcWidgetColumn::Mem, + ProcColumn::MemPercent => ProcWidgetColumn::Mem, + ProcColumn::Read => ProcWidgetColumn::ReadPerSecond, + ProcColumn::Write => ProcWidgetColumn::WritePerSecond, + ProcColumn::TotalRead => ProcWidgetColumn::TotalRead, + ProcColumn::TotalWrite => ProcWidgetColumn::TotalWrite, + ProcColumn::State => ProcWidgetColumn::State, + ProcColumn::User => ProcWidgetColumn::User, + ProcColumn::Time => ProcWidgetColumn::Time, + #[cfg(feature = "gpu")] + ProcColumn::GpuMem => ProcWidgetColumn::GpuMem, + #[cfg(feature = "gpu")] + ProcColumn::GpuPercent => ProcWidgetColumn::GpuUtil, + } + } } #[cfg(test)] mod test { - use super::ProcessesConfig; + use super::{ProcColumn, ProcessesConfig}; use crate::widgets::ProcWidgetColumn; #[test] @@ -23,6 +132,13 @@ mod test { assert!(generated.columns.is_empty()); } + fn to_columns(columns: Vec) -> Vec { + columns + .iter() + .map(ProcWidgetColumn::from) + .collect::>() + } + #[test] fn process_column_settings() { let config = r#" @@ -31,7 +147,7 @@ mod test { let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap(); assert_eq!( - generated.columns, + to_columns(generated.columns), vec![ ProcWidgetColumn::Cpu, ProcWidgetColumn::PidOrCount, @@ -58,18 +174,30 @@ mod test { fn process_column_settings_3() { let config = r#"columns = ["Twrite", "T.Write"]"#; let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap(); - assert_eq!(generated.columns, vec![ProcWidgetColumn::TotalWrite; 2]); + assert_eq!( + to_columns(generated.columns), + vec![ProcWidgetColumn::TotalWrite; 2] + ); let config = r#"columns = ["Tread", "T.read"]"#; let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap(); - assert_eq!(generated.columns, vec![ProcWidgetColumn::TotalRead; 2]); + assert_eq!( + to_columns(generated.columns), + vec![ProcWidgetColumn::TotalRead; 2] + ); let config = r#"columns = ["read", "rps", "r/s"]"#; let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap(); - assert_eq!(generated.columns, vec![ProcWidgetColumn::ReadPerSecond; 3]); + assert_eq!( + to_columns(generated.columns), + vec![ProcWidgetColumn::ReadPerSecond; 3] + ); let config = r#"columns = ["write", "wps", "w/s"]"#; let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap(); - assert_eq!(generated.columns, vec![ProcWidgetColumn::WritePerSecond; 3]); + assert_eq!( + to_columns(generated.columns), + vec![ProcWidgetColumn::WritePerSecond; 3] + ); } } diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs index 1b07274b..6599fee8 100644 --- a/src/widgets/process_table.rs +++ b/src/widgets/process_table.rs @@ -9,7 +9,6 @@ use indexmap::IndexSet; use itertools::Itertools; pub use proc_widget_column::*; pub use proc_widget_data::*; -use serde::{de::Error, Deserialize}; use sort_table::SortTableColumn; use crate::{ @@ -111,7 +110,6 @@ pub struct ProcTableConfig { /// A hacky workaround for now. #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] -#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] pub enum ProcWidgetColumn { PidOrCount, ProcNameOrCommand, @@ -130,36 +128,6 @@ pub enum ProcWidgetColumn { GpuUtil, } -impl<'de> Deserialize<'de> for ProcWidgetColumn { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value = String::deserialize(deserializer)?.to_lowercase(); - match value.as_str() { - "cpu%" => Ok(ProcWidgetColumn::Cpu), - "mem" => Ok(ProcWidgetColumn::Mem), - "mem%" => Ok(ProcWidgetColumn::Mem), - "pid" => Ok(ProcWidgetColumn::PidOrCount), - "count" => Ok(ProcWidgetColumn::PidOrCount), - "name" => Ok(ProcWidgetColumn::ProcNameOrCommand), - "command" => Ok(ProcWidgetColumn::ProcNameOrCommand), - "read" | "r/s" | "rps" => Ok(ProcWidgetColumn::ReadPerSecond), - "write" | "w/s" | "wps" => Ok(ProcWidgetColumn::WritePerSecond), - "tread" | "t.read" => Ok(ProcWidgetColumn::TotalRead), - "twrite" | "t.write" => Ok(ProcWidgetColumn::TotalWrite), - "state" => Ok(ProcWidgetColumn::State), - "user" => Ok(ProcWidgetColumn::User), - "time" => Ok(ProcWidgetColumn::Time), - #[cfg(feature = "gpu")] - "gmem" | "gmem%" => Ok(ProcWidgetColumn::GpuMem), - #[cfg(feature = "gpu")] - "gpu%" => Ok(ProcWidgetColumn::GpuUtil), - _ => Err(Error::custom("doesn't match any column type")), - } - } -} - // This is temporary. Switch back to `ProcColumn` later! pub struct ProcWidgetState {