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}
518
519const DISPLAY_COMMENTS: &[(&str, &str)] = &[
521 (
522 "pages_lookahead",
523 "Number of pages to buffer ahead of visible area\nLarger values = smoother scrolling but more memory",
524 ),
525 (
526 "pages_lookback",
527 "Number of pages to buffer behind visible area\nLarger values = smoother scrolling but more memory",
528 ),
529 (
530 "max_buffered_rows",
531 "Maximum rows in scroll buffer (0 = no limit)\nPrevents unbounded memory use when scrolling",
532 ),
533 (
534 "max_buffered_mb",
535 "Maximum buffer size in MB (0 = no limit)\nUses estimated memory; helps with very wide tables",
536 ),
537 ("row_numbers", "Display row numbers on the left side of the table"),
538 ("row_start_index", "Starting index for row numbers (0 or 1)"),
539 (
540 "table_cell_padding",
541 "Number of spaces between columns in the main data table (>= 0)\nDefault 2",
542 ),
543 (
544 "column_colors",
545 "Colorize main table cells by column type (string, int, float, bool, date/datetime)\nSet to false to use default text color for all cells",
546 ),
547];
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
550#[serde(default)]
551pub struct PerformanceConfig {
552 pub sampling_threshold: Option<usize>,
554 pub event_poll_interval_ms: u64,
555 pub polars_streaming: bool,
557}
558
559const PERFORMANCE_COMMENTS: &[(&str, &str)] = &[
561 (
562 "sampling_threshold",
563 "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",
564 ),
565 (
566 "event_poll_interval_ms",
567 "Event polling interval in milliseconds\nLower values = more responsive but higher CPU usage",
568 ),
569 (
570 "polars_streaming",
571 "Use Polars streaming engine for LazyFrame collect when available (default: true). Reduces memory and can improve performance on large or partitioned data.",
572 ),
573];
574
575pub const DEFAULT_CHART_ROW_LIMIT: usize = 10_000;
577pub const MAX_CHART_ROW_LIMIT: usize = u32::MAX as usize;
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
581#[serde(default)]
582pub struct ChartConfig {
583 pub row_limit: Option<usize>,
585}
586
587const CHART_COMMENTS: &[(&str, &str)] = &[
589 (
590 "row_limit",
591 "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",
592 ),
593];
594
595impl Default for ChartConfig {
596 fn default() -> Self {
597 Self {
598 row_limit: Some(DEFAULT_CHART_ROW_LIMIT),
599 }
600 }
601}
602
603impl ChartConfig {
604 pub fn merge(&mut self, other: Self) {
605 if other.row_limit.is_some() {
606 self.row_limit = other.row_limit;
607 }
608 }
609}
610
611#[derive(Debug, Clone, Serialize, Deserialize, Default)]
612#[serde(default)]
613pub struct ThemeConfig {
614 pub colors: ColorConfig,
615}
616
617const THEME_COMMENTS: &[(&str, &str)] = &[];
619
620fn default_row_numbers_color() -> String {
621 "dark_gray".to_string()
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
625#[serde(default)]
626pub struct ColorConfig {
674 pub keybind_hints: String,
675 pub keybind_labels: String,
676 pub throbber: String,
677 pub primary_chart_series_color: String,
678 pub secondary_chart_series_color: String,
679 pub success: String,
680 pub error: String,
681 pub warning: String,
682 pub dimmed: String,
683 pub background: String,
684 pub surface: String,
685 pub controls_bg: String,
686 pub text_primary: String,
687 pub text_secondary: String,
688 pub text_inverse: String,
689 pub table_header: String,
690 pub table_header_bg: String,
691 #[serde(default = "default_row_numbers_color")]
693 pub row_numbers: String,
694 pub column_separator: String,
695 pub table_selected: String,
696 pub sidebar_border: String,
697 pub modal_border_active: String,
698 pub modal_border_error: String,
699 pub distribution_normal: String,
700 pub distribution_skewed: String,
701 pub distribution_other: String,
702 pub outlier_marker: String,
703 pub cursor_focused: String,
704 pub cursor_dimmed: String,
705 pub alternate_row_color: String,
707 pub str_col: String,
709 pub int_col: String,
710 pub float_col: String,
711 pub bool_col: String,
712 pub temporal_col: String,
713 pub chart_series_color_1: String,
715 pub chart_series_color_2: String,
716 pub chart_series_color_3: String,
717 pub chart_series_color_4: String,
718 pub chart_series_color_5: String,
719 pub chart_series_color_6: String,
720 pub chart_series_color_7: String,
721}
722
723const COLOR_COMMENTS: &[(&str, &str)] = &[
725 (
726 "keybind_hints",
727 "Keybind hints (modals, breadcrumb, correlation matrix)",
728 ),
729 ("keybind_labels", "Action labels in controls bar"),
730 ("throbber", "Busy indicator (spinner) in control bar"),
731 (
732 "primary_chart_series_color",
733 "Chart data (histogram bars, Q-Q plot data points)",
734 ),
735 (
736 "secondary_chart_series_color",
737 "Chart theory (histogram overlays, Q-Q plot reference line)",
738 ),
739 ("success", "Success indicators, normal distributions"),
740 ("error", "Error messages, outliers"),
741 ("warning", "Warnings, skewed distributions"),
742 ("dimmed", "Dimmed elements, axis lines"),
743 ("background", "Main background"),
744 ("surface", "Modal/surface backgrounds"),
745 ("controls_bg", "Controls bar background"),
746 ("text_primary", "Primary text"),
747 ("text_secondary", "Secondary text"),
748 ("text_inverse", "Text on light backgrounds"),
749 ("table_header", "Table column header text"),
750 ("table_header_bg", "Table column header background"),
751 ("row_numbers", "Row numbers column text; use \"default\" for terminal default"),
752 ("column_separator", "Vertical line between columns"),
753 ("table_selected", "Selected row style"),
754 ("sidebar_border", "Sidebar borders"),
755 ("modal_border_active", "Active modal elements"),
756 ("modal_border_error", "Error modal borders"),
757 ("distribution_normal", "Normal distribution indicator"),
758 ("distribution_skewed", "Skewed distribution indicator"),
759 ("distribution_other", "Other distribution types"),
760 ("outlier_marker", "Outlier indicators"),
761 (
762 "cursor_focused",
763 "Cursor color when text input is focused\nText under cursor uses reverse of this color",
764 ),
765 (
766 "cursor_dimmed",
767 "Cursor color when text input is unfocused (currently unused - unfocused inputs hide cursor)",
768 ),
769 (
770 "alternate_row_color",
771 "Background color for every other row in the main data table\nSet to \"default\" to disable alternate row coloring",
772 ),
773 ("str_col", "Main table: string column text color"),
774 ("int_col", "Main table: integer column text color"),
775 ("float_col", "Main table: float column text color"),
776 ("bool_col", "Main table: boolean column text color"),
777 ("temporal_col", "Main table: date/datetime/time column text color"),
778 ("chart_series_color_1", "Chart view: first series color"),
779 ("chart_series_color_2", "Chart view: second series color"),
780 ("chart_series_color_3", "Chart view: third series color"),
781 ("chart_series_color_4", "Chart view: fourth series color"),
782 ("chart_series_color_5", "Chart view: fifth series color"),
783 ("chart_series_color_6", "Chart view: sixth series color"),
784 ("chart_series_color_7", "Chart view: seventh series color"),
785];
786
787#[derive(Debug, Clone, Serialize, Deserialize, Default)]
788#[serde(default)]
789pub struct UiConfig {
790 pub controls: ControlsConfig,
791}
792
793#[derive(Debug, Clone, Serialize, Deserialize)]
794#[serde(default)]
795pub struct ControlsConfig {
796 pub custom_controls: Option<Vec<(String, String)>>,
797 pub row_count_width: usize,
798}
799
800const CONTROLS_COMMENTS: &[(&str, &str)] = &[
802 (
803 "custom_controls",
804 "Custom control keybindings (optional)\nFormat: [[\"key\", \"label\"], [\"key\", \"label\"], ...]\nIf not specified, uses default controls",
805 ),
806 ("row_count_width", "Row count display width in characters"),
807];
808
809#[derive(Debug, Clone, Serialize, Deserialize)]
810#[serde(default)]
811pub struct QueryConfig {
812 pub history_limit: usize,
813 pub enable_history: bool,
814}
815
816const QUERY_COMMENTS: &[(&str, &str)] = &[
818 (
819 "history_limit",
820 "Maximum number of queries to keep in history",
821 ),
822 ("enable_history", "Enable query history caching"),
823];
824
825#[derive(Debug, Clone, Serialize, Deserialize, Default)]
826#[serde(default)]
827pub struct TemplateConfig {
828 pub auto_apply: bool,
829}
830
831const TEMPLATE_COMMENTS: &[(&str, &str)] = &[(
833 "auto_apply",
834 "Auto-apply most relevant template on file open",
835)];
836
837#[derive(Debug, Clone, Serialize, Deserialize)]
838#[serde(default)]
839pub struct DebugConfig {
840 pub enabled: bool,
841 pub show_performance: bool,
842 pub show_query: bool,
843 pub show_transformations: bool,
844}
845
846const DEBUG_COMMENTS: &[(&str, &str)] = &[
848 ("enabled", "Enable debug overlay by default"),
849 (
850 "show_performance",
851 "Show performance metrics in debug overlay",
852 ),
853 ("show_query", "Show LazyFrame query in debug overlay"),
854 (
855 "show_transformations",
856 "Show transformation state in debug overlay",
857 ),
858];
859
860impl Default for AppConfig {
862 fn default() -> Self {
863 Self {
864 version: "0.2".to_string(),
865 cloud: CloudConfig::default(),
866 file_loading: FileLoadingConfig::default(),
867 display: DisplayConfig::default(),
868 performance: PerformanceConfig::default(),
869 chart: ChartConfig::default(),
870 theme: ThemeConfig::default(),
871 ui: UiConfig::default(),
872 query: QueryConfig::default(),
873 templates: TemplateConfig::default(),
874 debug: DebugConfig::default(),
875 }
876 }
877}
878
879impl Default for DisplayConfig {
880 fn default() -> Self {
881 Self {
882 pages_lookahead: 3,
883 pages_lookback: 3,
884 max_buffered_rows: 100_000,
885 max_buffered_mb: 512,
886 row_numbers: false,
887 row_start_index: 1,
888 table_cell_padding: 2,
889 column_colors: true,
890 }
891 }
892}
893
894impl Default for PerformanceConfig {
895 fn default() -> Self {
896 Self {
897 sampling_threshold: None,
898 event_poll_interval_ms: 25,
899 polars_streaming: true,
900 }
901 }
902}
903
904impl Default for ColorConfig {
905 fn default() -> Self {
906 Self {
907 keybind_hints: "cyan".to_string(),
908 keybind_labels: "indexed(252)".to_string(),
909 throbber: "cyan".to_string(),
910 primary_chart_series_color: "cyan".to_string(),
911 secondary_chart_series_color: "indexed(245)".to_string(),
912 success: "green".to_string(),
913 error: "red".to_string(),
914 warning: "yellow".to_string(),
915 dimmed: "dark_gray".to_string(),
916 background: "default".to_string(),
917 surface: "default".to_string(),
918 controls_bg: "indexed(235)".to_string(),
919 text_primary: "default".to_string(),
920 text_secondary: "indexed(240)".to_string(),
921 text_inverse: "black".to_string(),
922 table_header: "white".to_string(),
923 table_header_bg: "indexed(235)".to_string(),
924 row_numbers: "dark_gray".to_string(),
925 column_separator: "cyan".to_string(),
926 table_selected: "reversed".to_string(),
927 sidebar_border: "indexed(235)".to_string(),
928 modal_border_active: "yellow".to_string(),
929 modal_border_error: "red".to_string(),
930 distribution_normal: "green".to_string(),
931 distribution_skewed: "yellow".to_string(),
932 distribution_other: "white".to_string(),
933 outlier_marker: "red".to_string(),
934 cursor_focused: "default".to_string(),
935 cursor_dimmed: "default".to_string(),
936 alternate_row_color: "indexed(235)".to_string(),
937 str_col: "green".to_string(),
938 int_col: "cyan".to_string(),
939 float_col: "blue".to_string(),
940 bool_col: "yellow".to_string(),
941 temporal_col: "magenta".to_string(),
942 chart_series_color_1: "cyan".to_string(),
943 chart_series_color_2: "magenta".to_string(),
944 chart_series_color_3: "green".to_string(),
945 chart_series_color_4: "yellow".to_string(),
946 chart_series_color_5: "blue".to_string(),
947 chart_series_color_6: "red".to_string(),
948 chart_series_color_7: "bright_cyan".to_string(),
949 }
950 }
951}
952
953impl Default for ControlsConfig {
954 fn default() -> Self {
955 Self {
956 custom_controls: None,
957 row_count_width: 20,
958 }
959 }
960}
961
962impl Default for QueryConfig {
963 fn default() -> Self {
964 Self {
965 history_limit: 1000,
966 enable_history: true,
967 }
968 }
969}
970
971impl Default for DebugConfig {
972 fn default() -> Self {
973 Self {
974 enabled: false,
975 show_performance: true,
976 show_query: true,
977 show_transformations: true,
978 }
979 }
980}
981
982impl AppConfig {
984 pub fn load(app_name: &str) -> Result<Self> {
986 let mut config = AppConfig::default();
987
988 let config_path = ConfigManager::new(app_name)
990 .ok()
991 .map(|m| m.config_path("config.toml"));
992 if let Ok(user_config) = Self::load_user_config(app_name) {
993 config.merge(user_config);
994 }
995
996 config.validate().map_err(|e| {
998 let path_hint = config_path
999 .as_ref()
1000 .map(|p| format!(" in {}", p.display()))
1001 .unwrap_or_default();
1002 eyre!("Invalid configuration{}: {}", path_hint, e)
1003 })?;
1004
1005 Ok(config)
1006 }
1007
1008 fn load_user_config(app_name: &str) -> Result<AppConfig> {
1010 let config_manager = ConfigManager::new(app_name)?;
1011 let config_path = config_manager.config_path("config.toml");
1012
1013 if !config_path.exists() {
1014 return Ok(AppConfig::default());
1015 }
1016
1017 let content = std::fs::read_to_string(&config_path).map_err(|e| {
1018 eyre!(
1019 "Failed to read config file at {}: {}",
1020 config_path.display(),
1021 e
1022 )
1023 })?;
1024
1025 toml::from_str(&content).map_err(|e| {
1026 eyre!(
1027 "Failed to parse config file at {}: {}",
1028 config_path.display(),
1029 e
1030 )
1031 })
1032 }
1033
1034 pub fn merge(&mut self, other: AppConfig) {
1036 if other.version != AppConfig::default().version {
1038 self.version = other.version;
1039 }
1040
1041 self.cloud.merge(other.cloud);
1043 self.file_loading.merge(other.file_loading);
1044 self.display.merge(other.display);
1045 self.performance.merge(other.performance);
1046 self.chart.merge(other.chart);
1047 self.theme.merge(other.theme);
1048 self.ui.merge(other.ui);
1049 self.query.merge(other.query);
1050 self.templates.merge(other.templates);
1051 self.debug.merge(other.debug);
1052 }
1053
1054 pub fn validate(&self) -> Result<()> {
1056 if !self.version.starts_with("0.2") {
1058 return Err(eyre!(
1059 "Unsupported config version: {}. Expected 0.2.x",
1060 self.version
1061 ));
1062 }
1063
1064 if let Some(t) = self.performance.sampling_threshold {
1066 if t == 0 {
1067 return Err(eyre!("sampling_threshold must be greater than 0 when set"));
1068 }
1069 }
1070
1071 if self.performance.event_poll_interval_ms == 0 {
1072 return Err(eyre!("event_poll_interval_ms must be greater than 0"));
1073 }
1074
1075 if let Some(n) = self.chart.row_limit {
1076 if n == 0 || n > MAX_CHART_ROW_LIMIT {
1077 return Err(eyre!(
1078 "chart.row_limit must be between 1 and {} when set, got {}",
1079 MAX_CHART_ROW_LIMIT,
1080 n
1081 ));
1082 }
1083 }
1084
1085 let parser = ColorParser::new();
1087 self.theme.colors.validate(&parser)?;
1088
1089 Ok(())
1090 }
1091}
1092
1093impl FileLoadingConfig {
1095 pub fn merge(&mut self, other: Self) {
1096 if other.delimiter.is_some() {
1097 self.delimiter = other.delimiter;
1098 }
1099 if other.has_header.is_some() {
1100 self.has_header = other.has_header;
1101 }
1102 if other.skip_lines.is_some() {
1103 self.skip_lines = other.skip_lines;
1104 }
1105 if other.skip_rows.is_some() {
1106 self.skip_rows = other.skip_rows;
1107 }
1108 if other.parse_dates.is_some() {
1109 self.parse_dates = other.parse_dates;
1110 }
1111 if other.decompress_in_memory.is_some() {
1112 self.decompress_in_memory = other.decompress_in_memory;
1113 }
1114 if other.temp_dir.is_some() {
1115 self.temp_dir = other.temp_dir.clone();
1116 }
1117 if other.single_spine_schema.is_some() {
1118 self.single_spine_schema = other.single_spine_schema;
1119 }
1120 }
1121}
1122
1123impl DisplayConfig {
1124 pub fn merge(&mut self, other: Self) {
1125 let default = DisplayConfig::default();
1126 if other.pages_lookahead != default.pages_lookahead {
1127 self.pages_lookahead = other.pages_lookahead;
1128 }
1129 if other.pages_lookback != default.pages_lookback {
1130 self.pages_lookback = other.pages_lookback;
1131 }
1132 if other.max_buffered_rows != default.max_buffered_rows {
1133 self.max_buffered_rows = other.max_buffered_rows;
1134 }
1135 if other.max_buffered_mb != default.max_buffered_mb {
1136 self.max_buffered_mb = other.max_buffered_mb;
1137 }
1138 if other.row_numbers != default.row_numbers {
1139 self.row_numbers = other.row_numbers;
1140 }
1141 if other.row_start_index != default.row_start_index {
1142 self.row_start_index = other.row_start_index;
1143 }
1144 if other.table_cell_padding != default.table_cell_padding {
1145 self.table_cell_padding = other.table_cell_padding;
1146 }
1147 if other.column_colors != default.column_colors {
1148 self.column_colors = other.column_colors;
1149 }
1150 }
1151}
1152
1153impl PerformanceConfig {
1154 pub fn merge(&mut self, other: Self) {
1155 let default = PerformanceConfig::default();
1156 if other.sampling_threshold != default.sampling_threshold {
1157 self.sampling_threshold = other.sampling_threshold;
1158 }
1159 if other.event_poll_interval_ms != default.event_poll_interval_ms {
1160 self.event_poll_interval_ms = other.event_poll_interval_ms;
1161 }
1162 if other.polars_streaming != default.polars_streaming {
1163 self.polars_streaming = other.polars_streaming;
1164 }
1165 }
1166}
1167
1168impl ThemeConfig {
1169 pub fn merge(&mut self, other: Self) {
1170 self.colors.merge(other.colors);
1171 }
1172}
1173
1174impl ColorConfig {
1175 fn validate(&self, parser: &ColorParser) -> Result<()> {
1177 macro_rules! validate_color {
1179 ($field:expr, $name:expr) => {
1180 parser.parse($field).map_err(|e| {
1181 eyre!(
1182 "theme.colors.{}: {}. Use a valid color name (e.g. red, cyan, bright_red), \
1183 hex (#rrggbb), or indexed(0-255)",
1184 $name,
1185 e
1186 )
1187 })?;
1188 };
1189 }
1190
1191 validate_color!(&self.keybind_hints, "keybind_hints");
1192 validate_color!(&self.keybind_labels, "keybind_labels");
1193 validate_color!(&self.throbber, "throbber");
1194 validate_color!(
1195 &self.primary_chart_series_color,
1196 "primary_chart_series_color"
1197 );
1198 validate_color!(
1199 &self.secondary_chart_series_color,
1200 "secondary_chart_series_color"
1201 );
1202 validate_color!(&self.success, "success");
1203 validate_color!(&self.error, "error");
1204 validate_color!(&self.warning, "warning");
1205 validate_color!(&self.dimmed, "dimmed");
1206 validate_color!(&self.background, "background");
1207 validate_color!(&self.surface, "surface");
1208 validate_color!(&self.controls_bg, "controls_bg");
1209 validate_color!(&self.text_primary, "text_primary");
1210 validate_color!(&self.text_secondary, "text_secondary");
1211 validate_color!(&self.text_inverse, "text_inverse");
1212 validate_color!(&self.table_header, "table_header");
1213 validate_color!(&self.table_header_bg, "table_header_bg");
1214 validate_color!(&self.row_numbers, "row_numbers");
1215 validate_color!(&self.column_separator, "column_separator");
1216 validate_color!(&self.table_selected, "table_selected");
1217 validate_color!(&self.sidebar_border, "sidebar_border");
1218 validate_color!(&self.modal_border_active, "modal_border_active");
1219 validate_color!(&self.modal_border_error, "modal_border_error");
1220 validate_color!(&self.distribution_normal, "distribution_normal");
1221 validate_color!(&self.distribution_skewed, "distribution_skewed");
1222 validate_color!(&self.distribution_other, "distribution_other");
1223 validate_color!(&self.outlier_marker, "outlier_marker");
1224 validate_color!(&self.cursor_focused, "cursor_focused");
1225 validate_color!(&self.cursor_dimmed, "cursor_dimmed");
1226 if self.alternate_row_color != "default" {
1227 validate_color!(&self.alternate_row_color, "alternate_row_color");
1228 }
1229 validate_color!(&self.str_col, "str_col");
1230 validate_color!(&self.int_col, "int_col");
1231 validate_color!(&self.float_col, "float_col");
1232 validate_color!(&self.bool_col, "bool_col");
1233 validate_color!(&self.temporal_col, "temporal_col");
1234 validate_color!(&self.chart_series_color_1, "chart_series_color_1");
1235 validate_color!(&self.chart_series_color_2, "chart_series_color_2");
1236 validate_color!(&self.chart_series_color_3, "chart_series_color_3");
1237 validate_color!(&self.chart_series_color_4, "chart_series_color_4");
1238 validate_color!(&self.chart_series_color_5, "chart_series_color_5");
1239 validate_color!(&self.chart_series_color_6, "chart_series_color_6");
1240 validate_color!(&self.chart_series_color_7, "chart_series_color_7");
1241
1242 Ok(())
1243 }
1244
1245 pub fn merge(&mut self, other: Self) {
1246 let default = ColorConfig::default();
1247
1248 if other.keybind_hints != default.keybind_hints {
1250 self.keybind_hints = other.keybind_hints;
1251 }
1252 if other.keybind_labels != default.keybind_labels {
1253 self.keybind_labels = other.keybind_labels;
1254 }
1255 if other.throbber != default.throbber {
1256 self.throbber = other.throbber;
1257 }
1258 if other.primary_chart_series_color != default.primary_chart_series_color {
1259 self.primary_chart_series_color = other.primary_chart_series_color;
1260 }
1261 if other.secondary_chart_series_color != default.secondary_chart_series_color {
1262 self.secondary_chart_series_color = other.secondary_chart_series_color;
1263 }
1264 if other.success != default.success {
1265 self.success = other.success;
1266 }
1267 if other.error != default.error {
1268 self.error = other.error;
1269 }
1270 if other.warning != default.warning {
1271 self.warning = other.warning;
1272 }
1273 if other.dimmed != default.dimmed {
1274 self.dimmed = other.dimmed;
1275 }
1276 if other.background != default.background {
1277 self.background = other.background;
1278 }
1279 if other.surface != default.surface {
1280 self.surface = other.surface;
1281 }
1282 if other.controls_bg != default.controls_bg {
1283 self.controls_bg = other.controls_bg;
1284 }
1285 if other.text_primary != default.text_primary {
1286 self.text_primary = other.text_primary;
1287 }
1288 if other.text_secondary != default.text_secondary {
1289 self.text_secondary = other.text_secondary;
1290 }
1291 if other.text_inverse != default.text_inverse {
1292 self.text_inverse = other.text_inverse;
1293 }
1294 if other.table_header != default.table_header {
1295 self.table_header = other.table_header;
1296 }
1297 if other.table_header_bg != default.table_header_bg {
1298 self.table_header_bg = other.table_header_bg;
1299 }
1300 if other.row_numbers != default.row_numbers {
1301 self.row_numbers = other.row_numbers;
1302 }
1303 if other.column_separator != default.column_separator {
1304 self.column_separator = other.column_separator;
1305 }
1306 if other.table_selected != default.table_selected {
1307 self.table_selected = other.table_selected;
1308 }
1309 if other.sidebar_border != default.sidebar_border {
1310 self.sidebar_border = other.sidebar_border;
1311 }
1312 if other.modal_border_active != default.modal_border_active {
1313 self.modal_border_active = other.modal_border_active;
1314 }
1315 if other.modal_border_error != default.modal_border_error {
1316 self.modal_border_error = other.modal_border_error;
1317 }
1318 if other.distribution_normal != default.distribution_normal {
1319 self.distribution_normal = other.distribution_normal;
1320 }
1321 if other.distribution_skewed != default.distribution_skewed {
1322 self.distribution_skewed = other.distribution_skewed;
1323 }
1324 if other.distribution_other != default.distribution_other {
1325 self.distribution_other = other.distribution_other;
1326 }
1327 if other.outlier_marker != default.outlier_marker {
1328 self.outlier_marker = other.outlier_marker;
1329 }
1330 if other.cursor_focused != default.cursor_focused {
1331 self.cursor_focused = other.cursor_focused;
1332 }
1333 if other.cursor_dimmed != default.cursor_dimmed {
1334 self.cursor_dimmed = other.cursor_dimmed;
1335 }
1336 if other.alternate_row_color != default.alternate_row_color {
1337 self.alternate_row_color = other.alternate_row_color;
1338 }
1339 if other.str_col != default.str_col {
1340 self.str_col = other.str_col;
1341 }
1342 if other.int_col != default.int_col {
1343 self.int_col = other.int_col;
1344 }
1345 if other.float_col != default.float_col {
1346 self.float_col = other.float_col;
1347 }
1348 if other.bool_col != default.bool_col {
1349 self.bool_col = other.bool_col;
1350 }
1351 if other.temporal_col != default.temporal_col {
1352 self.temporal_col = other.temporal_col;
1353 }
1354 if other.chart_series_color_1 != default.chart_series_color_1 {
1355 self.chart_series_color_1 = other.chart_series_color_1;
1356 }
1357 if other.chart_series_color_2 != default.chart_series_color_2 {
1358 self.chart_series_color_2 = other.chart_series_color_2;
1359 }
1360 if other.chart_series_color_3 != default.chart_series_color_3 {
1361 self.chart_series_color_3 = other.chart_series_color_3;
1362 }
1363 if other.chart_series_color_4 != default.chart_series_color_4 {
1364 self.chart_series_color_4 = other.chart_series_color_4;
1365 }
1366 if other.chart_series_color_5 != default.chart_series_color_5 {
1367 self.chart_series_color_5 = other.chart_series_color_5;
1368 }
1369 if other.chart_series_color_6 != default.chart_series_color_6 {
1370 self.chart_series_color_6 = other.chart_series_color_6;
1371 }
1372 if other.chart_series_color_7 != default.chart_series_color_7 {
1373 self.chart_series_color_7 = other.chart_series_color_7;
1374 }
1375 }
1376}
1377
1378impl UiConfig {
1379 pub fn merge(&mut self, other: Self) {
1380 self.controls.merge(other.controls);
1381 }
1382}
1383
1384impl ControlsConfig {
1385 pub fn merge(&mut self, other: Self) {
1386 if other.custom_controls.is_some() {
1387 self.custom_controls = other.custom_controls;
1388 }
1389 let default = ControlsConfig::default();
1390 if other.row_count_width != default.row_count_width {
1391 self.row_count_width = other.row_count_width;
1392 }
1393 }
1394}
1395
1396impl QueryConfig {
1397 pub fn merge(&mut self, other: Self) {
1398 let default = QueryConfig::default();
1399 if other.history_limit != default.history_limit {
1400 self.history_limit = other.history_limit;
1401 }
1402 if other.enable_history != default.enable_history {
1403 self.enable_history = other.enable_history;
1404 }
1405 }
1406}
1407
1408impl TemplateConfig {
1409 pub fn merge(&mut self, other: Self) {
1410 let default = TemplateConfig::default();
1411 if other.auto_apply != default.auto_apply {
1412 self.auto_apply = other.auto_apply;
1413 }
1414 }
1415}
1416
1417impl DebugConfig {
1418 pub fn merge(&mut self, other: Self) {
1419 let default = DebugConfig::default();
1420 if other.enabled != default.enabled {
1421 self.enabled = other.enabled;
1422 }
1423 if other.show_performance != default.show_performance {
1424 self.show_performance = other.show_performance;
1425 }
1426 if other.show_query != default.show_query {
1427 self.show_query = other.show_query;
1428 }
1429 if other.show_transformations != default.show_transformations {
1430 self.show_transformations = other.show_transformations;
1431 }
1432 }
1433}
1434
1435pub struct ColorParser {
1437 supports_true_color: bool,
1438 supports_256: bool,
1439 no_color: bool,
1440}
1441
1442impl ColorParser {
1443 pub fn new() -> Self {
1445 let no_color = std::env::var("NO_COLOR").is_ok();
1446 let support = supports_color::on(Stream::Stdout);
1447
1448 Self {
1449 supports_true_color: support.as_ref().map(|s| s.has_16m).unwrap_or(false),
1450 supports_256: support.as_ref().map(|s| s.has_256).unwrap_or(false),
1451 no_color,
1452 }
1453 }
1454
1455 pub fn parse(&self, s: &str) -> Result<Color> {
1457 if self.no_color {
1458 return Ok(Color::Reset);
1459 }
1460
1461 let trimmed = s.trim();
1462
1463 if trimmed.starts_with('#') && trimmed.len() == 7 {
1465 let (r, g, b) = parse_hex(trimmed)?;
1466 return Ok(self.convert_rgb_to_terminal_color(r, g, b));
1467 }
1468
1469 if trimmed.to_lowercase().starts_with("indexed(") && trimmed.ends_with(')') {
1471 let num_str = &trimmed[8..trimmed.len() - 1]; let num = num_str.parse::<u8>().map_err(|_| {
1473 eyre!(
1474 "Invalid indexed color: '{}'. Expected format: indexed(0-255)",
1475 trimmed
1476 )
1477 })?;
1478 return Ok(Color::Indexed(num));
1479 }
1480
1481 let lower = trimmed.to_lowercase();
1483 match lower.as_str() {
1484 "black" => Ok(Color::Black),
1486 "red" => Ok(Color::Red),
1487 "green" => Ok(Color::Green),
1488 "yellow" => Ok(Color::Yellow),
1489 "blue" => Ok(Color::Blue),
1490 "magenta" => Ok(Color::Magenta),
1491 "cyan" => Ok(Color::Cyan),
1492 "white" => Ok(Color::White),
1493
1494 "bright_black" | "bright black" => Ok(Color::Indexed(8)),
1496 "bright_red" | "bright red" => Ok(Color::Indexed(9)),
1497 "bright_green" | "bright green" => Ok(Color::Indexed(10)),
1498 "bright_yellow" | "bright yellow" => Ok(Color::Indexed(11)),
1499 "bright_blue" | "bright blue" => Ok(Color::Indexed(12)),
1500 "bright_magenta" | "bright magenta" => Ok(Color::Indexed(13)),
1501 "bright_cyan" | "bright cyan" => Ok(Color::Indexed(14)),
1502 "bright_white" | "bright white" => Ok(Color::Indexed(15)),
1503
1504 "gray" | "grey" => Ok(Color::Indexed(8)),
1506 "dark_gray" | "dark gray" | "dark_grey" | "dark grey" => Ok(Color::Indexed(8)),
1507 "light_gray" | "light gray" | "light_grey" | "light grey" => Ok(Color::Indexed(7)),
1508
1509 "reset" | "default" | "none" | "reversed" => Ok(Color::Reset),
1511
1512 _ => Err(eyre!(
1513 "Unknown color name: '{}'. Supported: basic ANSI colors (red, blue, etc.), \
1514 bright variants (bright_red, etc.), or hex colors (#ff0000)",
1515 trimmed
1516 )),
1517 }
1518 }
1519
1520 fn convert_rgb_to_terminal_color(&self, r: u8, g: u8, b: u8) -> Color {
1522 if self.supports_true_color {
1523 Color::Rgb(r, g, b)
1524 } else if self.supports_256 {
1525 Color::Indexed(rgb_to_256_color(r, g, b))
1526 } else {
1527 rgb_to_basic_ansi(r, g, b)
1528 }
1529 }
1530}
1531
1532impl Default for ColorParser {
1533 fn default() -> Self {
1534 Self::new()
1535 }
1536}
1537
1538fn parse_hex(s: &str) -> Result<(u8, u8, u8)> {
1540 if !s.starts_with('#') || s.len() != 7 {
1541 return Err(eyre!(
1542 "Invalid hex color format: '{}'. Expected format: #rrggbb",
1543 s
1544 ));
1545 }
1546
1547 let r = u8::from_str_radix(&s[1..3], 16)
1548 .map_err(|_| eyre!("Invalid red component in hex color: {}", s))?;
1549 let g = u8::from_str_radix(&s[3..5], 16)
1550 .map_err(|_| eyre!("Invalid green component in hex color: {}", s))?;
1551 let b = u8::from_str_radix(&s[5..7], 16)
1552 .map_err(|_| eyre!("Invalid blue component in hex color: {}", s))?;
1553
1554 Ok((r, g, b))
1555}
1556
1557pub fn rgb_to_256_color(r: u8, g: u8, b: u8) -> u8 {
1560 let max_diff = r.max(g).max(b) as i16 - r.min(g).min(b) as i16;
1562 if max_diff < 10 {
1563 let gray = (r as u16 + g as u16 + b as u16) / 3;
1565 if gray < 8 {
1566 return 16; } else if gray > 247 {
1568 return 231; } else {
1570 return 232 + ((gray - 8) * 24 / 240) as u8;
1571 }
1572 }
1573
1574 let r_idx = (r as u16 * 5 / 255) as u8;
1576 let g_idx = (g as u16 * 5 / 255) as u8;
1577 let b_idx = (b as u16 * 5 / 255) as u8;
1578
1579 16 + 36 * r_idx + 6 * g_idx + b_idx
1580}
1581
1582pub fn rgb_to_basic_ansi(r: u8, g: u8, b: u8) -> Color {
1584 let r_bright = r > 128;
1586 let g_bright = g > 128;
1587 let b_bright = b > 128;
1588
1589 let max_diff = r.max(g).max(b) as i16 - r.min(g).min(b) as i16;
1591 if max_diff < 30 {
1592 let avg = (r as u16 + g as u16 + b as u16) / 3;
1593 return if avg < 64 { Color::Black } else { Color::White };
1594 }
1595
1596 match (r_bright, g_bright, b_bright) {
1598 (false, false, false) => Color::Black,
1599 (true, false, false) => Color::Red,
1600 (false, true, false) => Color::Green,
1601 (true, true, false) => Color::Yellow,
1602 (false, false, true) => Color::Blue,
1603 (true, false, true) => Color::Magenta,
1604 (false, true, true) => Color::Cyan,
1605 (true, true, true) => Color::White,
1606 }
1607}
1608
1609#[derive(Debug, Clone)]
1611pub struct Theme {
1612 pub colors: HashMap<String, Color>,
1613}
1614
1615impl Theme {
1616 pub fn from_config(config: &ThemeConfig) -> Result<Self> {
1618 let parser = ColorParser::new();
1619 let mut colors = HashMap::new();
1620
1621 colors.insert(
1623 "keybind_hints".to_string(),
1624 parser.parse(&config.colors.keybind_hints)?,
1625 );
1626 colors.insert(
1627 "keybind_labels".to_string(),
1628 parser.parse(&config.colors.keybind_labels)?,
1629 );
1630 colors.insert(
1631 "throbber".to_string(),
1632 parser.parse(&config.colors.throbber)?,
1633 );
1634 colors.insert(
1635 "primary_chart_series_color".to_string(),
1636 parser.parse(&config.colors.primary_chart_series_color)?,
1637 );
1638 colors.insert(
1639 "secondary_chart_series_color".to_string(),
1640 parser.parse(&config.colors.secondary_chart_series_color)?,
1641 );
1642 colors.insert("success".to_string(), parser.parse(&config.colors.success)?);
1643 colors.insert("error".to_string(), parser.parse(&config.colors.error)?);
1644 colors.insert("warning".to_string(), parser.parse(&config.colors.warning)?);
1645 colors.insert("dimmed".to_string(), parser.parse(&config.colors.dimmed)?);
1646 colors.insert(
1647 "background".to_string(),
1648 parser.parse(&config.colors.background)?,
1649 );
1650 colors.insert("surface".to_string(), parser.parse(&config.colors.surface)?);
1651 colors.insert(
1652 "controls_bg".to_string(),
1653 parser.parse(&config.colors.controls_bg)?,
1654 );
1655 colors.insert(
1656 "text_primary".to_string(),
1657 parser.parse(&config.colors.text_primary)?,
1658 );
1659 colors.insert(
1660 "text_secondary".to_string(),
1661 parser.parse(&config.colors.text_secondary)?,
1662 );
1663 colors.insert(
1664 "text_inverse".to_string(),
1665 parser.parse(&config.colors.text_inverse)?,
1666 );
1667 colors.insert(
1668 "table_header".to_string(),
1669 parser.parse(&config.colors.table_header)?,
1670 );
1671 colors.insert(
1672 "table_header_bg".to_string(),
1673 parser.parse(&config.colors.table_header_bg)?,
1674 );
1675 colors.insert(
1676 "row_numbers".to_string(),
1677 parser.parse(&config.colors.row_numbers)?,
1678 );
1679 colors.insert(
1680 "column_separator".to_string(),
1681 parser.parse(&config.colors.column_separator)?,
1682 );
1683 colors.insert(
1684 "table_selected".to_string(),
1685 parser.parse(&config.colors.table_selected)?,
1686 );
1687 colors.insert(
1688 "sidebar_border".to_string(),
1689 parser.parse(&config.colors.sidebar_border)?,
1690 );
1691 colors.insert(
1692 "modal_border_active".to_string(),
1693 parser.parse(&config.colors.modal_border_active)?,
1694 );
1695 colors.insert(
1696 "modal_border_error".to_string(),
1697 parser.parse(&config.colors.modal_border_error)?,
1698 );
1699 colors.insert(
1700 "distribution_normal".to_string(),
1701 parser.parse(&config.colors.distribution_normal)?,
1702 );
1703 colors.insert(
1704 "distribution_skewed".to_string(),
1705 parser.parse(&config.colors.distribution_skewed)?,
1706 );
1707 colors.insert(
1708 "distribution_other".to_string(),
1709 parser.parse(&config.colors.distribution_other)?,
1710 );
1711 colors.insert(
1712 "outlier_marker".to_string(),
1713 parser.parse(&config.colors.outlier_marker)?,
1714 );
1715 colors.insert(
1716 "cursor_focused".to_string(),
1717 parser.parse(&config.colors.cursor_focused)?,
1718 );
1719 colors.insert(
1720 "cursor_dimmed".to_string(),
1721 parser.parse(&config.colors.cursor_dimmed)?,
1722 );
1723 if config.colors.alternate_row_color != "default" {
1724 colors.insert(
1725 "alternate_row_color".to_string(),
1726 parser.parse(&config.colors.alternate_row_color)?,
1727 );
1728 }
1729 colors.insert("str_col".to_string(), parser.parse(&config.colors.str_col)?);
1730 colors.insert("int_col".to_string(), parser.parse(&config.colors.int_col)?);
1731 colors.insert(
1732 "float_col".to_string(),
1733 parser.parse(&config.colors.float_col)?,
1734 );
1735 colors.insert(
1736 "bool_col".to_string(),
1737 parser.parse(&config.colors.bool_col)?,
1738 );
1739 colors.insert(
1740 "temporal_col".to_string(),
1741 parser.parse(&config.colors.temporal_col)?,
1742 );
1743 colors.insert(
1744 "chart_series_color_1".to_string(),
1745 parser.parse(&config.colors.chart_series_color_1)?,
1746 );
1747 colors.insert(
1748 "chart_series_color_2".to_string(),
1749 parser.parse(&config.colors.chart_series_color_2)?,
1750 );
1751 colors.insert(
1752 "chart_series_color_3".to_string(),
1753 parser.parse(&config.colors.chart_series_color_3)?,
1754 );
1755 colors.insert(
1756 "chart_series_color_4".to_string(),
1757 parser.parse(&config.colors.chart_series_color_4)?,
1758 );
1759 colors.insert(
1760 "chart_series_color_5".to_string(),
1761 parser.parse(&config.colors.chart_series_color_5)?,
1762 );
1763 colors.insert(
1764 "chart_series_color_6".to_string(),
1765 parser.parse(&config.colors.chart_series_color_6)?,
1766 );
1767 colors.insert(
1768 "chart_series_color_7".to_string(),
1769 parser.parse(&config.colors.chart_series_color_7)?,
1770 );
1771
1772 Ok(Self { colors })
1773 }
1774
1775 pub fn get(&self, name: &str) -> Color {
1777 self.colors.get(name).copied().unwrap_or(Color::Reset)
1778 }
1779
1780 pub fn get_optional(&self, name: &str) -> Option<Color> {
1782 self.colors.get(name).copied()
1783 }
1784}