oxur_cli/config/
terminal.rs1use colored::Colorize;
7use serde::{Deserialize, Serialize};
8
9const DEFAULT_BANNER: &str = include_str!("../../assets/banners/banner0.1.0.txt");
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(default)]
22pub struct TerminalConfig {
23 pub banner: Option<String>,
25 pub prompt: String,
27 pub continuation_prompt: String,
29 pub color_enabled: bool,
31 pub edit_mode: EditMode,
33}
34
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
37#[serde(rename_all = "lowercase")]
38pub enum EditMode {
39 #[default]
41 Emacs,
42 Vi,
44}
45
46impl Default for TerminalConfig {
47 fn default() -> Self {
48 Self {
49 banner: Some(DEFAULT_BANNER.to_string()),
50 prompt: "oxur> ".to_string(),
51 continuation_prompt: "....> ".to_string(),
52 color_enabled: true,
53 edit_mode: EditMode::Emacs,
54 }
55 }
56}
57
58impl TerminalConfig {
59 pub fn builder() -> TerminalConfigBuilder {
61 TerminalConfigBuilder::new()
62 }
63
64 pub fn formatted_prompt(&self) -> String {
74 if self.color_enabled {
75 if self.prompt.starts_with("oxur") {
77 let rest = &self.prompt[4..]; format!(
80 "{}{}{}{}{}",
81 "o".bright_yellow().bold(),
82 "x".bright_yellow(),
83 "u".yellow(),
84 "r".red(),
85 rest.bright_green()
86 )
87 } else {
88 self.prompt.green().to_string()
89 }
90 } else {
91 self.prompt.clone()
92 }
93 }
94
95 pub fn formatted_continuation_prompt(&self) -> String {
97 if self.color_enabled {
98 self.continuation_prompt.green().to_string()
99 } else {
100 self.continuation_prompt.clone()
101 }
102 }
103
104 pub fn merge(&mut self, other: TerminalConfig) {
106 if other.banner.is_some() {
107 self.banner = other.banner;
108 }
109 self.prompt = other.prompt;
112 self.continuation_prompt = other.continuation_prompt;
113 self.color_enabled = other.color_enabled;
114 self.edit_mode = other.edit_mode;
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct TerminalConfigBuilder {
121 config: TerminalConfig,
122}
123
124impl TerminalConfigBuilder {
125 pub fn new() -> Self {
127 Self { config: TerminalConfig::default() }
128 }
129
130 pub fn banner(mut self, banner: impl Into<String>) -> Self {
132 self.config.banner = Some(banner.into());
133 self
134 }
135
136 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
138 self.config.prompt = prompt.into();
139 self
140 }
141
142 pub fn continuation_prompt(mut self, prompt: impl Into<String>) -> Self {
144 self.config.continuation_prompt = prompt.into();
145 self
146 }
147
148 pub fn color(mut self, enabled: bool) -> Self {
150 self.config.color_enabled = enabled;
151 self
152 }
153
154 pub fn edit_mode(mut self, mode: EditMode) -> Self {
156 self.config.edit_mode = mode;
157 self
158 }
159
160 pub fn build(self) -> TerminalConfig {
162 self.config
163 }
164}
165
166impl Default for TerminalConfigBuilder {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_default_config() {
178 let config = TerminalConfig::default();
179 assert_eq!(config.prompt, "oxur> ");
180 assert_eq!(config.continuation_prompt, "....> ");
181 assert!(config.color_enabled);
182 assert_eq!(config.edit_mode, EditMode::Emacs);
183 assert!(config.banner.is_some());
184 }
185
186 #[test]
187 fn test_builder() {
188 let config = TerminalConfig::builder()
189 .banner("Welcome!")
190 .prompt("λ> ")
191 .continuation_prompt(" | ")
192 .color(false)
193 .edit_mode(EditMode::Vi)
194 .build();
195
196 assert_eq!(config.banner, Some("Welcome!".to_string()));
197 assert_eq!(config.prompt, "λ> ");
198 assert_eq!(config.continuation_prompt, " | ");
199 assert!(!config.color_enabled);
200 assert_eq!(config.edit_mode, EditMode::Vi);
201 }
202
203 #[test]
204 #[serial_test::serial]
205 fn test_formatted_prompt_with_color() {
206 colored::control::set_override(true);
208
209 let config = TerminalConfig::builder().prompt("test> ").color(true).build();
211 let colored_prompt = config.formatted_prompt();
212 assert_ne!(colored_prompt, "test> ");
214 assert!(colored_prompt.contains("\x1b["));
216 assert!(colored_prompt.contains("test> "));
217
218 colored::control::unset_override();
220 }
221
222 #[test]
223 #[serial_test::serial]
224 fn test_formatted_prompt_oxur_colors() {
225 colored::control::set_override(true);
227
228 let config = TerminalConfig::builder().prompt("oxur> ").color(true).build();
230 let prompt = config.formatted_prompt();
231 assert_ne!(prompt, "oxur> ");
233 assert!(prompt.contains("\x1b["));
235 assert!(prompt.contains("o"));
237 assert!(prompt.contains("x"));
238 assert!(prompt.contains("u"));
239 assert!(prompt.contains("r"));
240 assert!(prompt.contains("> "));
241
242 colored::control::unset_override();
244 }
245
246 #[test]
247 fn test_formatted_prompt_without_color() {
248 let config = TerminalConfig::builder().prompt("test> ").color(false).build();
249 assert_eq!(config.formatted_prompt(), "test> ");
250 }
251
252 #[test]
253 fn test_serde_roundtrip() {
254 let config = TerminalConfig::builder()
255 .banner("Test Banner")
256 .prompt(">>> ")
257 .edit_mode(EditMode::Vi)
258 .build();
259
260 let toml = toml::to_string(&config).unwrap();
261 let parsed: TerminalConfig = toml::from_str(&toml).unwrap();
262
263 assert_eq!(config.banner, parsed.banner);
264 assert_eq!(config.prompt, parsed.prompt);
265 assert_eq!(config.edit_mode, parsed.edit_mode);
266 }
267
268 #[test]
269 fn test_edit_mode_serde() {
270 #[derive(Debug, serde::Deserialize)]
272 struct Wrapper {
273 mode: EditMode,
274 }
275
276 let emacs: Wrapper = toml::from_str("mode = \"emacs\"").unwrap();
277 let vi: Wrapper = toml::from_str("mode = \"vi\"").unwrap();
278
279 assert_eq!(emacs.mode, EditMode::Emacs);
280 assert_eq!(vi.mode, EditMode::Vi);
281 }
282
283 #[test]
284 fn test_default_banner_embedded() {
285 let config = TerminalConfig::default();
286 let banner = config.banner.expect("Default config should have banner");
287
288 assert!(banner.contains("Welcome to"));
290 assert!(banner.contains("oxur:"));
291 assert!(banner.contains("http://oxur.li/"));
292 assert!(banner.contains("(help)"));
293 assert!(banner.contains("(quit)"));
294
295 assert!(banner.len() > 1000);
297 assert!(banner.len() < 10000);
298 }
299}