1use color_eyre::eyre::eyre;
2use color_eyre::Result;
3use ratatui::style::Color;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use supports_color::Stream;
8
9#[derive(Clone)]
11pub struct ConfigManager {
12 pub(crate) config_dir: PathBuf,
13}
14
15impl ConfigManager {
16 pub fn with_dir(config_dir: PathBuf) -> Self {
18 Self { config_dir }
19 }
20
21 pub fn new(app_name: &str) -> Result<Self> {
23 let config_dir = dirs::config_dir()
24 .ok_or_else(|| eyre!("Could not determine config directory"))?
25 .join(app_name);
26
27 Ok(Self { config_dir })
28 }
29
30 pub fn config_dir(&self) -> &Path {
32 &self.config_dir
33 }
34
35 pub fn config_path(&self, path: &str) -> PathBuf {
37 self.config_dir.join(path)
38 }
39
40 pub fn ensure_config_dir(&self) -> Result<()> {
42 if !self.config_dir.exists() {
43 std::fs::create_dir_all(&self.config_dir)?;
44 }
45 Ok(())
46 }
47
48 pub fn ensure_subdir(&self, subdir: &str) -> Result<PathBuf> {
50 let subdir_path = self.config_dir.join(subdir);
51 if !subdir_path.exists() {
52 std::fs::create_dir_all(&subdir_path)?;
53 }
54 Ok(subdir_path)
55 }
56
57 pub fn generate_default_config(&self) -> String {
60 let config = AppConfig::default();
62 let toml_str = toml::to_string_pretty(&config)
63 .unwrap_or_else(|e| panic!("Failed to serialize default config: {}", e));
64
65 let comments = Self::collect_all_comments();
67
68 Self::comment_all_fields(toml_str, comments)
70 }
71
72 fn collect_all_comments() -> std::collections::HashMap<String, String> {
74 let mut comments = std::collections::HashMap::new();
75
76 for (field, comment) in APP_COMMENTS {
78 comments.insert(field.to_string(), comment.to_string());
79 }
80
81 for (field, comment) in CLOUD_COMMENTS {
83 comments.insert(format!("cloud.{}", field), comment.to_string());
84 }
85
86 for (field, comment) in FILE_LOADING_COMMENTS {
88 comments.insert(format!("file_loading.{}", field), comment.to_string());
89 }
90
91 for (field, comment) in DISPLAY_COMMENTS {
93 comments.insert(format!("display.{}", field), comment.to_string());
94 }
95
96 for (field, comment) in PERFORMANCE_COMMENTS {
98 comments.insert(format!("performance.{}", field), comment.to_string());
99 }
100
101 for (field, comment) in CHART_COMMENTS {
103 comments.insert(format!("chart.{}", field), comment.to_string());
104 }
105
106 for (field, comment) in THEME_COMMENTS {
108 comments.insert(format!("theme.{}", field), comment.to_string());
109 }
110
111 for (field, comment) in COLOR_COMMENTS {
113 comments.insert(format!("theme.colors.{}", field), comment.to_string());
114 }
115
116 for (field, comment) in CONTROLS_COMMENTS {
118 comments.insert(format!("ui.controls.{}", field), comment.to_string());
119 }
120
121 for (field, comment) in QUERY_COMMENTS {
123 comments.insert(format!("query.{}", field), comment.to_string());
124 }
125
126 for (field, comment) in TEMPLATE_COMMENTS {
128 comments.insert(format!("templates.{}", field), comment.to_string());
129 }
130
131 for (field, comment) in DEBUG_COMMENTS {
133 comments.insert(format!("debug.{}", field), comment.to_string());
134 }
135
136 comments
137 }
138
139 fn comment_all_fields(
142 toml: String,
143 comments: std::collections::HashMap<String, String>,
144 ) -> String {
145 let mut result = String::new();
146 result.push_str("# datui configuration file\n");
147 result
148 .push_str("# This file uses TOML format. See https://toml.io/ for syntax reference.\n");
149 result.push('\n');
150
151 let lines: Vec<&str> = toml.lines().collect();
152 let mut i = 0;
153 let mut current_section = String::new();
154 let mut seen_fields: std::collections::HashSet<String> = std::collections::HashSet::new();
155
156 while i < lines.len() {
158 let line = lines[i];
159
160 if let Some(section) = Self::extract_section_name(line) {
162 current_section = section.clone();
163
164 if let Some(header) = SECTION_HEADERS.iter().find(|(s, _)| s == §ion) {
166 result.push_str(header.1);
167 result.push('\n');
168 }
169
170 result.push_str("# ");
172 result.push_str(line);
173 result.push('\n');
174 i += 1;
175 continue;
176 }
177
178 if let Some(field_path) = Self::extract_field_path_simple(line, ¤t_section) {
180 seen_fields.insert(field_path.clone());
181
182 if let Some(comment) = comments.get(&field_path) {
184 for comment_line in comment.lines() {
185 result.push_str("# ");
186 result.push_str(comment_line);
187 result.push('\n');
188 }
189 }
190
191 result.push_str("# ");
193 result.push_str(line);
194 result.push('\n');
195 } else {
196 result.push_str(line);
198 result.push('\n');
199 }
200
201 i += 1;
202 }
203
204 result = Self::add_missing_option_fields(result, &comments, &seen_fields);
206
207 result
208 }
209
210 fn add_missing_option_fields(
212 mut result: String,
213 comments: &std::collections::HashMap<String, String>,
214 seen_fields: &std::collections::HashSet<String>,
215 ) -> String {
216 let option_fields = [
218 "cloud.s3_endpoint_url",
219 "cloud.s3_access_key_id",
220 "cloud.s3_secret_access_key",
221 "cloud.s3_region",
222 "file_loading.delimiter",
223 "file_loading.has_header",
224 "file_loading.skip_lines",
225 "file_loading.skip_rows",
226 "file_loading.single_spine_schema",
227 "chart.row_limit",
228 "ui.controls.custom_controls",
229 ];
230
231 let mut missing_by_section: std::collections::HashMap<String, Vec<&str>> =
233 std::collections::HashMap::new();
234
235 for field_path in &option_fields {
236 if !seen_fields.contains(*field_path) && comments.contains_key(*field_path) {
237 if let Some(dot_pos) = field_path.find('.') {
238 let section = &field_path[..dot_pos];
239 missing_by_section
240 .entry(section.to_string())
241 .or_default()
242 .push(field_path);
243 }
244 }
245 }
246
247 for (section, fields) in &missing_by_section {
249 let section_header = format!("[{}]", section);
250 if let Some(section_pos) = result.find(§ion_header) {
251 let after_header_start = section_pos + section_header.len();
253 let after_header = &result[after_header_start..];
254
255 let newline_pos = after_header.find('\n').unwrap_or(0);
257 let insert_pos = after_header_start + newline_pos + 1;
258
259 let mut new_content = String::new();
261 for field_path in fields {
262 if let Some(comment) = comments.get(*field_path) {
263 for comment_line in comment.lines() {
264 new_content.push_str("# ");
265 new_content.push_str(comment_line);
266 new_content.push('\n');
267 }
268 }
269 let field_name = field_path.rsplit('.').next().unwrap_or(field_path);
270 new_content.push_str(&format!("# {} = null\n", field_name));
271 new_content.push('\n');
272 }
273
274 result.insert_str(insert_pos, &new_content);
275 }
276 }
277
278 result
279 }
280
281 fn extract_section_name(line: &str) -> Option<String> {
283 let trimmed = line.trim();
284 if trimmed.starts_with('[') && trimmed.ends_with(']') {
285 Some(trimmed[1..trimmed.len() - 1].to_string())
286 } else {
287 None
288 }
289 }
290
291 fn extract_field_path_simple(line: &str, current_section: &str) -> Option<String> {
293 let trimmed = line.trim();
294 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('[') {
295 return None;
296 }
297
298 if let Some(eq_pos) = trimmed.find('=') {
300 let field_name = trimmed[..eq_pos].trim();
301 if current_section.is_empty() {
302 Some(field_name.to_string())
303 } else {
304 Some(format!("{}.{}", current_section, field_name))
305 }
306 } else {
307 None
308 }
309 }
310
311 pub fn write_default_config(&self, force: bool) -> Result<PathBuf> {
313 let config_path = self.config_path("config.toml");
314
315 if config_path.exists() && !force {
316 return Err(eyre!(
317 "Config file already exists at {}. Use --force to overwrite.",
318 config_path.display()
319 ));
320 }
321
322 self.ensure_config_dir()?;
324
325 let template = self.generate_default_config();
327 std::fs::write(&config_path, template)?;
328
329 Ok(config_path)
330 }
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335#[serde(default)]
336pub struct AppConfig {
337 pub version: String,
339 pub cloud: CloudConfig,
340 pub file_loading: FileLoadingConfig,
341 pub display: DisplayConfig,
342 pub performance: PerformanceConfig,
343 pub chart: ChartConfig,
344 pub theme: ThemeConfig,
345 pub ui: UiConfig,
346 pub query: QueryConfig,
347 pub templates: TemplateConfig,
348 pub debug: DebugConfig,
349}
350
351const APP_COMMENTS: &[(&str, &str)] = &[(
353 "version",
354 "Configuration format version (for future compatibility)",
355)];
356
357const SECTION_HEADERS: &[(&str, &str)] = &[
359 (
360 "cloud",
361 "# ============================================================================\n# Cloud / Object Storage (S3, MinIO)\n# ============================================================================\n# Optional overrides for s3:// URLs. Leave unset to use AWS defaults (env, ~/.aws/).\n# Set endpoint_url to use MinIO or other S3-compatible backends.",
362 ),
363 (
364 "file_loading",
365 "# ============================================================================\n# File Loading Defaults\n# ============================================================================",
366 ),
367 (
368 "display",
369 "# ============================================================================\n# Display Settings\n# ============================================================================",
370 ),
371 (
372 "performance",
373 "# ============================================================================\n# Performance Settings\n# ============================================================================",
374 ),
375 (
376 "chart",
377 "# ============================================================================\n# Chart View\n# ============================================================================",
378 ),
379 (
380 "theme",
381 "# ============================================================================\n# Color Theme\n# ============================================================================",
382 ),
383 (
384 "theme.colors",
385 "# Color definitions\n# Supported formats:\n# - Named colors: \"red\", \"blue\", \"bright_red\", \"dark_gray\", etc. (case-insensitive)\n# - Hex colors: \"#ff0000\" or \"#FF0000\" (case-insensitive)\n# - Indexed colors: \"indexed(0-255)\" for specific xterm 256-color palette entries\n# Colors automatically adapt to your terminal's capabilities",
386 ),
387 (
388 "ui",
389 "# ============================================================================\n# UI Layout\n# ============================================================================",
390 ),
391 ("ui.controls", "# Control bar settings"),
392 (
393 "query",
394 "# ============================================================================\n# Query System\n# ============================================================================",
395 ),
396 (
397 "templates",
398 "# ============================================================================\n# Template Settings\n# ============================================================================",
399 ),
400 (
401 "debug",
402 "# ============================================================================\n# Debug Settings\n# ============================================================================",
403 ),
404];
405
406#[derive(Debug, Clone, Serialize, Deserialize, Default)]
407#[serde(default)]
408pub struct CloudConfig {
409 pub s3_endpoint_url: Option<String>,
411 pub s3_access_key_id: Option<String>,
413 pub s3_secret_access_key: Option<String>,
415 pub s3_region: Option<String>,
417}
418
419const CLOUD_COMMENTS: &[(&str, &str)] = &[
420 (
421 "s3_endpoint_url",
422 "Custom endpoint for S3-compatible storage (MinIO, etc.). Example: \"http://localhost:9000\". Unset = AWS.",
423 ),
424 (
425 "s3_access_key_id",
426 "Access key when using custom endpoint (or set AWS_ACCESS_KEY_ID).",
427 ),
428 (
429 "s3_secret_access_key",
430 "Secret key when using custom endpoint (or set AWS_SECRET_ACCESS_KEY).",
431 ),
432 (
433 "s3_region",
434 "Region (e.g. us-east-1). Required for custom endpoints; MinIO often uses us-east-1.",
435 ),
436];
437
438impl CloudConfig {
439 pub fn merge(&mut self, other: Self) {
440 if other.s3_endpoint_url.is_some() {
441 self.s3_endpoint_url = other.s3_endpoint_url;
442 }
443 if other.s3_access_key_id.is_some() {
444 self.s3_access_key_id = other.s3_access_key_id;
445 }
446 if other.s3_secret_access_key.is_some() {
447 self.s3_secret_access_key = other.s3_secret_access_key;
448 }
449 if other.s3_region.is_some() {
450 self.s3_region = other.s3_region;
451 }
452 }
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, Default)]
456#[serde(default)]
457pub struct FileLoadingConfig {
458 pub delimiter: Option<u8>,
459 pub has_header: Option<bool>,
460 pub skip_lines: Option<usize>,
461 pub skip_rows: Option<usize>,
462 pub parse_dates: Option<bool>,
464 pub decompress_in_memory: Option<bool>,
466 pub temp_dir: Option<String>,
468 pub single_spine_schema: Option<bool>,
470}
471
472const FILE_LOADING_COMMENTS: &[(&str, &str)] = &[
475 (
476 "delimiter",
477 "Default delimiter for CSV files (as ASCII value, e.g., 44 for comma)\nIf not specified, auto-detection is used",
478 ),
479 (
480 "has_header",
481 "Whether files have headers by default\nnull = auto-detect, true = has header, false = no header",
482 ),
483 ("skip_lines", "Number of lines to skip at the start of files"),
484 ("skip_rows", "Number of rows to skip when reading files"),
485 (
486 "parse_dates",
487 "When true (default), CSV reader tries to parse string columns as dates (e.g. YYYY-MM-DD, ISO datetime)",
488 ),
489 (
490 "decompress_in_memory",
491 "When true, decompress compressed CSV into memory (eager). When false (default), decompress to a temp file and use lazy scan",
492 ),
493 (
494 "temp_dir",
495 "Directory for decompression temp files. null = system default (e.g. TMPDIR)",
496 ),
497 (
498 "single_spine_schema",
499 "When true (default), infer Hive/partitioned Parquet schema from one file for faster load. When false, use full schema scan (Polars collect_schema).",
500 ),
501];
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
504#[serde(default)]
505pub struct DisplayConfig {
506 pub pages_lookahead: usize,
507 pub pages_lookback: usize,
508 pub max_buffered_rows: usize,
510 pub max_buffered_mb: usize,
512 pub row_numbers: bool,
513 pub row_start_index: usize,
514 pub table_cell_padding: usize,
515 pub column_colors: bool,
517 #[serde(default)]
519 pub sidebar_width: Option<u16>,
520}
521
522const DISPLAY_COMMENTS: &[(&str, &str)] = &[
524 (
525 "pages_lookahead",
526 "Number of pages to buffer ahead of visible area\nLarger values = smoother scrolling but more memory",
527 ),
528 (
529 "pages_lookback",
530 "Number of pages to buffer behind visible area\nLarger values = smoother scrolling but more memory",
531 ),
532 (
533 "max_buffered_rows",
534 "Maximum rows in scroll buffer (0 = no limit)\nPrevents unbounded memory use when scrolling",
535 ),
536 (
537 "max_buffered_mb",
538 "Maximum buffer size in MB (0 = no limit)\nUses estimated memory; helps with very wide tables",
539 ),
540 ("row_numbers", "Display row numbers on the left side of the table"),
541 ("row_start_index", "Starting index for row numbers (0 or 1)"),
542 (
543 "table_cell_padding",
544 "Number of spaces between columns in the main data table (>= 0)\nDefault 2",
545 ),
546 (
547 "column_colors",
548 "Colorize main table cells by column type (string, int, float, bool, date/datetime)\nSet to false to use default text color for all cells",
549 ),
550 (
551 "sidebar_width",
552 "Optional: fixed width in characters for all sidebars (Info, Sort & Filter, Templates, Pivot & Melt). When unset, each sidebar uses its default width. Example: sidebar_width = 70",
553 ),
554];
555
556#[derive(Debug, Clone, Serialize, Deserialize)]
557#[serde(default)]
558pub struct PerformanceConfig {
559 pub sampling_threshold: Option<usize>,
561 pub event_poll_interval_ms: u64,
562 pub polars_streaming: bool,
564}
565
566const PERFORMANCE_COMMENTS: &[(&str, &str)] = &[
568 (
569 "sampling_threshold",
570 "Optional: when set, datasets with >= this many rows are sampled for analysis (faster, less memory).\nWhen unset or omitted, full dataset is used. Example: sampling_threshold = 10000",
571 ),
572 (
573 "event_poll_interval_ms",
574 "Event polling interval in milliseconds\nLower values = more responsive but higher CPU usage",
575 ),
576 (
577 "polars_streaming",
578 "Use Polars streaming engine for LazyFrame collect when available (default: true). Reduces memory and can improve performance on large or partitioned data.",
579 ),
580];
581
582pub const DEFAULT_CHART_ROW_LIMIT: usize = 10_000;
584pub const MAX_CHART_ROW_LIMIT: usize = u32::MAX as usize;
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
588#[serde(default)]
589pub struct ChartConfig {
590 pub row_limit: Option<usize>,
592}
593
594const CHART_COMMENTS: &[(&str, &str)] = &[
596 (
597 "row_limit",
598 "Maximum rows used when building charts (display and export).\nSet to null for unlimited (uses full dataset). Set to a number (e.g. 10000) to cap. Can also be changed in chart view (Limit Rows). Example: row_limit = 10000",
599 ),
600];
601
602impl Default for ChartConfig {
603 fn default() -> Self {
604 Self {
605 row_limit: Some(DEFAULT_CHART_ROW_LIMIT),
606 }
607 }
608}
609
610impl ChartConfig {
611 pub fn merge(&mut self, other: Self) {
612 if other.row_limit.is_some() {
613 self.row_limit = other.row_limit;
614 }
615 }
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize, Default)]
619#[serde(default)]
620pub struct ThemeConfig {
621 pub colors: ColorConfig,
622}
623
624const THEME_COMMENTS: &[(&str, &str)] = &[];
626
627fn default_row_numbers_color() -> String {
628 "dark_gray".to_string()
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize)]
632#[serde(default)]
633pub struct ColorConfig {
681 pub keybind_hints: String,
682 pub keybind_labels: String,
683 pub throbber: String,
684 pub primary_chart_series_color: String,
685 pub secondary_chart_series_color: String,
686 pub success: String,
687 pub error: String,
688 pub warning: String,
689 pub dimmed: String,
690 pub background: String,
691 pub surface: String,
692 pub controls_bg: String,
693 pub text_primary: String,
694 pub text_secondary: String,
695 pub text_inverse: String,
696 pub table_header: String,
697 pub table_header_bg: String,
698 #[serde(default = "default_row_numbers_color")]
700 pub row_numbers: String,
701 pub column_separator: String,
702 pub table_selected: String,
703 pub sidebar_border: String,
704 pub modal_border_active: String,
705 pub modal_border_error: String,
706 pub distribution_normal: String,
707 pub distribution_skewed: String,
708 pub distribution_other: String,
709 pub outlier_marker: String,
710 pub cursor_focused: String,
711 pub cursor_dimmed: String,
712 pub alternate_row_color: String,
714 pub str_col: String,
716 pub int_col: String,
717 pub float_col: String,
718 pub bool_col: String,
719 pub temporal_col: String,
720 pub chart_series_color_1: String,
722 pub chart_series_color_2: String,
723 pub chart_series_color_3: String,
724 pub chart_series_color_4: String,
725 pub chart_series_color_5: String,
726 pub chart_series_color_6: String,
727 pub chart_series_color_7: String,
728}
729
730const COLOR_COMMENTS: &[(&str, &str)] = &[
732 (
733 "keybind_hints",
734 "Keybind hints (modals, breadcrumb, correlation matrix)",
735 ),
736 ("keybind_labels", "Action labels in controls bar"),
737 ("throbber", "Busy indicator (spinner) in control bar"),
738 (
739 "primary_chart_series_color",
740 "Chart data (histogram bars, Q-Q plot data points)",
741 ),
742 (
743 "secondary_chart_series_color",
744 "Chart theory (histogram overlays, Q-Q plot reference line)",
745 ),
746 ("success", "Success indicators, normal distributions"),
747 ("error", "Error messages, outliers"),
748 ("warning", "Warnings, skewed distributions"),
749 ("dimmed", "Dimmed elements, axis lines"),
750 ("background", "Main background"),
751 ("surface", "Modal/surface backgrounds"),
752 ("controls_bg", "Controls bar background"),
753 ("text_primary", "Primary text"),
754 ("text_secondary", "Secondary text"),
755 ("text_inverse", "Text on light backgrounds"),
756 ("table_header", "Table column header text"),
757 ("table_header_bg", "Table column header background"),
758 ("row_numbers", "Row numbers column text; use \"default\" for terminal default"),
759 ("column_separator", "Vertical line between columns"),
760 ("table_selected", "Selected row style"),
761 ("sidebar_border", "Sidebar borders"),
762 ("modal_border_active", "Active modal elements"),
763 ("modal_border_error", "Error modal borders"),
764 ("distribution_normal", "Normal distribution indicator"),
765 ("distribution_skewed", "Skewed distribution indicator"),
766 ("distribution_other", "Other distribution types"),
767 ("outlier_marker", "Outlier indicators"),
768 (
769 "cursor_focused",
770 "Cursor color when text input is focused\nText under cursor uses reverse of this color",
771 ),
772 (
773 "cursor_dimmed",
774 "Cursor color when text input is unfocused (currently unused - unfocused inputs hide cursor)",
775 ),
776 (
777 "alternate_row_color",
778 "Background color for every other row in the main data table\nSet to \"default\" to disable alternate row coloring",
779 ),
780 ("str_col", "Main table: string column text color"),
781 ("int_col", "Main table: integer column text color"),
782 ("float_col", "Main table: float column text color"),
783 ("bool_col", "Main table: boolean column text color"),
784 ("temporal_col", "Main table: date/datetime/time column text color"),
785 ("chart_series_color_1", "Chart view: first series color"),
786 ("chart_series_color_2", "Chart view: second series color"),
787 ("chart_series_color_3", "Chart view: third series color"),
788 ("chart_series_color_4", "Chart view: fourth series color"),
789 ("chart_series_color_5", "Chart view: fifth series color"),
790 ("chart_series_color_6", "Chart view: sixth series color"),
791 ("chart_series_color_7", "Chart view: seventh series color"),
792];
793
794#[derive(Debug, Clone, Serialize, Deserialize, Default)]
795#[serde(default)]
796pub struct UiConfig {
797 pub controls: ControlsConfig,
798}
799
800#[derive(Debug, Clone, Serialize, Deserialize)]
801#[serde(default)]
802pub struct ControlsConfig {
803 pub custom_controls: Option<Vec<(String, String)>>,
804 pub row_count_width: usize,
805}
806
807const CONTROLS_COMMENTS: &[(&str, &str)] = &[
809 (
810 "custom_controls",
811 "Custom control keybindings (optional)\nFormat: [[\"key\", \"label\"], [\"key\", \"label\"], ...]\nIf not specified, uses default controls",
812 ),
813 ("row_count_width", "Row count display width in characters"),
814];
815
816#[derive(Debug, Clone, Serialize, Deserialize)]
817#[serde(default)]
818pub struct QueryConfig {
819 pub history_limit: usize,
820 pub enable_history: bool,
821}
822
823const QUERY_COMMENTS: &[(&str, &str)] = &[
825 (
826 "history_limit",
827 "Maximum number of queries to keep in history",
828 ),
829 ("enable_history", "Enable query history caching"),
830];
831
832#[derive(Debug, Clone, Serialize, Deserialize, Default)]
833#[serde(default)]
834pub struct TemplateConfig {
835 pub auto_apply: bool,
836}
837
838const TEMPLATE_COMMENTS: &[(&str, &str)] = &[(
840 "auto_apply",
841 "Auto-apply most relevant template on file open",
842)];
843
844#[derive(Debug, Clone, Serialize, Deserialize)]
845#[serde(default)]
846pub struct DebugConfig {
847 pub enabled: bool,
848 pub show_performance: bool,
849 pub show_query: bool,
850 pub show_transformations: bool,
851}
852
853const DEBUG_COMMENTS: &[(&str, &str)] = &[
855 ("enabled", "Enable debug overlay by default"),
856 (
857 "show_performance",
858 "Show performance metrics in debug overlay",
859 ),
860 ("show_query", "Show LazyFrame query in debug overlay"),
861 (
862 "show_transformations",
863 "Show transformation state in debug overlay",
864 ),
865];
866
867impl Default for AppConfig {
869 fn default() -> Self {
870 Self {
871 version: "0.2".to_string(),
872 cloud: CloudConfig::default(),
873 file_loading: FileLoadingConfig::default(),
874 display: DisplayConfig::default(),
875 performance: PerformanceConfig::default(),
876 chart: ChartConfig::default(),
877 theme: ThemeConfig::default(),
878 ui: UiConfig::default(),
879 query: QueryConfig::default(),
880 templates: TemplateConfig::default(),
881 debug: DebugConfig::default(),
882 }
883 }
884}
885
886impl Default for DisplayConfig {
887 fn default() -> Self {
888 Self {
889 pages_lookahead: 3,
890 pages_lookback: 3,
891 max_buffered_rows: 100_000,
892 max_buffered_mb: 512,
893 row_numbers: false,
894 row_start_index: 1,
895 table_cell_padding: 2,
896 column_colors: true,
897 sidebar_width: None,
898 }
899 }
900}
901
902impl Default for PerformanceConfig {
903 fn default() -> Self {
904 Self {
905 sampling_threshold: None,
906 event_poll_interval_ms: 25,
907 polars_streaming: true,
908 }
909 }
910}
911
912impl Default for ColorConfig {
913 fn default() -> Self {
914 Self {
915 keybind_hints: "cyan".to_string(),
916 keybind_labels: "indexed(252)".to_string(),
917 throbber: "cyan".to_string(),
918 primary_chart_series_color: "cyan".to_string(),
919 secondary_chart_series_color: "indexed(245)".to_string(),
920 success: "green".to_string(),
921 error: "red".to_string(),
922 warning: "yellow".to_string(),
923 dimmed: "dark_gray".to_string(),
924 background: "default".to_string(),
925 surface: "default".to_string(),
926 controls_bg: "indexed(235)".to_string(),
927 text_primary: "default".to_string(),
928 text_secondary: "indexed(240)".to_string(),
929 text_inverse: "black".to_string(),
930 table_header: "white".to_string(),
931 table_header_bg: "indexed(235)".to_string(),
932 row_numbers: "dark_gray".to_string(),
933 column_separator: "cyan".to_string(),
934 table_selected: "reversed".to_string(),
935 sidebar_border: "indexed(235)".to_string(),
936 modal_border_active: "yellow".to_string(),
937 modal_border_error: "red".to_string(),
938 distribution_normal: "green".to_string(),
939 distribution_skewed: "yellow".to_string(),
940 distribution_other: "white".to_string(),
941 outlier_marker: "red".to_string(),
942 cursor_focused: "default".to_string(),
943 cursor_dimmed: "default".to_string(),
944 alternate_row_color: "indexed(235)".to_string(),
945 str_col: "green".to_string(),
946 int_col: "cyan".to_string(),
947 float_col: "blue".to_string(),
948 bool_col: "yellow".to_string(),
949 temporal_col: "magenta".to_string(),
950 chart_series_color_1: "cyan".to_string(),
951 chart_series_color_2: "magenta".to_string(),
952 chart_series_color_3: "green".to_string(),
953 chart_series_color_4: "yellow".to_string(),
954 chart_series_color_5: "blue".to_string(),
955 chart_series_color_6: "red".to_string(),
956 chart_series_color_7: "bright_cyan".to_string(),
957 }
958 }
959}
960
961impl Default for ControlsConfig {
962 fn default() -> Self {
963 Self {
964 custom_controls: None,
965 row_count_width: 20,
966 }
967 }
968}
969
970impl Default for QueryConfig {
971 fn default() -> Self {
972 Self {
973 history_limit: 1000,
974 enable_history: true,
975 }
976 }
977}
978
979impl Default for DebugConfig {
980 fn default() -> Self {
981 Self {
982 enabled: false,
983 show_performance: true,
984 show_query: true,
985 show_transformations: true,
986 }
987 }
988}
989
990impl AppConfig {
992 pub fn load(app_name: &str) -> Result<Self> {
994 let mut config = AppConfig::default();
995
996 let config_path = ConfigManager::new(app_name)
998 .ok()
999 .map(|m| m.config_path("config.toml"));
1000 if let Ok(user_config) = Self::load_user_config(app_name) {
1001 config.merge(user_config);
1002 }
1003
1004 config.validate().map_err(|e| {
1006 let path_hint = config_path
1007 .as_ref()
1008 .map(|p| format!(" in {}", p.display()))
1009 .unwrap_or_default();
1010 eyre!("Invalid configuration{}: {}", path_hint, e)
1011 })?;
1012
1013 Ok(config)
1014 }
1015
1016 fn load_user_config(app_name: &str) -> Result<AppConfig> {
1018 let config_manager = ConfigManager::new(app_name)?;
1019 let config_path = config_manager.config_path("config.toml");
1020
1021 if !config_path.exists() {
1022 return Ok(AppConfig::default());
1023 }
1024
1025 let content = std::fs::read_to_string(&config_path).map_err(|e| {
1026 eyre!(
1027 "Failed to read config file at {}: {}",
1028 config_path.display(),
1029 e
1030 )
1031 })?;
1032
1033 toml::from_str(&content).map_err(|e| {
1034 eyre!(
1035 "Failed to parse config file at {}: {}",
1036 config_path.display(),
1037 e
1038 )
1039 })
1040 }
1041
1042 pub fn merge(&mut self, other: AppConfig) {
1044 if other.version != AppConfig::default().version {
1046 self.version = other.version;
1047 }
1048
1049 self.cloud.merge(other.cloud);
1051 self.file_loading.merge(other.file_loading);
1052 self.display.merge(other.display);
1053 self.performance.merge(other.performance);
1054 self.chart.merge(other.chart);
1055 self.theme.merge(other.theme);
1056 self.ui.merge(other.ui);
1057 self.query.merge(other.query);
1058 self.templates.merge(other.templates);
1059 self.debug.merge(other.debug);
1060 }
1061
1062 pub fn validate(&self) -> Result<()> {
1064 if !self.version.starts_with("0.2") {
1066 return Err(eyre!(
1067 "Unsupported config version: {}. Expected 0.2.x",
1068 self.version
1069 ));
1070 }
1071
1072 if let Some(t) = self.performance.sampling_threshold {
1074 if t == 0 {
1075 return Err(eyre!("sampling_threshold must be greater than 0 when set"));
1076 }
1077 }
1078
1079 if self.performance.event_poll_interval_ms == 0 {
1080 return Err(eyre!("event_poll_interval_ms must be greater than 0"));
1081 }
1082
1083 if let Some(n) = self.chart.row_limit {
1084 if n == 0 || n > MAX_CHART_ROW_LIMIT {
1085 return Err(eyre!(
1086 "chart.row_limit must be between 1 and {} when set, got {}",
1087 MAX_CHART_ROW_LIMIT,
1088 n
1089 ));
1090 }
1091 }
1092
1093 let parser = ColorParser::new();
1095 self.theme.colors.validate(&parser)?;
1096
1097 Ok(())
1098 }
1099}
1100
1101impl FileLoadingConfig {
1103 pub fn merge(&mut self, other: Self) {
1104 if other.delimiter.is_some() {
1105 self.delimiter = other.delimiter;
1106 }
1107 if other.has_header.is_some() {
1108 self.has_header = other.has_header;
1109 }
1110 if other.skip_lines.is_some() {
1111 self.skip_lines = other.skip_lines;
1112 }
1113 if other.skip_rows.is_some() {
1114 self.skip_rows = other.skip_rows;
1115 }
1116 if other.parse_dates.is_some() {
1117 self.parse_dates = other.parse_dates;
1118 }
1119 if other.decompress_in_memory.is_some() {
1120 self.decompress_in_memory = other.decompress_in_memory;
1121 }
1122 if other.temp_dir.is_some() {
1123 self.temp_dir = other.temp_dir.clone();
1124 }
1125 if other.single_spine_schema.is_some() {
1126 self.single_spine_schema = other.single_spine_schema;
1127 }
1128 }
1129}
1130
1131impl DisplayConfig {
1132 pub fn merge(&mut self, other: Self) {
1133 let default = DisplayConfig::default();
1134 if other.pages_lookahead != default.pages_lookahead {
1135 self.pages_lookahead = other.pages_lookahead;
1136 }
1137 if other.pages_lookback != default.pages_lookback {
1138 self.pages_lookback = other.pages_lookback;
1139 }
1140 if other.max_buffered_rows != default.max_buffered_rows {
1141 self.max_buffered_rows = other.max_buffered_rows;
1142 }
1143 if other.max_buffered_mb != default.max_buffered_mb {
1144 self.max_buffered_mb = other.max_buffered_mb;
1145 }
1146 if other.row_numbers != default.row_numbers {
1147 self.row_numbers = other.row_numbers;
1148 }
1149 if other.row_start_index != default.row_start_index {
1150 self.row_start_index = other.row_start_index;
1151 }
1152 if other.table_cell_padding != default.table_cell_padding {
1153 self.table_cell_padding = other.table_cell_padding;
1154 }
1155 if other.column_colors != default.column_colors {
1156 self.column_colors = other.column_colors;
1157 }
1158 if other.sidebar_width != default.sidebar_width {
1159 self.sidebar_width = other.sidebar_width;
1160 }
1161 }
1162}
1163
1164impl PerformanceConfig {
1165 pub fn merge(&mut self, other: Self) {
1166 let default = PerformanceConfig::default();
1167 if other.sampling_threshold != default.sampling_threshold {
1168 self.sampling_threshold = other.sampling_threshold;
1169 }
1170 if other.event_poll_interval_ms != default.event_poll_interval_ms {
1171 self.event_poll_interval_ms = other.event_poll_interval_ms;
1172 }
1173 if other.polars_streaming != default.polars_streaming {
1174 self.polars_streaming = other.polars_streaming;
1175 }
1176 }
1177}
1178
1179impl ThemeConfig {
1180 pub fn merge(&mut self, other: Self) {
1181 self.colors.merge(other.colors);
1182 }
1183}
1184
1185impl ColorConfig {
1186 fn validate(&self, parser: &ColorParser) -> Result<()> {
1188 macro_rules! validate_color {
1190 ($field:expr, $name:expr) => {
1191 parser.parse($field).map_err(|e| {
1192 eyre!(
1193 "theme.colors.{}: {}. Use a valid color name (e.g. red, cyan, bright_red), \
1194 hex (#rrggbb), or indexed(0-255)",
1195 $name,
1196 e
1197 )
1198 })?;
1199 };
1200 }
1201
1202 validate_color!(&self.keybind_hints, "keybind_hints");
1203 validate_color!(&self.keybind_labels, "keybind_labels");
1204 validate_color!(&self.throbber, "throbber");
1205 validate_color!(
1206 &self.primary_chart_series_color,
1207 "primary_chart_series_color"
1208 );
1209 validate_color!(
1210 &self.secondary_chart_series_color,
1211 "secondary_chart_series_color"
1212 );
1213 validate_color!(&self.success, "success");
1214 validate_color!(&self.error, "error");
1215 validate_color!(&self.warning, "warning");
1216 validate_color!(&self.dimmed, "dimmed");
1217 validate_color!(&self.background, "background");
1218 validate_color!(&self.surface, "surface");
1219 validate_color!(&self.controls_bg, "controls_bg");
1220 validate_color!(&self.text_primary, "text_primary");
1221 validate_color!(&self.text_secondary, "text_secondary");
1222 validate_color!(&self.text_inverse, "text_inverse");
1223 validate_color!(&self.table_header, "table_header");
1224 validate_color!(&self.table_header_bg, "table_header_bg");
1225 validate_color!(&self.row_numbers, "row_numbers");
1226 validate_color!(&self.column_separator, "column_separator");
1227 validate_color!(&self.table_selected, "table_selected");
1228 validate_color!(&self.sidebar_border, "sidebar_border");
1229 validate_color!(&self.modal_border_active, "modal_border_active");
1230 validate_color!(&self.modal_border_error, "modal_border_error");
1231 validate_color!(&self.distribution_normal, "distribution_normal");
1232 validate_color!(&self.distribution_skewed, "distribution_skewed");
1233 validate_color!(&self.distribution_other, "distribution_other");
1234 validate_color!(&self.outlier_marker, "outlier_marker");
1235 validate_color!(&self.cursor_focused, "cursor_focused");
1236 validate_color!(&self.cursor_dimmed, "cursor_dimmed");
1237 if self.alternate_row_color != "default" {
1238 validate_color!(&self.alternate_row_color, "alternate_row_color");
1239 }
1240 validate_color!(&self.str_col, "str_col");
1241 validate_color!(&self.int_col, "int_col");
1242 validate_color!(&self.float_col, "float_col");
1243 validate_color!(&self.bool_col, "bool_col");
1244 validate_color!(&self.temporal_col, "temporal_col");
1245 validate_color!(&self.chart_series_color_1, "chart_series_color_1");
1246 validate_color!(&self.chart_series_color_2, "chart_series_color_2");
1247 validate_color!(&self.chart_series_color_3, "chart_series_color_3");
1248 validate_color!(&self.chart_series_color_4, "chart_series_color_4");
1249 validate_color!(&self.chart_series_color_5, "chart_series_color_5");
1250 validate_color!(&self.chart_series_color_6, "chart_series_color_6");
1251 validate_color!(&self.chart_series_color_7, "chart_series_color_7");
1252
1253 Ok(())
1254 }
1255
1256 pub fn merge(&mut self, other: Self) {
1257 let default = ColorConfig::default();
1258
1259 if other.keybind_hints != default.keybind_hints {
1261 self.keybind_hints = other.keybind_hints;
1262 }
1263 if other.keybind_labels != default.keybind_labels {
1264 self.keybind_labels = other.keybind_labels;
1265 }
1266 if other.throbber != default.throbber {
1267 self.throbber = other.throbber;
1268 }
1269 if other.primary_chart_series_color != default.primary_chart_series_color {
1270 self.primary_chart_series_color = other.primary_chart_series_color;
1271 }
1272 if other.secondary_chart_series_color != default.secondary_chart_series_color {
1273 self.secondary_chart_series_color = other.secondary_chart_series_color;
1274 }
1275 if other.success != default.success {
1276 self.success = other.success;
1277 }
1278 if other.error != default.error {
1279 self.error = other.error;
1280 }
1281 if other.warning != default.warning {
1282 self.warning = other.warning;
1283 }
1284 if other.dimmed != default.dimmed {
1285 self.dimmed = other.dimmed;
1286 }
1287 if other.background != default.background {
1288 self.background = other.background;
1289 }
1290 if other.surface != default.surface {
1291 self.surface = other.surface;
1292 }
1293 if other.controls_bg != default.controls_bg {
1294 self.controls_bg = other.controls_bg;
1295 }
1296 if other.text_primary != default.text_primary {
1297 self.text_primary = other.text_primary;
1298 }
1299 if other.text_secondary != default.text_secondary {
1300 self.text_secondary = other.text_secondary;
1301 }
1302 if other.text_inverse != default.text_inverse {
1303 self.text_inverse = other.text_inverse;
1304 }
1305 if other.table_header != default.table_header {
1306 self.table_header = other.table_header;
1307 }
1308 if other.table_header_bg != default.table_header_bg {
1309 self.table_header_bg = other.table_header_bg;
1310 }
1311 if other.row_numbers != default.row_numbers {
1312 self.row_numbers = other.row_numbers;
1313 }
1314 if other.column_separator != default.column_separator {
1315 self.column_separator = other.column_separator;
1316 }
1317 if other.table_selected != default.table_selected {
1318 self.table_selected = other.table_selected;
1319 }
1320 if other.sidebar_border != default.sidebar_border {
1321 self.sidebar_border = other.sidebar_border;
1322 }
1323 if other.modal_border_active != default.modal_border_active {
1324 self.modal_border_active = other.modal_border_active;
1325 }
1326 if other.modal_border_error != default.modal_border_error {
1327 self.modal_border_error = other.modal_border_error;
1328 }
1329 if other.distribution_normal != default.distribution_normal {
1330 self.distribution_normal = other.distribution_normal;
1331 }
1332 if other.distribution_skewed != default.distribution_skewed {
1333 self.distribution_skewed = other.distribution_skewed;
1334 }
1335 if other.distribution_other != default.distribution_other {
1336 self.distribution_other = other.distribution_other;
1337 }
1338 if other.outlier_marker != default.outlier_marker {
1339 self.outlier_marker = other.outlier_marker;
1340 }
1341 if other.cursor_focused != default.cursor_focused {
1342 self.cursor_focused = other.cursor_focused;
1343 }
1344 if other.cursor_dimmed != default.cursor_dimmed {
1345 self.cursor_dimmed = other.cursor_dimmed;
1346 }
1347 if other.alternate_row_color != default.alternate_row_color {
1348 self.alternate_row_color = other.alternate_row_color;
1349 }
1350 if other.str_col != default.str_col {
1351 self.str_col = other.str_col;
1352 }
1353 if other.int_col != default.int_col {
1354 self.int_col = other.int_col;
1355 }
1356 if other.float_col != default.float_col {
1357 self.float_col = other.float_col;
1358 }
1359 if other.bool_col != default.bool_col {
1360 self.bool_col = other.bool_col;
1361 }
1362 if other.temporal_col != default.temporal_col {
1363 self.temporal_col = other.temporal_col;
1364 }
1365 if other.chart_series_color_1 != default.chart_series_color_1 {
1366 self.chart_series_color_1 = other.chart_series_color_1;
1367 }
1368 if other.chart_series_color_2 != default.chart_series_color_2 {
1369 self.chart_series_color_2 = other.chart_series_color_2;
1370 }
1371 if other.chart_series_color_3 != default.chart_series_color_3 {
1372 self.chart_series_color_3 = other.chart_series_color_3;
1373 }
1374 if other.chart_series_color_4 != default.chart_series_color_4 {
1375 self.chart_series_color_4 = other.chart_series_color_4;
1376 }
1377 if other.chart_series_color_5 != default.chart_series_color_5 {
1378 self.chart_series_color_5 = other.chart_series_color_5;
1379 }
1380 if other.chart_series_color_6 != default.chart_series_color_6 {
1381 self.chart_series_color_6 = other.chart_series_color_6;
1382 }
1383 if other.chart_series_color_7 != default.chart_series_color_7 {
1384 self.chart_series_color_7 = other.chart_series_color_7;
1385 }
1386 }
1387}
1388
1389impl UiConfig {
1390 pub fn merge(&mut self, other: Self) {
1391 self.controls.merge(other.controls);
1392 }
1393}
1394
1395impl ControlsConfig {
1396 pub fn merge(&mut self, other: Self) {
1397 if other.custom_controls.is_some() {
1398 self.custom_controls = other.custom_controls;
1399 }
1400 let default = ControlsConfig::default();
1401 if other.row_count_width != default.row_count_width {
1402 self.row_count_width = other.row_count_width;
1403 }
1404 }
1405}
1406
1407impl QueryConfig {
1408 pub fn merge(&mut self, other: Self) {
1409 let default = QueryConfig::default();
1410 if other.history_limit != default.history_limit {
1411 self.history_limit = other.history_limit;
1412 }
1413 if other.enable_history != default.enable_history {
1414 self.enable_history = other.enable_history;
1415 }
1416 }
1417}
1418
1419impl TemplateConfig {
1420 pub fn merge(&mut self, other: Self) {
1421 let default = TemplateConfig::default();
1422 if other.auto_apply != default.auto_apply {
1423 self.auto_apply = other.auto_apply;
1424 }
1425 }
1426}
1427
1428impl DebugConfig {
1429 pub fn merge(&mut self, other: Self) {
1430 let default = DebugConfig::default();
1431 if other.enabled != default.enabled {
1432 self.enabled = other.enabled;
1433 }
1434 if other.show_performance != default.show_performance {
1435 self.show_performance = other.show_performance;
1436 }
1437 if other.show_query != default.show_query {
1438 self.show_query = other.show_query;
1439 }
1440 if other.show_transformations != default.show_transformations {
1441 self.show_transformations = other.show_transformations;
1442 }
1443 }
1444}
1445
1446pub struct ColorParser {
1448 supports_true_color: bool,
1449 supports_256: bool,
1450 no_color: bool,
1451}
1452
1453impl ColorParser {
1454 pub fn new() -> Self {
1456 let no_color = std::env::var("NO_COLOR").is_ok();
1457 let support = supports_color::on(Stream::Stdout);
1458
1459 Self {
1460 supports_true_color: support.as_ref().map(|s| s.has_16m).unwrap_or(false),
1461 supports_256: support.as_ref().map(|s| s.has_256).unwrap_or(false),
1462 no_color,
1463 }
1464 }
1465
1466 pub fn parse(&self, s: &str) -> Result<Color> {
1468 if self.no_color {
1469 return Ok(Color::Reset);
1470 }
1471
1472 let trimmed = s.trim();
1473
1474 if trimmed.starts_with('#') && trimmed.len() == 7 {
1476 let (r, g, b) = parse_hex(trimmed)?;
1477 return Ok(self.convert_rgb_to_terminal_color(r, g, b));
1478 }
1479
1480 if trimmed.to_lowercase().starts_with("indexed(") && trimmed.ends_with(')') {
1482 let num_str = &trimmed[8..trimmed.len() - 1]; let num = num_str.parse::<u8>().map_err(|_| {
1484 eyre!(
1485 "Invalid indexed color: '{}'. Expected format: indexed(0-255)",
1486 trimmed
1487 )
1488 })?;
1489 return Ok(Color::Indexed(num));
1490 }
1491
1492 let lower = trimmed.to_lowercase();
1494 match lower.as_str() {
1495 "black" => Ok(Color::Black),
1497 "red" => Ok(Color::Red),
1498 "green" => Ok(Color::Green),
1499 "yellow" => Ok(Color::Yellow),
1500 "blue" => Ok(Color::Blue),
1501 "magenta" => Ok(Color::Magenta),
1502 "cyan" => Ok(Color::Cyan),
1503 "white" => Ok(Color::White),
1504
1505 "bright_black" | "bright black" => Ok(Color::Indexed(8)),
1507 "bright_red" | "bright red" => Ok(Color::Indexed(9)),
1508 "bright_green" | "bright green" => Ok(Color::Indexed(10)),
1509 "bright_yellow" | "bright yellow" => Ok(Color::Indexed(11)),
1510 "bright_blue" | "bright blue" => Ok(Color::Indexed(12)),
1511 "bright_magenta" | "bright magenta" => Ok(Color::Indexed(13)),
1512 "bright_cyan" | "bright cyan" => Ok(Color::Indexed(14)),
1513 "bright_white" | "bright white" => Ok(Color::Indexed(15)),
1514
1515 "gray" | "grey" => Ok(Color::Indexed(8)),
1517 "dark_gray" | "dark gray" | "dark_grey" | "dark grey" => Ok(Color::Indexed(8)),
1518 "light_gray" | "light gray" | "light_grey" | "light grey" => Ok(Color::Indexed(7)),
1519
1520 "reset" | "default" | "none" | "reversed" => Ok(Color::Reset),
1522
1523 _ => Err(eyre!(
1524 "Unknown color name: '{}'. Supported: basic ANSI colors (red, blue, etc.), \
1525 bright variants (bright_red, etc.), or hex colors (#ff0000)",
1526 trimmed
1527 )),
1528 }
1529 }
1530
1531 fn convert_rgb_to_terminal_color(&self, r: u8, g: u8, b: u8) -> Color {
1533 if self.supports_true_color {
1534 Color::Rgb(r, g, b)
1535 } else if self.supports_256 {
1536 Color::Indexed(rgb_to_256_color(r, g, b))
1537 } else {
1538 rgb_to_basic_ansi(r, g, b)
1539 }
1540 }
1541}
1542
1543impl Default for ColorParser {
1544 fn default() -> Self {
1545 Self::new()
1546 }
1547}
1548
1549fn parse_hex(s: &str) -> Result<(u8, u8, u8)> {
1551 if !s.starts_with('#') || s.len() != 7 {
1552 return Err(eyre!(
1553 "Invalid hex color format: '{}'. Expected format: #rrggbb",
1554 s
1555 ));
1556 }
1557
1558 let r = u8::from_str_radix(&s[1..3], 16)
1559 .map_err(|_| eyre!("Invalid red component in hex color: {}", s))?;
1560 let g = u8::from_str_radix(&s[3..5], 16)
1561 .map_err(|_| eyre!("Invalid green component in hex color: {}", s))?;
1562 let b = u8::from_str_radix(&s[5..7], 16)
1563 .map_err(|_| eyre!("Invalid blue component in hex color: {}", s))?;
1564
1565 Ok((r, g, b))
1566}
1567
1568pub fn rgb_to_256_color(r: u8, g: u8, b: u8) -> u8 {
1571 let max_diff = r.max(g).max(b) as i16 - r.min(g).min(b) as i16;
1573 if max_diff < 10 {
1574 let gray = (r as u16 + g as u16 + b as u16) / 3;
1576 if gray < 8 {
1577 return 16; } else if gray > 247 {
1579 return 231; } else {
1581 return 232 + ((gray - 8) * 24 / 240) as u8;
1582 }
1583 }
1584
1585 let r_idx = (r as u16 * 5 / 255) as u8;
1587 let g_idx = (g as u16 * 5 / 255) as u8;
1588 let b_idx = (b as u16 * 5 / 255) as u8;
1589
1590 16 + 36 * r_idx + 6 * g_idx + b_idx
1591}
1592
1593pub fn rgb_to_basic_ansi(r: u8, g: u8, b: u8) -> Color {
1595 let r_bright = r > 128;
1597 let g_bright = g > 128;
1598 let b_bright = b > 128;
1599
1600 let max_diff = r.max(g).max(b) as i16 - r.min(g).min(b) as i16;
1602 if max_diff < 30 {
1603 let avg = (r as u16 + g as u16 + b as u16) / 3;
1604 return if avg < 64 { Color::Black } else { Color::White };
1605 }
1606
1607 match (r_bright, g_bright, b_bright) {
1609 (false, false, false) => Color::Black,
1610 (true, false, false) => Color::Red,
1611 (false, true, false) => Color::Green,
1612 (true, true, false) => Color::Yellow,
1613 (false, false, true) => Color::Blue,
1614 (true, false, true) => Color::Magenta,
1615 (false, true, true) => Color::Cyan,
1616 (true, true, true) => Color::White,
1617 }
1618}
1619
1620#[derive(Debug, Clone)]
1622pub struct Theme {
1623 pub colors: HashMap<String, Color>,
1624}
1625
1626impl Theme {
1627 pub fn from_config(config: &ThemeConfig) -> Result<Self> {
1629 let parser = ColorParser::new();
1630 let mut colors = HashMap::new();
1631
1632 colors.insert(
1634 "keybind_hints".to_string(),
1635 parser.parse(&config.colors.keybind_hints)?,
1636 );
1637 colors.insert(
1638 "keybind_labels".to_string(),
1639 parser.parse(&config.colors.keybind_labels)?,
1640 );
1641 colors.insert(
1642 "throbber".to_string(),
1643 parser.parse(&config.colors.throbber)?,
1644 );
1645 colors.insert(
1646 "primary_chart_series_color".to_string(),
1647 parser.parse(&config.colors.primary_chart_series_color)?,
1648 );
1649 colors.insert(
1650 "secondary_chart_series_color".to_string(),
1651 parser.parse(&config.colors.secondary_chart_series_color)?,
1652 );
1653 colors.insert("success".to_string(), parser.parse(&config.colors.success)?);
1654 colors.insert("error".to_string(), parser.parse(&config.colors.error)?);
1655 colors.insert("warning".to_string(), parser.parse(&config.colors.warning)?);
1656 colors.insert("dimmed".to_string(), parser.parse(&config.colors.dimmed)?);
1657 colors.insert(
1658 "background".to_string(),
1659 parser.parse(&config.colors.background)?,
1660 );
1661 colors.insert("surface".to_string(), parser.parse(&config.colors.surface)?);
1662 colors.insert(
1663 "controls_bg".to_string(),
1664 parser.parse(&config.colors.controls_bg)?,
1665 );
1666 colors.insert(
1667 "text_primary".to_string(),
1668 parser.parse(&config.colors.text_primary)?,
1669 );
1670 colors.insert(
1671 "text_secondary".to_string(),
1672 parser.parse(&config.colors.text_secondary)?,
1673 );
1674 colors.insert(
1675 "text_inverse".to_string(),
1676 parser.parse(&config.colors.text_inverse)?,
1677 );
1678 colors.insert(
1679 "table_header".to_string(),
1680 parser.parse(&config.colors.table_header)?,
1681 );
1682 colors.insert(
1683 "table_header_bg".to_string(),
1684 parser.parse(&config.colors.table_header_bg)?,
1685 );
1686 colors.insert(
1687 "row_numbers".to_string(),
1688 parser.parse(&config.colors.row_numbers)?,
1689 );
1690 colors.insert(
1691 "column_separator".to_string(),
1692 parser.parse(&config.colors.column_separator)?,
1693 );
1694 colors.insert(
1695 "table_selected".to_string(),
1696 parser.parse(&config.colors.table_selected)?,
1697 );
1698 colors.insert(
1699 "sidebar_border".to_string(),
1700 parser.parse(&config.colors.sidebar_border)?,
1701 );
1702 colors.insert(
1703 "modal_border_active".to_string(),
1704 parser.parse(&config.colors.modal_border_active)?,
1705 );
1706 colors.insert(
1707 "modal_border_error".to_string(),
1708 parser.parse(&config.colors.modal_border_error)?,
1709 );
1710 colors.insert(
1711 "distribution_normal".to_string(),
1712 parser.parse(&config.colors.distribution_normal)?,
1713 );
1714 colors.insert(
1715 "distribution_skewed".to_string(),
1716 parser.parse(&config.colors.distribution_skewed)?,
1717 );
1718 colors.insert(
1719 "distribution_other".to_string(),
1720 parser.parse(&config.colors.distribution_other)?,
1721 );
1722 colors.insert(
1723 "outlier_marker".to_string(),
1724 parser.parse(&config.colors.outlier_marker)?,
1725 );
1726 colors.insert(
1727 "cursor_focused".to_string(),
1728 parser.parse(&config.colors.cursor_focused)?,
1729 );
1730 colors.insert(
1731 "cursor_dimmed".to_string(),
1732 parser.parse(&config.colors.cursor_dimmed)?,
1733 );
1734 if config.colors.alternate_row_color != "default" {
1735 colors.insert(
1736 "alternate_row_color".to_string(),
1737 parser.parse(&config.colors.alternate_row_color)?,
1738 );
1739 }
1740 colors.insert("str_col".to_string(), parser.parse(&config.colors.str_col)?);
1741 colors.insert("int_col".to_string(), parser.parse(&config.colors.int_col)?);
1742 colors.insert(
1743 "float_col".to_string(),
1744 parser.parse(&config.colors.float_col)?,
1745 );
1746 colors.insert(
1747 "bool_col".to_string(),
1748 parser.parse(&config.colors.bool_col)?,
1749 );
1750 colors.insert(
1751 "temporal_col".to_string(),
1752 parser.parse(&config.colors.temporal_col)?,
1753 );
1754 colors.insert(
1755 "chart_series_color_1".to_string(),
1756 parser.parse(&config.colors.chart_series_color_1)?,
1757 );
1758 colors.insert(
1759 "chart_series_color_2".to_string(),
1760 parser.parse(&config.colors.chart_series_color_2)?,
1761 );
1762 colors.insert(
1763 "chart_series_color_3".to_string(),
1764 parser.parse(&config.colors.chart_series_color_3)?,
1765 );
1766 colors.insert(
1767 "chart_series_color_4".to_string(),
1768 parser.parse(&config.colors.chart_series_color_4)?,
1769 );
1770 colors.insert(
1771 "chart_series_color_5".to_string(),
1772 parser.parse(&config.colors.chart_series_color_5)?,
1773 );
1774 colors.insert(
1775 "chart_series_color_6".to_string(),
1776 parser.parse(&config.colors.chart_series_color_6)?,
1777 );
1778 colors.insert(
1779 "chart_series_color_7".to_string(),
1780 parser.parse(&config.colors.chart_series_color_7)?,
1781 );
1782
1783 Ok(Self { colors })
1784 }
1785
1786 pub fn get(&self, name: &str) -> Color {
1788 self.colors.get(name).copied().unwrap_or(Color::Reset)
1789 }
1790
1791 pub fn get_optional(&self, name: &str) -> Option<Color> {
1793 self.colors.get(name).copied()
1794 }
1795}