diff --git a/Cargo.lock b/Cargo.lock index 003b2fa28..c6cc9f2b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -697,6 +697,41 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.100", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.100", +] + [[package]] name = "data-encoding" version = "2.8.0" @@ -786,6 +821,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + [[package]] name = "either" version = "1.15.0" @@ -1134,6 +1175,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.2" @@ -1151,7 +1198,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1335,6 +1382,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1356,6 +1409,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.8.0" @@ -1363,7 +1427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1379,7 +1443,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "232929e1d75fe899576a3d5c7416ad0d88dbfbb3c3d6aa00873a7408a50ddb88" dependencies = [ "ahash", - "indexmap", + "indexmap 2.8.0", "is-terminal", "itoa", "log", @@ -1708,11 +1772,16 @@ dependencies = [ "miette", "nu-ansi-term 0.50.1", "rustyline", + "schemars", + "serde", "shlex", "syntect", + "toml", + "toml_edit", "tracing", "tracing-appender", "tracing-subscriber", + "validator", ] [[package]] @@ -1904,7 +1973,7 @@ dependencies = [ "cc", "env_logger 0.11.7", "fallible-iterator", - "indexmap", + "indexmap 2.8.0", "log", "memchr", "miette", @@ -2018,7 +2087,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" dependencies = [ - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -2259,6 +2328,7 @@ version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ + "serde", "windows-sys 0.52.0", ] @@ -2441,7 +2511,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ "base64", - "indexmap", + "indexmap 2.8.0", "quick-xml 0.32.0", "serde", "time", @@ -2570,6 +2640,28 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -3065,6 +3157,31 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.100", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3097,6 +3214,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "serde_json" version = "1.0.140" @@ -3109,6 +3237,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3518,6 +3655,48 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "indexmap 2.8.0", + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap 2.8.0", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "tracing" version = "0.1.41" @@ -3686,6 +3865,36 @@ dependencies = [ "getrandom 0.3.2", ] +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "valuable" version = "0.1.1" @@ -4079,6 +4288,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e27d6ad3dac991091e4d35de9ba2d2d00647c5d0fc26c5496dee55984ae111b" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 253c08b45..db2635e88 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -33,7 +33,7 @@ limbo_core = { path = "../core", default-features = true, features = [ "completion", ] } miette = { version = "7.4.0", features = ["fancy"] } -nu-ansi-term = "0.50.1" +nu-ansi-term = {version = "0.50.1", features = ["serde", "derive_serde_style"]} rustyline = { version = "15.0.0", default-features = true, features = [ "derive", ] } @@ -42,6 +42,11 @@ syntect = "5.2.0" tracing = "0.1.41" tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +toml = {version = "0.8.20", features = ["preserve_order"]} +schemars = {version = "0.8.22", features = ["preserve_order"]} +serde = {version = "1.0.218", features = ["derive"]} +validator = {version = "0.20.0", features = ["derive"]} +toml_edit = {version = "0.22.24", features = ["serde"]} [features] default = ["io_uring"] diff --git a/cli/app.rs b/cli/app.rs index 2744666c4..69b770202 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -4,12 +4,13 @@ use crate::{ import::ImportFile, Command, CommandParser, }, + config::Config, helper::LimboHelper, input::{get_io, get_writer, DbLocation, OutputMode, Settings}, opcodes_dictionary::OPCODE_DESCRIPTIONS, HISTORY_FILE, }; -use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Row, Table}; +use comfy_table::{Attribute, Cell, CellAlignment, ContentArrangement, Row, Table}; use limbo_core::{Database, LimboError, Statement, StepResult, Value}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; @@ -72,6 +73,7 @@ pub struct Limbo { input_buff: String, opts: Settings, pub rl: Option>, + config: Option, } struct QueryStatistics { @@ -104,8 +106,6 @@ macro_rules! query_internal { }}; } -static COLORS: &[Color] = &[Color::Green, Color::Black, Color::Grey]; - impl Limbo { pub fn new() -> anyhow::Result { let opts = Opts::parse(); @@ -154,13 +154,23 @@ impl Limbo { input_buff: String::new(), opts: Settings::from(opts), rl: None, + config: None, }; app.first_run(sql, quiet)?; Ok(app) } + pub fn with_config(mut self, config: Config) -> Self { + self.config = Some(config); + self + } + pub fn with_readline(mut self, mut rl: Editor) -> Self { - let h = LimboHelper::new(self.conn.clone(), self.io.clone()); + let h = LimboHelper::new( + self.conn.clone(), + self.io.clone(), + self.config.as_ref().map(|c| c.highlight.clone()), + ); rl.set_helper(Some(h)); self.rl = Some(rl); self @@ -727,6 +737,7 @@ impl Limbo { println!("Query interrupted."); return Ok(()); } + let config = self.config.as_ref().unwrap(); let mut table = Table::new(); table .set_content_arrangement(ContentArrangement::Dynamic) @@ -738,7 +749,7 @@ impl Limbo { let name = rows.get_column_name(i); Cell::new(name) .add_attribute(Attribute::Bold) - .fg(Color::White) + .fg(config.table.header_color.into_comfy_table_color()) }) .collect::>(); table.set_header(header); @@ -774,7 +785,9 @@ impl Limbo { row.add_cell( Cell::new(content) .set_alignment(alignment) - .fg(COLORS[idx % COLORS.len()]), + .fg(config.table.column_colors + [idx % config.table.column_colors.len()] + .into_comfy_table_color()), ); } table.add_row(row); diff --git a/cli/config/mod.rs b/cli/config/mod.rs new file mode 100644 index 000000000..b61cc7d86 --- /dev/null +++ b/cli/config/mod.rs @@ -0,0 +1,137 @@ +mod palette; + +use crate::HOME_DIR; +use nu_ansi_term::Color; +use palette::LimboColor; +use schemars::JsonSchema; +use serde::{Deserialize, Deserializer}; +use std::fmt::Debug; +use std::fs::read_to_string; +use std::path::PathBuf; +use std::sync::LazyLock; +use validator::Validate; + +pub static CONFIG_DIR: LazyLock = LazyLock::new(|| HOME_DIR.join(".config/limbo")); + +fn ok_or_default<'de, T, D>(deserializer: D) -> Result +where + T: Deserialize<'de> + Default + Validate + Debug, + D: Deserializer<'de>, +{ + let v: toml::Value = Deserialize::deserialize(deserializer)?; + let x = T::deserialize(v) + .map(|v| { + let validate = v.validate(); + if validate.is_err() { + tracing::error!( + "Invalid value for {}.\n Original config value: {:?}", + validate.unwrap_err(), + v + ); + T::default() + } else { + v + } + }) + .unwrap_or_default(); + Ok(x) +} + +#[derive(Debug, Deserialize, Clone, Default, JsonSchema)] +#[serde(default, deny_unknown_fields)] +pub struct Config { + #[serde(deserialize_with = "ok_or_default")] + pub table: TableConfig, + pub highlight: HighlightConfig, +} + +impl Config { + pub fn from_config_file(path: PathBuf) -> Self { + if let Some(config) = Self::read_config_str(path) { + Self::from_config_str(&config) + } else { + Self::default() + } + } + + pub fn from_config_str(config: &str) -> Self { + toml::from_str(config) + .inspect_err(|err| tracing::error!("{}", err)) + .unwrap_or_default() + } + + fn read_config_str(path: PathBuf) -> Option { + if path.exists() { + tracing::trace!("Trying to read from {:?}", path); + + let result = read_to_string(path); + + if result.is_err() { + tracing::debug!("Error reading file: {:?}", result); + } else { + tracing::trace!("File read successfully"); + }; + + result.ok() + } else { + None + } + } +} + +#[derive(Debug, Deserialize, Clone, JsonSchema, Validate)] +#[serde(default, deny_unknown_fields)] +pub struct TableConfig { + #[serde(default = "TableConfig::default_header_color")] + pub header_color: LimboColor, + #[serde(default = "TableConfig::default_column_colors")] + #[validate(length(min = 1))] + pub column_colors: Vec, +} + +impl Default for TableConfig { + fn default() -> Self { + Self { + header_color: TableConfig::default_header_color(), + column_colors: TableConfig::default_column_colors(), + } + } +} + +impl TableConfig { + // Default colors for Pekka's tastes + fn default_header_color() -> LimboColor { + LimboColor(Color::White) + } + + fn default_column_colors() -> Vec { + vec![ + LimboColor(Color::Green), + LimboColor(Color::Black), + // Comfy Table Color::Grey + LimboColor(Color::Fixed(7)), + ] + } +} + +#[derive(Debug, Deserialize, Clone, JsonSchema)] +#[serde(default, deny_unknown_fields)] +pub struct HighlightConfig { + pub enable: bool, + pub theme: String, + pub prompt: LimboColor, + pub hint: LimboColor, + pub candidate: LimboColor, +} + +impl Default for HighlightConfig { + fn default() -> Self { + Self { + enable: true, + theme: "base16-ocean.dark".to_string(), + prompt: LimboColor(Color::Rgb(34u8, 197u8, 94u8)), + hint: LimboColor(Color::Rgb(107u8, 114u8, 128u8)), + candidate: LimboColor(Color::Green), + } + } +} diff --git a/cli/config/palette.rs b/cli/config/palette.rs new file mode 100644 index 000000000..3b6962270 --- /dev/null +++ b/cli/config/palette.rs @@ -0,0 +1,261 @@ +use core::fmt; +use std::{ + fmt::Display, + ops::{Deref, DerefMut}, +}; + +use nu_ansi_term::Color; +use schemars::JsonSchema; +use serde::{ + de::{self, Visitor}, + Deserialize, Deserializer, Serialize, +}; +use tracing::trace; +use validator::Validate; + +#[derive(Debug, Clone, Serialize)] +pub struct LimboColor(pub Color); + +impl TryFrom<&str> for LimboColor { + type Error = String; + + fn try_from(value: &str) -> Result { + // Parse RGB hex values + trace!("Parsing color_string: {}", value); + + let color = match value.chars().collect::>()[..] { + // #rrggbb hex color + ['#', r1, r2, g1, g2, b1, b2] => { + let r = u8::from_str_radix(&format!("{r1}{r2}"), 16).map_err(|e| e.to_string())?; + let g = u8::from_str_radix(&format!("{g1}{g2}"), 16).map_err(|e| e.to_string())?; + let b = u8::from_str_radix(&format!("{b1}{b2}"), 16).map_err(|e| e.to_string())?; + Some(Color::Rgb(r, g, b)) + } + // #rgb shorthand hex color + ['#', r, g, b] => { + let r = u8::from_str_radix(&format!("{r}{r}"), 16).map_err(|e| e.to_string())?; + let g = u8::from_str_radix(&format!("{g}{g}"), 16).map_err(|e| e.to_string())?; + let b = u8::from_str_radix(&format!("{b}{b}"), 16).map_err(|e| e.to_string())?; + Some(Color::Rgb(r, g, b)) + } + // 0-255 color code + [c1, c2, c3] => { + if let Ok(ansi_color_num) = str::parse::(&format!("{c1}{c2}{c3}")) { + Some(Color::Fixed(ansi_color_num)) + } else { + None + } + } + [c1, c2] => { + if let Ok(ansi_color_num) = str::parse::(&format!("{c1}{c2}")) { + Some(Color::Fixed(ansi_color_num)) + } else { + None + } + } + [c1] => { + if let Ok(ansi_color_num) = str::parse::(&format!("{c1}")) { + Some(Color::Fixed(ansi_color_num)) + } else { + None + } + } + // unknown format + _ => None, + }; + + if let Some(color) = color { + return Ok(LimboColor(color)); + } + + // Check for any predefined color strings + // There are no predefined enums for bright colors, so we use Color::Fixed + let predefined_color = match value.to_lowercase().as_str() { + "black" => Color::Black, + "red" => Color::Red, + "green" => Color::Green, + "yellow" => Color::Yellow, + "blue" => Color::Blue, + "purple" => Color::Purple, + "cyan" => Color::Cyan, + "magenta" => Color::Magenta, + "white" => Color::White, + "bright-black" => Color::DarkGray, // "bright-black" is dark grey + "bright-red" => Color::LightRed, + "bright-green" => Color::LightGreen, + "bright-yellow" => Color::LightYellow, + "bright-blue" => Color::LightBlue, + "bright-cyan" => Color::LightCyan, + "birght-magenta" => Color::LightMagenta, + "bright-white" => Color::LightGray, + "dark-red" => Color::Fixed(1), + "dark-green" => Color::Fixed(2), + "dark-yellow" => Color::Fixed(3), + "dark-blue" => Color::Fixed(4), + "dark-magenta" => Color::Fixed(5), + "dark-cyan" => Color::Fixed(6), + "grey" => Color::Fixed(7), + "dark-grey" => Color::Fixed(8), + _ => return Err(format!("Could not parse color in string: {}", value)), + }; + + trace!("Read predefined color: {}", value); + Ok(LimboColor(predefined_color)) + } +} + +impl Display for LimboColor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let val = match self.0 { + Color::Black => "black".to_string(), + Color::Red => "red".to_string(), + Color::Green => "green".to_string(), + Color::Yellow => "yellow".to_string(), + Color::Blue => "blue".to_string(), + Color::Purple => "purple".to_string(), + Color::Cyan => "cyan".to_string(), + Color::Magenta => "magenta".to_string(), + Color::White => "white".to_string(), + Color::DarkGray => "bright-black".to_string(), // "bright-black" is dark grey + Color::LightRed => "bright-red".to_string(), + Color::LightGreen => "bright-green".to_string(), + Color::LightYellow => "bright-yellow".to_string(), + Color::LightBlue => "bright-blue".to_string(), + Color::LightCyan => "bright-cyan".to_string(), + Color::LightMagenta | Color::LightPurple => "bright-magenta".to_string(), + Color::LightGray => "bright-white".to_string(), + Color::Fixed(1) => "dark-red".to_string(), + Color::Fixed(2) => "dark-green".to_string(), + Color::Fixed(3) => "dark-yellow".to_string(), + Color::Fixed(4) => "dark-blue".to_string(), + Color::Fixed(5) => "dark-magenta".to_string(), + Color::Fixed(6) => "dark-cyan".to_string(), + Color::Fixed(7) => "grey".to_string(), + Color::Fixed(8) => "dark-grey".to_string(), + Color::Rgb(r, g, b) => format!("#{r:x}{g:x}{b:X}"), + Color::Fixed(ansi_color_num) => format!("{ansi_color_num}"), + Color::Default => unreachable!(), + }; + write!(f, "{val}") + } +} + +impl From for LimboColor { + fn from(value: comfy_table::Color) -> Self { + let color = match value { + comfy_table::Color::Rgb { r, g, b } => Color::Rgb(r, g, b), + comfy_table::Color::AnsiValue(ansi_color_num) => Color::Fixed(ansi_color_num), + comfy_table::Color::Black => Color::Black, + comfy_table::Color::Red => Color::Red, + comfy_table::Color::Green => Color::Green, + comfy_table::Color::Yellow => Color::Yellow, + comfy_table::Color::Blue => Color::Blue, + comfy_table::Color::Cyan => Color::Cyan, + comfy_table::Color::Magenta => Color::Magenta, + comfy_table::Color::White => Color::White, + comfy_table::Color::DarkRed => Color::Fixed(1), + comfy_table::Color::DarkGreen => Color::Fixed(2), + comfy_table::Color::DarkYellow => Color::Fixed(3), + comfy_table::Color::DarkBlue => Color::Fixed(4), + comfy_table::Color::DarkMagenta => Color::Fixed(5), + comfy_table::Color::DarkCyan => Color::Fixed(6), + comfy_table::Color::Grey => Color::Fixed(7), + comfy_table::Color::DarkGrey => Color::Fixed(8), + comfy_table::Color::Reset => unreachable!(), // Should never have Reset Color here + }; + LimboColor(color) + } +} + +impl<'de> Deserialize<'de> for LimboColor { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct LimboColorVisitor; + + impl<'de> Visitor<'de> for LimboColorVisitor { + type Value = LimboColor; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct LimboColor") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + LimboColor::try_from(v).map_err(de::Error::custom) + } + } + + deserializer.deserialize_str(LimboColorVisitor) + } +} + +impl JsonSchema for LimboColor { + fn schema_name() -> String { + "LimboColor".into() + } + + fn schema_id() -> std::borrow::Cow<'static, str> { + // Include the module, in case a type with the same name is in another module/crate + std::borrow::Cow::Borrowed(concat!(module_path!(), "::LimboColor")) + } + + fn json_schema(generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + generator.subschema_for::() + } +} + +impl Deref for LimboColor { + type Target = Color; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for LimboColor { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Validate for LimboColor { + fn validate(&self) -> Result<(), validator::ValidationErrors> { + Ok(()) + } +} + +impl LimboColor { + pub fn into_comfy_table_color(&self) -> comfy_table::Color { + match self.0 { + Color::Black => comfy_table::Color::Black, + Color::Red => comfy_table::Color::Red, + Color::Green => comfy_table::Color::Green, + Color::Yellow => comfy_table::Color::Yellow, + Color::Blue => comfy_table::Color::Blue, + Color::Magenta | Color::Purple => comfy_table::Color::Magenta, + Color::Cyan => comfy_table::Color::Cyan, + Color::White | Color::Default => comfy_table::Color::White, + Color::Fixed(1) => comfy_table::Color::DarkRed, + Color::Fixed(2) => comfy_table::Color::DarkGreen, + Color::Fixed(3) => comfy_table::Color::DarkYellow, + Color::Fixed(4) => comfy_table::Color::DarkBlue, + Color::Fixed(5) => comfy_table::Color::DarkMagenta, + Color::Fixed(6) => comfy_table::Color::DarkCyan, + Color::Fixed(7) => comfy_table::Color::Grey, + Color::Fixed(8) => comfy_table::Color::DarkGrey, + Color::DarkGray => comfy_table::Color::AnsiValue(241), + Color::LightRed => comfy_table::Color::AnsiValue(09), + Color::LightGreen => comfy_table::Color::AnsiValue(10), + Color::LightYellow => comfy_table::Color::AnsiValue(11), + Color::LightBlue => comfy_table::Color::AnsiValue(12), + Color::LightMagenta | Color::LightPurple => comfy_table::Color::AnsiValue(13), + Color::LightCyan => comfy_table::Color::AnsiValue(14), + Color::LightGray => comfy_table::Color::AnsiValue(15), + Color::Rgb(r, g, b) => comfy_table::Color::Rgb { r, g, b }, + Color::Fixed(ansi_color_num) => comfy_table::Color::AnsiValue(ansi_color_num), + } + } +} diff --git a/cli/docs/config.md b/cli/docs/config.md new file mode 100644 index 000000000..42e38b385 --- /dev/null +++ b/cli/docs/config.md @@ -0,0 +1,110 @@ +# Config + +Config folder should be located at `$HOME/.config/limbo`. The config file inside should be named `limbo.toml`. Optionally you can have a `themes` folder whithin to store `.tmTheme` files to be discovered by the CLI on startup. + +Describes the Limbo Config file for the CLI\ + +**Note**: Colors can be inputted as +- Rrggbb string -> `"#010101"` +- Rgb string -> `"#A3F"` +- 256 Ansi Color -> `"100"` +- Predefined Color Names: + - `"black"` + - `"red"` + - `"green"` + - `"yellow"` + - `"blue"` + - `"purple"` + - `"cyan"` + - `"magenta"` + - `"white"` + - `"grey"` + - `"bright-black"` + - `"bright-red"` + - `"bright-green"` + - `"bright-yellow"` + - `"bright-blue"` + - `"bright-cyan"` + - `"bright-magenta"` + - `"bright-white"` + - `"dark-red"` + - `"dark-green"` + - `"dark-yellow"` + - `"dark-blue"` + - `"dark-magenta"` + - `"dark-cyan"` + - `"dark-grey"` + +## `table` + +### `column_colors` +**Type**: `List[Color]`\ +*Example*: `["cyan"]` + +### `header_color` +**Type**: `Color`\ +*Example*: `"red"` + +## `highlight` + +### `enable` +**Type**: `bool`\ +*Example*: `true` + +### `theme` +**Type**: `String`\ +*Example*: `"base16-ocean.dark"` + +Preloaded themes: +- `base16-ocean.dark` +- `base16-eighties.dark` +- `base16-mocha.dark` +- `base16-ocean.light` + +You can reference a custom theme in your `themes` folder directly by name from the config file. + +*Example*: + +Folder structure + +``` +limbo +├── limbo.toml +└── themes + └── Amy.tmTheme +``` + +`limbo.toml` + +```toml +[highlight] +theme = "Amy" +``` + +### `prompt` +**Type**: `Color`\ +*Example*: `"green"` + +### `hint` +**Type**: `Color`\ +*Example*: `"grey"` + +### `candidate` +**Type**: `Color`\ +*Example*: `"yellow"` + +## Example `limbo.toml` + +```toml +[table] +column_colors = ["cyan", "black", "#010101"] +header_color = "red" + +[highlight] +enable = true +prompt = "bright-blue" +theme = "base16-ocean.light" +hint = "123" +candidate = "dark-yellow" +``` + diff --git a/cli/helper.rs b/cli/helper.rs index 70194234d..f8e606a89 100644 --- a/cli/helper.rs +++ b/cli/helper.rs @@ -11,8 +11,14 @@ use std::marker::PhantomData; use std::rc::Rc; use std::sync::Arc; use std::{ffi::OsString, path::PathBuf, str::FromStr as _}; +use syntect::dumps::from_uncompressed_data; +use syntect::easy::HighlightLines; +use syntect::highlighting::ThemeSet; +use syntect::parsing::{Scope, SyntaxSet}; +use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings}; use crate::commands::CommandParser; +use crate::config::{HighlightConfig, CONFIG_DIR}; macro_rules! try_result { ($expr:expr, $err:expr) => { @@ -27,14 +33,37 @@ macro_rules! try_result { pub struct LimboHelper { #[rustyline(Completer)] completer: SqlCompleter, + syntax_set: SyntaxSet, + theme_set: ThemeSet, + syntax_config: HighlightConfig, #[rustyline(Hinter)] hinter: HistoryHinter, } impl LimboHelper { - pub fn new(conn: Rc, io: Arc) -> Self { + pub fn new( + conn: Rc, + io: Arc, + syntax_config: Option, + ) -> Self { + // Load only predefined syntax + let ps = from_uncompressed_data(include_bytes!(concat!( + env!("OUT_DIR"), + "/SQL_syntax_set_dump.packdump" + ))) + .unwrap(); + let mut ts = ThemeSet::load_defaults(); + let theme_dir = CONFIG_DIR.join("themes"); + if theme_dir.exists() { + if let Err(err) = ts.add_from_folder(theme_dir) { + tracing::error!("{err}"); + } + } LimboHelper { completer: SqlCompleter::new(conn, io), + syntax_set: ps, + theme_set: ts, + syntax_config: syntax_config.unwrap_or_default(), hinter: HistoryHinter::new(), } } @@ -43,9 +72,37 @@ impl LimboHelper { impl Highlighter for LimboHelper { fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> { let _ = pos; - let style = Style::new().fg(Color::White); // Standard shell text color - let styled_str = style.paint(line); - std::borrow::Cow::Owned(styled_str.to_string()) + if self.syntax_config.enable { + // TODO use lifetimes to store highlight lines + let syntax = self + .syntax_set + .find_syntax_by_scope(Scope::new("source.sql").unwrap()) + .unwrap(); + let theme = self + .theme_set + .themes + .get(&self.syntax_config.theme) + .unwrap_or(&self.theme_set.themes["base16-ocean.dark"]); + let mut h = HighlightLines::new(syntax, theme); + let ranges = { + let mut ret_ranges = Vec::new(); + for new_line in LinesWithEndings::from(line) { + let ranges: Vec<(syntect::highlighting::Style, &str)> = + h.highlight_line(new_line, &self.syntax_set).unwrap(); + ret_ranges.extend(ranges); + } + ret_ranges + }; + let mut ret_line = as_24_bit_terminal_escaped(&ranges[..], false); + // Push this escape sequence to reset terminal color modes at the end of the string + ret_line.push_str("\x1b[0m"); + std::borrow::Cow::Owned(ret_line) + } else { + // Appease Pekka in syntax highlighting + let style = Style::new().fg(Color::White); // Standard shell text color + let styled_str = style.paint(line); + std::borrow::Cow::Owned(styled_str.to_string()) + } } fn highlight_prompt<'b, 's: 'b, 'p: 'b>( @@ -55,13 +112,13 @@ impl Highlighter for LimboHelper { ) -> std::borrow::Cow<'b, str> { let _ = default; // Dark emerald green for prompt - let style = Style::new().bold().fg(Color::Rgb(34u8, 197u8, 94u8)); + let style = Style::new().bold().fg(self.syntax_config.prompt.0); let styled_str = style.paint(prompt); std::borrow::Cow::Owned(styled_str.to_string()) } fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> { - let style = Style::new().bold().fg(Color::Rgb(107u8, 114u8, 128u8)); // Brighter dark grey for hints + let style = Style::new().bold().fg(self.syntax_config.hint.0); // Brighter dark grey for hints let styled_str = style.paint(hint); std::borrow::Cow::Owned(styled_str.to_string()) } @@ -72,7 +129,7 @@ impl Highlighter for LimboHelper { completion: rustyline::CompletionType, ) -> std::borrow::Cow<'c, str> { let _ = completion; - let style = Style::new().fg(Color::Green); + let style = Style::new().fg(self.syntax_config.candidate.0); let styled_str = style.paint(candidate); std::borrow::Cow::Owned(styled_str.to_string()) } diff --git a/cli/main.rs b/cli/main.rs index 82eb64953..843d6580e 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -1,10 +1,12 @@ #![allow(clippy::arc_with_non_send_sync)] mod app; mod commands; +mod config; mod helper; mod input; mod opcodes_dictionary; +use config::CONFIG_DIR; use rustyline::{error::ReadlineError, Config, Editor}; use std::{ path::PathBuf, @@ -32,6 +34,12 @@ fn main() -> anyhow::Result<()> { if HISTORY_FILE.exists() { rl.load_history(HISTORY_FILE.as_path())?; } + let config_file = CONFIG_DIR.join("limbo.toml"); + + let config = config::Config::from_config_file(config_file); + tracing::info!("Configuration: {:?}", config); + app = app.with_config(config); + app = app.with_readline(rl); } else { tracing::debug!("not in tty");