Skip to main content

cossh/config/
schema.rs

1//! Config schema definitions deserialized from YAML.
2//!
3//! This module defines stable user-facing config fields and runtime metadata
4//! attached after parsing.
5
6use crate::highlighter::CompiledHighlightRule;
7use regex::{Regex, RegexSet};
8use serde::{Deserialize, Deserializer};
9use std::{collections::HashMap, path::PathBuf};
10
11/// Main configuration structure loaded from YAML
12#[derive(Debug, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct Config {
15    /// Application-wide settings
16    #[serde(default)]
17    pub settings: Settings,
18    /// Authentication and vault settings.
19    #[serde(default)]
20    pub auth_settings: AuthSettings,
21    /// Interactive session-manager settings (optional block)
22    #[serde(default)]
23    pub interactive_settings: Option<InteractiveSettings>,
24    /// Color palette mapping names to hex codes (converted to ANSI at runtime)
25    pub palette: HashMap<String, String>,
26    /// Syntax highlighting rules
27    pub rules: Vec<HighlightRule>,
28    /// Runtime metadata (not from config file)
29    #[serde(default)]
30    pub metadata: Metadata,
31}
32
33/// Application settings
34#[derive(Debug, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct Settings {
37    /// Regex patterns for secrets to redact from logs
38    #[serde(default)]
39    pub remove_secrets: Option<Vec<String>>,
40    /// Whether to show the ASCII art banner on startup
41    #[serde(default = "default_show_title")]
42    pub show_title: bool,
43    /// Enable debug logging
44    #[serde(default)]
45    pub debug_mode: bool,
46    /// Enable SSH session logging
47    #[serde(default)]
48    pub ssh_logging: bool,
49}
50
51impl Default for Settings {
52    fn default() -> Self {
53        Self {
54            remove_secrets: None,
55            show_title: true,
56            debug_mode: false,
57            ssh_logging: false,
58        }
59    }
60}
61
62/// Authentication settings for the shared password vault.
63#[derive(Debug, Clone, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct AuthSettings {
66    /// Idle timeout in seconds before the unlock agent relocks the vault.
67    #[serde(default = "default_idle_timeout_seconds")]
68    pub idle_timeout_seconds: u64,
69    /// Maximum unlock lifetime in seconds before the agent relocks regardless of activity.
70    #[serde(default = "default_session_timeout_seconds")]
71    pub session_timeout_seconds: u64,
72    /// Whether direct `cossh ssh host` launches should attempt password auto-login.
73    #[serde(default = "default_direct_password_autologin")]
74    pub direct_password_autologin: bool,
75    /// Whether TUI launches should attempt password auto-login.
76    #[serde(default = "default_tui_password_autologin")]
77    pub tui_password_autologin: bool,
78}
79
80impl Default for AuthSettings {
81    fn default() -> Self {
82        Self {
83            idle_timeout_seconds: default_idle_timeout_seconds(),
84            session_timeout_seconds: default_session_timeout_seconds(),
85            direct_password_autologin: default_direct_password_autologin(),
86            tui_password_autologin: default_tui_password_autologin(),
87        }
88    }
89}
90
91/// Interactive-only session manager settings.
92#[derive(Debug, Deserialize, Default)]
93#[serde(deny_unknown_fields)]
94pub struct InteractiveSettings {
95    /// History buffer size (scrollback lines for session manager tabs)
96    #[serde(default = "default_history_buffer")]
97    pub history_buffer: usize,
98    /// Whether host tree folders should start uncollapsed in session manager.
99    /// `false` (default) means the tree starts collapsed.
100    #[serde(default = "default_host_tree_uncollapsed")]
101    pub host_tree_uncollapsed: bool,
102    /// Whether the host info pane is shown by default
103    #[serde(default = "default_info_view")]
104    pub info_view: bool,
105    /// Host panel width as a percentage of terminal width
106    #[serde(default = "default_host_view_size", deserialize_with = "deserialize_host_view_size")]
107    pub host_view_size: u16,
108    /// Host info pane height as a percentage of host panel height
109    #[serde(default = "default_info_view_size", deserialize_with = "deserialize_info_view_size")]
110    pub info_view_size: u16,
111    /// Allow remote OSC 52 clipboard write requests emitted by SSH sessions.
112    /// Disabled by default for safety.
113    #[serde(default = "default_remote_clipboard_write")]
114    pub allow_remote_clipboard_write: bool,
115    /// Maximum clipboard payload size accepted from remote OSC 52 requests.
116    #[serde(default = "default_remote_clipboard_max_bytes", deserialize_with = "deserialize_remote_clipboard_max_bytes")]
117    pub remote_clipboard_max_bytes: usize,
118}
119
120fn default_show_title() -> bool {
121    true
122}
123
124fn default_history_buffer() -> usize {
125    1000
126}
127
128fn default_idle_timeout_seconds() -> u64 {
129    900
130}
131
132fn default_session_timeout_seconds() -> u64 {
133    28_800
134}
135
136fn default_direct_password_autologin() -> bool {
137    true
138}
139
140fn default_tui_password_autologin() -> bool {
141    true
142}
143
144fn default_host_tree_uncollapsed() -> bool {
145    false
146}
147
148fn default_info_view() -> bool {
149    true
150}
151
152fn default_host_view_size() -> u16 {
153    25
154}
155
156fn default_info_view_size() -> u16 {
157    40
158}
159
160fn default_remote_clipboard_write() -> bool {
161    false
162}
163
164fn default_remote_clipboard_max_bytes() -> usize {
165    4096
166}
167
168fn deserialize_host_view_size<'de, D>(deserializer: D) -> Result<u16, D::Error>
169where
170    D: Deserializer<'de>,
171{
172    // Clamp persisted values so invalid config does not break layout math.
173    let value = u16::deserialize(deserializer)?;
174    Ok(value.clamp(10, 70))
175}
176
177fn deserialize_info_view_size<'de, D>(deserializer: D) -> Result<u16, D::Error>
178where
179    D: Deserializer<'de>,
180{
181    // Clamp persisted values so invalid config does not break layout math.
182    let value = u16::deserialize(deserializer)?;
183    Ok(value.clamp(10, 80))
184}
185
186fn deserialize_remote_clipboard_max_bytes<'de, D>(deserializer: D) -> Result<usize, D::Error>
187where
188    D: Deserializer<'de>,
189{
190    // Keep clipboard payload bounds within a safe and practical range.
191    let value = usize::deserialize(deserializer)?;
192    Ok(value.clamp(64, 1_048_576))
193}
194
195/// A single highlight rule mapping a regex pattern to a color
196#[derive(Debug, Deserialize)]
197#[serde(deny_unknown_fields)]
198pub struct HighlightRule {
199    /// Regex pattern to match (will be compiled at config load time)
200    pub regex: String,
201    /// Color name from the palette to apply to matches (foreground)
202    pub color: String,
203    /// Optional user-facing description for this rule (not used by runtime matching)
204    #[serde(default)]
205    pub description: Option<String>,
206    /// Optional background color name from the palette
207    #[serde(default)]
208    pub bg_color: Option<String>,
209}
210
211/// Runtime metadata not stored in config file
212#[derive(Debug, Deserialize, Default)]
213#[serde(deny_unknown_fields)]
214pub struct Metadata {
215    /// Path to the loaded configuration file
216    #[serde(default)]
217    pub config_path: PathBuf,
218    /// Name of the current SSH session (for log file naming)
219    pub session_name: String,
220    /// Compiled regex rules (regex + ANSI color code)
221    #[serde(skip)]
222    pub(crate) compiled_rules: Vec<CompiledHighlightRule>,
223    /// Regex-set prefilter used to cheaply identify rules that might match a chunk.
224    #[serde(skip)]
225    pub compiled_rule_set: Option<RegexSet>,
226    /// Pre-compiled secret redaction patterns
227    #[serde(skip)]
228    pub compiled_secret_patterns: Vec<Regex>,
229    /// Version counter incremented on each config reload
230    #[serde(skip)]
231    pub version: u64,
232}