sql_cli/config/
config.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(default)]
9#[derive(Default)]
10pub struct Config {
11    pub display: DisplayConfig,
12    pub keybindings: KeybindingConfig,
13    pub behavior: BehaviorConfig,
14    pub theme: ThemeConfig,
15    pub redis_cache: RedisCacheConfig,
16    pub web: WebConfig,
17    pub tokens: TokenConfig,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(default)]
22pub struct DisplayConfig {
23    /// Use Unicode/Nerd Font glyphs for icons
24    pub use_glyphs: bool,
25
26    /// Show row numbers by default
27    pub show_row_numbers: bool,
28
29    /// Compact mode by default
30    pub compact_mode: bool,
31
32    /// Icons for different states (can be overridden)
33    pub icons: IconConfig,
34
35    /// Show key press indicator by default
36    pub show_key_indicator: bool,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(default)]
41pub struct IconConfig {
42    pub pin: String,
43    pub lock: String,
44    pub cache: String,
45    pub file: String,
46    pub database: String,
47    pub api: String,
48    pub case_insensitive: String,
49    pub warning: String,
50    pub error: String,
51    pub info: String,
52    pub success: String,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(default)]
57pub struct KeybindingConfig {
58    /// Whether to use vim-style keybindings
59    pub vim_mode: bool,
60
61    /// Custom key mappings (future expansion)
62    /// Format: "action" -> "`key_sequence`"
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub custom_mappings: Option<std::collections::HashMap<String, String>>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(default)]
69pub struct BehaviorConfig {
70    /// Auto-execute SELECT * when loading CSV/JSON
71    pub auto_execute_on_load: bool,
72
73    /// Case-insensitive by default
74    pub case_insensitive_default: bool,
75
76    /// Start mode when loading files: "command" or "results"
77    pub start_mode: String,
78
79    /// Maximum rows to display without pagination warning
80    pub max_display_rows: usize,
81
82    /// Default cache directory
83    pub cache_dir: Option<PathBuf>,
84
85    /// Enable query history
86    pub enable_history: bool,
87
88    /// Maximum history entries
89    pub max_history_entries: usize,
90
91    /// Automatically hide empty/null columns on data load
92    pub hide_empty_columns: bool,
93
94    /// Default date notation: "us" (MM/DD/YYYY) or "european" (DD/MM/YYYY)
95    /// This determines how ambiguous dates like 04/09/2025 are interpreted
96    pub default_date_notation: String,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(default)]
101pub struct ThemeConfig {
102    /// Color scheme: "default", "dark", "light", "solarized"
103    pub color_scheme: String,
104
105    /// Rainbow parentheses
106    pub rainbow_parentheses: bool,
107
108    /// Syntax highlighting
109    pub syntax_highlighting: bool,
110
111    /// Cell selection style
112    pub cell_selection_style: CellSelectionStyle,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(default)]
117pub struct CellSelectionStyle {
118    /// Style mode: "underline", "block", "border", "corners", "subtle"
119    pub mode: String,
120
121    /// Foreground color for selected cell (e.g., "yellow", "orange", "cyan")
122    pub foreground: String,
123
124    /// Whether to use background color
125    pub use_background: bool,
126
127    /// Background color if `use_background` is true
128    pub background: String,
129
130    /// Whether to bold the text
131    pub bold: bool,
132
133    /// Whether to underline the text (legacy, use mode instead)
134    pub underline: bool,
135
136    /// Border style for "border" mode: "single", "double", "rounded", "thick"
137    pub border_style: String,
138
139    /// Whether to show cell corners in "corners" mode
140    pub corner_chars: String, // e.g., "┌┐└┘" or "╭╮╰╯" for rounded
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(default)]
145pub struct RedisCacheConfig {
146    /// Whether cache is enabled (can be overridden by SQL_CLI_CACHE env var)
147    pub enabled: bool,
148
149    /// Redis URL (can be overridden by SQL_CLI_REDIS_URL env var)
150    pub redis_url: String,
151
152    /// Default cache duration in seconds when CACHE is specified without duration
153    pub default_duration: u64,
154
155    /// Cache durations by URL pattern (glob patterns supported)
156    pub duration_rules: HashMap<String, u64>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(default)]
161pub struct WebConfig {
162    /// Default timeout for web requests in seconds
163    pub timeout: u64,
164
165    /// Maximum response size in MB
166    pub max_response_size: usize,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(default)]
171pub struct TokenConfig {
172    /// Token definitions with their refresh commands
173    /// Key: Token variable name (e.g., "JWT_TOKEN", "JWT_TOKEN_PROD")
174    /// Value: TokenDefinition with refresh command
175    pub tokens: HashMap<String, TokenDefinition>,
176
177    /// Auto-refresh tokens before they expire (if expiry is known)
178    pub auto_refresh: bool,
179
180    /// Default token lifetime in seconds (if not specified per token)
181    pub default_lifetime: u64,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct TokenDefinition {
186    /// Command to run to get/refresh this token
187    pub refresh_command: String,
188
189    /// Optional description
190    pub description: Option<String>,
191
192    /// Token lifetime in seconds (overrides default)
193    pub lifetime: Option<u64>,
194
195    /// Last refresh timestamp (managed by the system)
196    #[serde(skip_serializing, skip_deserializing)]
197    pub last_refreshed: Option<std::time::SystemTime>,
198}
199
200impl Default for DisplayConfig {
201    fn default() -> Self {
202        Self {
203            use_glyphs: true, // Default to glyphs, can be disabled
204            show_row_numbers: false,
205            compact_mode: false,
206            icons: IconConfig::default(),
207            show_key_indicator: true, // Default on for better debugging
208        }
209    }
210}
211
212impl Default for IconConfig {
213    fn default() -> Self {
214        Self {
215            // Default to Unicode/Nerd Font icons
216            pin: "📌".to_string(),
217            lock: "🔒".to_string(),
218            cache: "📦".to_string(),
219            file: "📁".to_string(),
220            database: "🗄️".to_string(),
221            api: "🌐".to_string(),
222            case_insensitive: "Ⓘ".to_string(),
223            warning: "⚠️".to_string(),
224            error: "❌".to_string(),
225            info: "ℹ️".to_string(),
226            success: "✅".to_string(),
227        }
228    }
229}
230
231impl IconConfig {
232    /// Get simple ASCII alternatives for terminals without glyph support
233    #[must_use]
234    pub fn simple() -> Self {
235        Self {
236            pin: "[P]".to_string(),
237            lock: "[L]".to_string(),
238            cache: "[C]".to_string(),
239            file: "[F]".to_string(),
240            database: "[DB]".to_string(),
241            api: "[API]".to_string(),
242            case_insensitive: "[i]".to_string(),
243            warning: "[!]".to_string(),
244            error: "[X]".to_string(),
245            info: "[i]".to_string(),
246            success: "[OK]".to_string(),
247        }
248    }
249}
250
251impl Default for KeybindingConfig {
252    fn default() -> Self {
253        Self {
254            vim_mode: true,
255            custom_mappings: None,
256        }
257    }
258}
259
260impl Default for BehaviorConfig {
261    fn default() -> Self {
262        Self {
263            auto_execute_on_load: true,
264            case_insensitive_default: true, // Default to case-insensitive for practical use
265            start_mode: "results".to_string(), // Default to results mode for immediate data view
266            max_display_rows: 10000,
267            cache_dir: None,
268            enable_history: true,
269            max_history_entries: 1000,
270            hide_empty_columns: false, // Default to false to avoid hiding data unexpectedly
271            default_date_notation: "us".to_string(), // Default to US format (MM/DD/YYYY)
272        }
273    }
274}
275
276impl Default for ThemeConfig {
277    fn default() -> Self {
278        Self {
279            color_scheme: "default".to_string(),
280            rainbow_parentheses: true,
281            syntax_highlighting: true,
282            cell_selection_style: CellSelectionStyle::default(),
283        }
284    }
285}
286
287impl Default for CellSelectionStyle {
288    fn default() -> Self {
289        Self {
290            mode: "underline".to_string(), // Default to current behavior
291            foreground: "yellow".to_string(),
292            use_background: false,
293            background: "cyan".to_string(),
294            bold: true,
295            underline: true, // Keep for backward compatibility
296            border_style: "single".to_string(),
297            corner_chars: "┌┐└┘".to_string(),
298        }
299    }
300}
301
302impl Default for RedisCacheConfig {
303    fn default() -> Self {
304        Self {
305            enabled: false,
306            redis_url: "redis://127.0.0.1:6379".to_string(),
307            default_duration: 600, // 10 minutes default
308            duration_rules: HashMap::new(),
309        }
310    }
311}
312
313impl Default for WebConfig {
314    fn default() -> Self {
315        Self {
316            timeout: 30,
317            max_response_size: 100,
318        }
319    }
320}
321
322impl Default for TokenConfig {
323    fn default() -> Self {
324        Self {
325            tokens: HashMap::new(),
326            auto_refresh: false,
327            default_lifetime: 3600, // 1 hour default
328        }
329    }
330}
331
332/// Simple glob pattern matching
333fn glob_match(pattern: &str, text: &str) -> bool {
334    // Very simple glob matching for * wildcard
335    if pattern.contains('*') {
336        let parts: Vec<&str> = pattern.split('*').collect();
337        if parts.is_empty() {
338            return true;
339        }
340
341        let mut pos = 0;
342        for (i, part) in parts.iter().enumerate() {
343            if part.is_empty() {
344                continue;
345            }
346
347            // First part must match at beginning
348            if i == 0 && !text.starts_with(part) {
349                return false;
350            }
351            // Last part must match at end
352            else if i == parts.len() - 1 && !text.ends_with(part) {
353                return false;
354            }
355            // Middle parts must be found
356            else if let Some(idx) = text[pos..].find(part) {
357                pos += idx + part.len();
358            } else {
359                return false;
360            }
361        }
362        true
363    } else {
364        text.contains(pattern)
365    }
366}
367
368impl Config {
369    /// Generate debug info string for display
370    #[must_use]
371    pub fn debug_info(&self) -> String {
372        let mut info = String::new();
373        info.push_str("\n========== CONFIGURATION ==========\n");
374
375        // Display configuration
376        info.push_str("[display]\n");
377        info.push_str(&format!("  use_glyphs = {}\n", self.display.use_glyphs));
378        info.push_str(&format!(
379            "  show_row_numbers = {}\n",
380            self.display.show_row_numbers
381        ));
382        info.push_str(&format!("  compact_mode = {}\n", self.display.compact_mode));
383        info.push_str(&format!(
384            "  show_key_indicator = {}\n",
385            self.display.show_key_indicator
386        ));
387
388        // Behavior configuration
389        info.push_str("\n[behavior]\n");
390        info.push_str(&format!(
391            "  auto_execute_on_load = {}\n",
392            self.behavior.auto_execute_on_load
393        ));
394        info.push_str(&format!(
395            "  case_insensitive_default = {}\n",
396            self.behavior.case_insensitive_default
397        ));
398        info.push_str(&format!(
399            "  start_mode = \"{}\"\n",
400            self.behavior.start_mode
401        ));
402        info.push_str(&format!(
403            "  max_display_rows = {}\n",
404            self.behavior.max_display_rows
405        ));
406        info.push_str(&format!(
407            "  enable_history = {}\n",
408            self.behavior.enable_history
409        ));
410        info.push_str(&format!(
411            "  max_history_entries = {}\n",
412            self.behavior.max_history_entries
413        ));
414        info.push_str(&format!(
415            "  hide_empty_columns = {}\n",
416            self.behavior.hide_empty_columns
417        ));
418        info.push_str(&format!(
419            "  default_date_notation = \"{}\"\n",
420            self.behavior.default_date_notation
421        ));
422
423        // Keybindings configuration
424        info.push_str("\n[keybindings]\n");
425        info.push_str(&format!("  vim_mode = {}\n", self.keybindings.vim_mode));
426
427        // Theme configuration
428        info.push_str("\n[theme]\n");
429        info.push_str(&format!("  color_scheme = {}\n", self.theme.color_scheme));
430        info.push_str(&format!(
431            "  rainbow_parentheses = {}\n",
432            self.theme.rainbow_parentheses
433        ));
434        info.push_str(&format!(
435            "  syntax_highlighting = {}\n",
436            self.theme.syntax_highlighting
437        ));
438        info.push_str(&format!(
439            "  cell_selection_style = {}\n",
440            self.theme.cell_selection_style.mode
441        ));
442
443        // Redis cache configuration
444        info.push_str("\n[redis_cache]\n");
445        info.push_str(&format!("  enabled = {}\n", self.redis_cache.enabled));
446        info.push_str(&format!("  redis_url = {}\n", self.redis_cache.redis_url));
447        info.push_str(&format!(
448            "  default_duration = {}s\n",
449            self.redis_cache.default_duration
450        ));
451        info.push_str(&format!(
452            "  duration_rules = {} patterns\n",
453            self.redis_cache.duration_rules.len()
454        ));
455
456        // Web configuration
457        info.push_str("\n[web]\n");
458        info.push_str(&format!("  timeout = {}s\n", self.web.timeout));
459        info.push_str(&format!(
460            "  max_response_size = {} MB\n",
461            self.web.max_response_size
462        ));
463
464        // Token configuration
465        info.push_str("\n[tokens]\n");
466        info.push_str(&format!("  auto_refresh = {}\n", self.tokens.auto_refresh));
467        info.push_str(&format!(
468            "  default_lifetime = {}s\n",
469            self.tokens.default_lifetime
470        ));
471        info.push_str(&format!(
472            "  configured_tokens = {} tokens\n",
473            self.tokens.tokens.len()
474        ));
475        for (name, _) in &self.tokens.tokens {
476            info.push_str(&format!("    - {}\n", name));
477        }
478
479        info.push_str("==========================================\n");
480        info
481    }
482
483    /// Load config from the default location
484    pub fn load() -> Result<Self> {
485        let config_path = Self::get_config_path()?;
486
487        if !config_path.exists() {
488            // Create default config if it doesn't exist
489            let default_config = Self::default();
490            default_config.save()?;
491            return Ok(default_config.with_env_overrides());
492        }
493
494        let contents = fs::read_to_string(&config_path)?;
495        let config: Config = toml::from_str(&contents)?;
496
497        // Apply simple mode if glyphs are disabled
498        let mut config = config;
499        if !config.display.use_glyphs {
500            config.display.icons = IconConfig::simple();
501        }
502
503        Ok(config.with_env_overrides())
504    }
505
506    /// Apply environment variable overrides
507    fn with_env_overrides(mut self) -> Self {
508        // Override cache enabled from env
509        if let Ok(val) = std::env::var("SQL_CLI_CACHE") {
510            self.redis_cache.enabled =
511                val.eq_ignore_ascii_case("true") || val.eq_ignore_ascii_case("yes") || val == "1";
512        }
513
514        // Override Redis URL from env
515        if let Ok(url) = std::env::var("SQL_CLI_REDIS_URL") {
516            self.redis_cache.redis_url = url;
517        }
518
519        // Override default cache duration from env
520        if let Ok(duration) = std::env::var("SQL_CLI_CACHE_DEFAULT_DURATION") {
521            if let Ok(seconds) = duration.parse::<u64>() {
522                self.redis_cache.default_duration = seconds;
523            }
524        }
525
526        self
527    }
528
529    /// Get cache duration for a URL, checking rules first then falling back to default
530    pub fn get_cache_duration(&self, url: &str) -> u64 {
531        // Check if any rule matches the URL
532        for (pattern, duration) in &self.redis_cache.duration_rules {
533            if url.contains(pattern) || glob_match(pattern, url) {
534                return *duration;
535            }
536        }
537
538        // Fall back to default
539        self.redis_cache.default_duration
540    }
541
542    /// Save config to the default location
543    pub fn save(&self) -> Result<()> {
544        let config_path = Self::get_config_path()?;
545
546        // Ensure parent directory exists
547        if let Some(parent) = config_path.parent() {
548            fs::create_dir_all(parent)?;
549        }
550
551        let contents = toml::to_string_pretty(self)?;
552        fs::write(&config_path, contents)?;
553
554        Ok(())
555    }
556
557    /// Get the default config file path
558    pub fn get_config_path() -> Result<PathBuf> {
559        let config_dir = dirs::config_dir()
560            .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?;
561
562        Ok(config_dir.join("sql-cli").join("config.toml"))
563    }
564
565    /// Create a default config file with comments
566    #[must_use]
567    pub fn create_default_with_comments() -> String {
568        r#"# SQL CLI Configuration File
569# Location: ~/.config/sql-cli/config.toml (Linux/macOS)
570#           %APPDATA%\sql-cli\config.toml (Windows)
571
572[display]
573# Use Unicode/Nerd Font glyphs for icons
574# Set to false for ASCII-only mode (better compatibility)
575use_glyphs = true
576
577# Show row numbers by default in results view
578show_row_numbers = false
579
580# Use compact mode by default (less padding, more data visible)
581compact_mode = false
582
583# Show key press indicator on status line (useful for debugging)
584show_key_indicator = true
585
586# Icon configuration
587# These are automatically set to ASCII when use_glyphs = false
588[display.icons]
589pin = "📌"
590lock = "🔒"
591cache = "📦"
592file = "📁"
593database = "🗄️"
594api = "🌐"
595case_insensitive = "Ⓘ"
596warning = "⚠️"
597error = "❌"
598info = "ℹ️"
599success = "✅"
600
601[keybindings]
602# Use vim-style keybindings (j/k navigation, yy to yank, etc.)
603vim_mode = true
604
605# Custom key mappings (future feature)
606# [keybindings.custom_mappings]
607# "copy_row" = "ctrl+c"
608# "paste" = "ctrl+v"
609
610[behavior]
611# Automatically execute SELECT * when loading CSV/JSON files
612auto_execute_on_load = true
613
614# Use case-insensitive string comparisons by default (recommended for practical use)
615case_insensitive_default = true
616
617# Start mode when loading files: "command" or "results"
618# - "command": Start in command mode (focus on SQL input)
619# - "results": Start in results mode (focus on data, press 'i' to edit query)
620start_mode = "results"
621
622# Maximum rows to display without warning
623max_display_rows = 10000
624
625# Cache directory (leave commented to use default)
626# cache_dir = "/path/to/cache"
627
628# Enable query history
629enable_history = true
630
631# Maximum number of history entries to keep
632max_history_entries = 1000
633
634# Automatically hide empty/null columns when data is loaded (can be toggled with 'E' key)
635hide_empty_columns = false
636
637# Default date notation for parsing ambiguous dates
638# "us" = MM/DD/YYYY format (e.g., 04/09/2025 = April 9, 2025)
639# "european" = DD/MM/YYYY format (e.g., 04/09/2025 = September 4, 2025)
640default_date_notation = "us"
641
642[theme]
643# Color scheme: "default", "dark", "light", "solarized"
644color_scheme = "default"
645
646# Enable rainbow parentheses in SQL queries
647rainbow_parentheses = true
648
649# Enable syntax highlighting
650syntax_highlighting = true
651
652# Cell selection highlighting style (for cell mode)
653[theme.cell_selection_style]
654# Foreground color: "yellow", "red", "green", "blue", "magenta", "cyan", "white"
655foreground = "yellow"
656
657# Whether to change background color (can be hard to read with some color schemes)
658use_background = false
659
660# Background color if use_background is true
661background = "cyan"
662
663# Text styling
664bold = true
665underline = true
666
667[redis_cache]
668# Enable Redis cache (can be overridden by SQL_CLI_CACHE env var)
669enabled = false
670
671# Redis connection URL (can be overridden by SQL_CLI_REDIS_URL env var)
672redis_url = "redis://127.0.0.1:6379"
673
674# Default cache duration in seconds when CACHE is specified without a value
675# or when no CACHE directive is present in the query
676default_duration = 600  # 10 minutes
677
678# Cache duration rules based on URL patterns
679# Pattern matching uses simple glob syntax (* for wildcards)
680# These override the default_duration for matching URLs
681[redis_cache.duration_rules]
682# Production APIs - cache for 1 hour
683"*.bloomberg.com/*" = 3600
684"*prod*" = 3600
685"*production*" = 3600
686
687# Staging/UAT - cache for 5 minutes
688"*staging*" = 300
689"*uat*" = 300
690
691# Historical data endpoints - cache for 24 hours
692"*/historical/*" = 86400
693"*/archive/*" = 86400
694"*/trades/20*" = 43200  # Yesterday's trades - 12 hours
695
696# Real-time/volatile data - short cache
697"*/realtime/*" = 60
698"*/live/*" = 30
699"*/prices/*" = 120
700
701# Specific endpoints
702"api.barclays.com/trades" = 7200  # 2 hours for Barclays trades
703"api.jpmorgan.com/fx" = 1800      # 30 minutes for JPM FX
704
705[web]
706# Default timeout for web requests in seconds
707timeout = 30
708
709# Maximum response size in MB
710max_response_size = 100
711
712[tokens]
713# Auto-refresh tokens before they expire
714auto_refresh = false
715
716# Default token lifetime in seconds (1 hour)
717default_lifetime = 3600
718
719# Token definitions with their refresh commands
720# Each token needs a refresh_command that outputs the token to stdout
721[tokens.tokens.JWT_TOKEN]
722description = "UAT environment JWT token"
723refresh_command = "~/.config/sql-cli/get_uat_token.sh"
724lifetime = 3600  # 1 hour
725
726[tokens.tokens.JWT_TOKEN_PROD]
727description = "Production environment JWT token"
728refresh_command = "~/.config/sql-cli/get_prod_token.sh"
729lifetime = 7200  # 2 hours
730
731# Example: Azure CLI token
732# [tokens.tokens.AZURE_TOKEN]
733# description = "Azure access token"
734# refresh_command = "az account get-access-token --resource https://api.example.com --query accessToken -o tsv"
735# lifetime = 3600
736"#
737        .to_string()
738    }
739
740    /// Initialize config with a setup wizard
741    pub fn init_wizard() -> Result<Self> {
742        println!("SQL CLI Configuration Setup");
743        println!("============================");
744
745        // Ask about glyph support
746        print!("Does your terminal support Unicode/Nerd Font icons? (y/n) [y]: ");
747        std::io::Write::flush(&mut std::io::stdout())?;
748        let mut input = String::new();
749        std::io::stdin().read_line(&mut input)?;
750        let use_glyphs = !input.trim().eq_ignore_ascii_case("n");
751
752        let mut config = Config::default();
753        config.display.use_glyphs = use_glyphs;
754        if !use_glyphs {
755            config.display.icons = IconConfig::simple();
756        }
757
758        // Ask about vim mode
759        print!("Enable vim-style keybindings? (y/n) [y]: ");
760        std::io::Write::flush(&mut std::io::stdout())?;
761        input.clear();
762        std::io::stdin().read_line(&mut input)?;
763        config.keybindings.vim_mode = !input.trim().eq_ignore_ascii_case("n");
764
765        config.save()?;
766
767        println!("\nConfiguration saved to: {:?}", Config::get_config_path()?);
768        println!("You can edit this file directly to customize further.");
769
770        Ok(config)
771    }
772}
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777
778    #[test]
779    fn test_default_config() {
780        let config = Config::default();
781        assert!(config.display.use_glyphs);
782        assert!(config.keybindings.vim_mode);
783    }
784
785    #[test]
786    fn test_simple_icons() {
787        let icons = IconConfig::simple();
788        assert_eq!(icons.pin, "[P]");
789        assert_eq!(icons.lock, "[L]");
790    }
791
792    #[test]
793    fn test_config_serialization() {
794        let config = Config::default();
795        let toml_str = toml::to_string(&config).unwrap();
796        let parsed: Config = toml::from_str(&toml_str).unwrap();
797        assert_eq!(config.display.use_glyphs, parsed.display.use_glyphs);
798    }
799}