Skip to main content

rtcom_config/
profile.rs

1//! `Profile` struct and TOML-persisted sub-sections for rtcom settings.
2
3use serde::{Deserialize, Serialize};
4
5/// Top-level rtcom profile persisted to TOML.
6///
7/// Unknown TOML keys are silently ignored (serde default), and missing leaf
8/// fields within a declared section fall back to the section's [`Default`]
9/// impl — so hand-edited profiles with partial overrides keep working across
10/// rtcom versions that add fields.
11#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
12pub struct Profile {
13    /// Serial-port settings (baud, framing, flow control).
14    #[serde(default)]
15    pub serial: SerialSection,
16    /// Line-ending translation (CR/LF) on input, output, and echo paths.
17    #[serde(default)]
18    pub line_endings: LineEndingsSection,
19    /// Modem control line startup policy.
20    #[serde(default)]
21    pub modem: ModemSection,
22    /// Screen / TUI rendering preferences.
23    #[serde(default)]
24    pub screen: ScreenSection,
25}
26
27/// Serial-port settings.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(default)]
30pub struct SerialSection {
31    /// Baud rate in bits per second (e.g. `115_200`).
32    pub baud: u32,
33    /// Number of data bits per frame (5..=8).
34    pub data_bits: u8,
35    /// Number of stop bits (1 or 2).
36    pub stop_bits: u8,
37    /// Parity: `none`, `even`, `odd`, `mark`, or `space`.
38    pub parity: String,
39    /// Flow control: `none`, `hw` (RTS/CTS), or `sw` (XON/XOFF).
40    pub flow: String,
41}
42
43impl Default for SerialSection {
44    fn default() -> Self {
45        Self {
46            baud: 115_200,
47            data_bits: 8,
48            stop_bits: 1,
49            parity: "none".into(),
50            flow: "none".into(),
51        }
52    }
53}
54
55/// Line-ending mappers.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(default)]
58pub struct LineEndingsSection {
59    /// Output map applied to bytes sent to the device.
60    pub omap: String,
61    /// Input map applied to bytes received from the device.
62    pub imap: String,
63    /// Echo map applied to locally-echoed bytes.
64    pub emap: String,
65}
66
67impl Default for LineEndingsSection {
68    fn default() -> Self {
69        Self {
70            omap: "none".into(),
71            imap: "none".into(),
72            emap: "none".into(),
73        }
74    }
75}
76
77/// Modem control line startup policy.
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(default)]
80pub struct ModemSection {
81    /// Initial DTR state: `unchanged`, `raise`, or `lower`.
82    pub initial_dtr: String,
83    /// Initial RTS state: `unchanged`, `raise`, or `lower`.
84    pub initial_rts: String,
85}
86
87impl Default for ModemSection {
88    fn default() -> Self {
89        Self {
90            initial_dtr: "unchanged".into(),
91            initial_rts: "unchanged".into(),
92        }
93    }
94}
95
96/// Screen / TUI rendering preferences.
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(default)]
99pub struct ScreenSection {
100    /// How modal dialogs render over the terminal stream.
101    pub modal_style: ModalStyle,
102    /// Number of scrollback rows retained in the TUI buffer.
103    pub scrollback_rows: usize,
104    /// Lines scrolled per mouse-wheel notch in the serial pane.
105    ///
106    /// Values less than 1 are treated as 1 at runtime so the wheel
107    /// always has a visible effect. v0.2 has no menu control for this
108    /// — hand-edit the TOML to change it; a menu-editable control is
109    /// deferred to v0.2.1.
110    pub wheel_scroll_lines: u16,
111}
112
113impl Default for ScreenSection {
114    fn default() -> Self {
115        Self {
116            modal_style: ModalStyle::Overlay,
117            scrollback_rows: 10_000,
118            wheel_scroll_lines: 3,
119        }
120    }
121}
122
123/// How modal dialogs (menus, prompts) render over the terminal stream.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
125#[serde(rename_all = "kebab-case")]
126pub enum ModalStyle {
127    /// Draw on top of the live stream without altering its contents.
128    #[default]
129    Overlay,
130    /// Overlay with the background stream dimmed.
131    DimmedOverlay,
132    /// Take over the full terminal while active.
133    Fullscreen,
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn profile_default_values() {
142        let p = Profile::default();
143        assert_eq!(p.serial.baud, 115_200);
144        assert_eq!(p.serial.data_bits, 8);
145        assert_eq!(p.screen.modal_style, ModalStyle::Overlay);
146        assert_eq!(p.screen.scrollback_rows, 10_000);
147        assert_eq!(p.screen.wheel_scroll_lines, 3);
148    }
149
150    #[test]
151    fn profile_partial_screen_section_keeps_wheel_default() {
152        // Simulate an existing profile written before T24 — the file
153        // has modal_style but no wheel_scroll_lines. Serde's
154        // `#[serde(default)]` must fall back to the section default
155        // without failing to parse.
156        let partial = r#"
157            [screen]
158            modal_style = "fullscreen"
159        "#;
160        let parsed: Profile = toml::from_str(partial).expect("parse");
161        assert_eq!(parsed.screen.modal_style, ModalStyle::Fullscreen);
162        assert_eq!(parsed.screen.wheel_scroll_lines, 3);
163    }
164
165    #[test]
166    fn profile_roundtrip_toml() {
167        let original = Profile::default();
168        let serialized = toml::to_string(&original).expect("serialize");
169        let parsed: Profile = toml::from_str(&serialized).expect("parse");
170        assert_eq!(parsed, original);
171    }
172
173    #[test]
174    fn profile_unknown_keys_are_dropped() {
175        let with_unknown = r#"
176            [serial]
177            baud = 9600
178            unknown_field = "ignored"
179            data_bits = 8
180            stop_bits = 1
181            parity = "none"
182            flow = "none"
183        "#;
184        let parsed: Profile = toml::from_str(with_unknown).expect("parse");
185        assert_eq!(parsed.serial.baud, 9600);
186    }
187
188    #[test]
189    fn profile_partial_section_uses_defaults_for_missing_leaf_fields() {
190        let partial = r"
191            [serial]
192            baud = 9600
193        ";
194        let parsed: Profile = toml::from_str(partial).expect("parse");
195        assert_eq!(parsed.serial.baud, 9600);
196        assert_eq!(parsed.serial.data_bits, 8); // default preserved
197        assert_eq!(parsed.serial.parity, "none"); // default preserved
198    }
199}