pub mod appearance;
pub mod highlight;
pub mod loader;
use std::collections::BTreeMap;
use thiserror::Error;
pub use crate::config::appearance::{Appearance, CALCLI_THEME};
pub use crate::config::highlight::HighlightColors;
pub use crate::config::loader::load_config;
use crate::domain::format::{AngleMode, FormatSettings, Notation};
use crate::keymap::Keymap;
use crate::theme::{
Color, ColorOverrides, Palette, ThemeColors, ThemeRegistry,
};
const DEFAULT_DECIMALS: usize = 3;
const DEFAULT_MAX_HISTORY: usize = 500;
const CALCLI_COLORS: ThemeColors = ThemeColors {
accent: Color::hex("#82e38e"),
foreground: Color::hex("#e5e5e5"),
background: Color::Default,
header: Color::hex("#0e0c12"),
footer: Color::hex("#0e0c12"),
panel: Color::hex("#1b1926"),
surface: Color::hex("#16141d"),
border: Color::hex("#3e3e3e"),
border_focus: Color::hex("#4a7c60"),
success: Color::hex("#a3c995"),
warning: Color::hex("#ded483"),
error: Color::hex("#e06c6c"),
info: Color::hex("#7fb3d4"),
};
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
pub notation: Notation,
pub decimals: usize,
pub angle_mode: AngleMode,
pub decimal_separator: char,
pub thousands_separator: String,
pub trim_trailing_zeros: bool,
pub max_history: usize,
pub restore_last_settings: bool,
pub live_feedback: bool,
pub history_spacing: usize,
pub history_separator: bool,
pub input_max_lines: usize,
pub appearance: Appearance,
pub themes: Vec<(String, ThemeColors)>,
pub highlight: HighlightColors,
pub keys: BTreeMap<String, Vec<String>>,
pub confirm_delete: bool,
pub confirm_quit: bool,
}
impl Default for Config {
fn default() -> Self {
Config {
notation: Notation::default(),
decimals: DEFAULT_DECIMALS,
angle_mode: AngleMode::default(),
decimal_separator: '.',
thousands_separator: " ".to_string(),
trim_trailing_zeros: true,
max_history: DEFAULT_MAX_HISTORY,
restore_last_settings: true,
live_feedback: true,
history_spacing: 1,
history_separator: true,
input_max_lines: 5,
appearance: Appearance::default(),
themes: Vec::new(),
highlight: HighlightColors::default(),
keys: BTreeMap::new(),
confirm_delete: true,
confirm_quit: false,
}
}
}
impl Config {
pub fn format_settings(&self) -> FormatSettings {
FormatSettings {
notation: self.notation,
decimals: self.decimals,
angle_mode: self.angle_mode,
decimal_separator: self.decimal_separator,
thousands_separator: self.thousands_separator.clone(),
trim_trailing_zeros: self.trim_trailing_zeros,
}
}
pub fn keymap(&self) -> Keymap {
Keymap::from_overrides(&self.keys)
}
pub fn color_overrides(&self) -> ColorOverrides<'_> {
ColorOverrides::from_lookup(|name| {
self.appearance.colors.get(name).map_or("", String::as_str)
})
}
pub fn theme_registry(&self) -> ThemeRegistry {
ThemeRegistry::builtin()
.with_custom([(CALCLI_THEME.to_string(), CALCLI_COLORS)])
.with_custom(self.themes.iter().cloned())
}
pub fn palette(&self) -> Palette {
let base = self.theme_registry().resolve(&self.appearance.theme);
Palette::resolve(base, &self.color_overrides())
}
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("cannot read config file {path}")]
Read {
path: String,
#[source]
source: std::io::Error,
},
#[error("invalid config file {path}")]
Parse {
path: String,
#[source]
source: toml::de::Error,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_calcli_theme_is_registered_and_selected_by_default() {
let config = Config::default();
assert_eq!(config.appearance.theme, CALCLI_THEME);
assert!(config.theme_registry().contains(CALCLI_THEME));
}
#[test]
fn the_palette_keeps_the_calcli_accent_and_panel_bands() {
let palette = Config::default().palette();
assert_eq!(palette.accent, Color::hex("#82e38e"));
assert_eq!(palette.header, Color::hex("#0e0c12"));
assert_eq!(palette.footer, Color::hex("#0e0c12"));
assert_eq!(palette.surface, Color::hex("#16141d"));
}
#[test]
fn the_content_surface_is_lighter_than_the_bands() {
let palette = Config::default().palette();
assert!(palette.surface.luminance() > palette.header.luminance());
}
#[test]
fn the_outer_background_is_the_terminal_default() {
assert_eq!(Config::default().palette().background, Color::Default);
}
#[test]
fn the_input_field_recedes_at_rest_and_lifts_when_focused() {
let palette = Config::default().palette();
let surface = palette.surface.luminance();
assert!(palette.input_bg.luminance() < surface, "resting fill");
assert!(palette.input_bg.luminance() > palette.header.luminance());
assert!(
palette.input_bg_active.luminance() > surface,
"focused fill"
);
assert!(
palette.input_bg_active.luminance() < palette.border.luminance()
);
}
#[test]
fn the_selected_row_is_marked_without_shouting() {
let palette = Config::default().palette();
assert!(palette.selection.luminance() > palette.surface.luminance());
assert!(
palette.selection.luminance() < palette.border_focus.luminance()
);
assert!(palette.selection.luminance() < palette.foreground.luminance());
let (red, green, blue) =
palette.selection.rgb().expect("a concrete colour");
assert!(green > red && green > blue, "the accent tint survives");
}
#[test]
fn a_selection_override_still_wins() {
let config = loader::config_from_str(
"[appearance.colors]\nselection = \"#010203\"\n",
)
.expect("parses");
assert_eq!(config.palette().selection, Color::Rgb(1, 2, 3));
}
#[test]
fn an_input_fill_override_still_wins() {
let config = loader::config_from_str(
"[appearance.colors]\ninput_bg = \"#010203\"\n",
)
.expect("parses");
assert_eq!(config.palette().input_bg, Color::Rgb(1, 2, 3));
assert_eq!(
config.palette().input_bg_active,
Color::hex(appearance::DEFAULT_INPUT_BG_ACTIVE),
);
}
#[test]
fn the_block_cursor_stays_red_rather_than_following_the_accent() {
let palette = Config::default().palette();
assert_eq!(palette.cursor, Color::hex("#d65c5c"));
assert_ne!(palette.cursor, palette.accent);
}
#[test]
fn a_user_theme_replaces_the_built_in_calcli_theme() {
let custom =
ThemeColors::from_accent(Color::Rgb(1, 2, 3), Color::Rgb(4, 5, 6));
let config = Config {
themes: vec![(CALCLI_THEME.to_string(), custom)],
..Config::default()
};
assert_eq!(config.palette().accent, Color::Rgb(1, 2, 3));
}
#[test]
fn confirm_quit_defaults_off_and_confirm_delete_defaults_on() {
let config = Config::default();
assert!(!config.confirm_quit);
assert!(config.confirm_delete);
}
}