oxur_cli/config/
terminal.rs1use colored::Colorize;
7use serde::{Deserialize, Serialize};
8
9const DEFAULT_BANNER: &str = include_str!("../../assets/banners/banner0.2.3.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 {
60 pub fn builder() -> TerminalConfigBuilder {
62 TerminalConfigBuilder::new()
63 }
64
65 pub fn formatted_prompt(&self) -> String {
75 if self.color_enabled {
76 if self.prompt.starts_with("oxur") {
78 let rest = &self.prompt[4..]; format!(
81 "{}{}{}{}{}",
82 "o".truecolor(240, 120, 45),
83 "x".truecolor(195, 90, 30),
84 "u".truecolor(135, 60, 15),
85 "r".truecolor(105, 45, 15),
86 rest.truecolor(0, 255, 0),
87 )
88 } else {
89 self.prompt.green().to_string()
90 }
91 } else {
92 self.prompt.clone()
93 }
94 }
95
96 pub fn formatted_continuation_prompt(&self) -> String {
98 if self.color_enabled {
99 self.continuation_prompt.green().to_string()
100 } else {
101 self.continuation_prompt.clone()
102 }
103 }
104
105 pub fn merge(&mut self, other: TerminalConfig) {
107 if other.banner.is_some() {
108 self.banner = other.banner;
109 }
110 self.prompt = other.prompt;
113 self.continuation_prompt = other.continuation_prompt;
114 self.color_enabled = other.color_enabled;
115 self.edit_mode = other.edit_mode;
116 }
117}
118
119#[derive(Debug, Clone)]
121pub struct TerminalConfigBuilder {
122 config: TerminalConfig,
123}
124
125impl TerminalConfigBuilder {
126 pub fn new() -> Self {
128 Self { config: TerminalConfig::default() }
129 }
130
131 pub fn banner(mut self, banner: impl Into<String>) -> Self {
133 self.config.banner = Some(banner.into());
134 self
135 }
136
137 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
139 self.config.prompt = prompt.into();
140 self
141 }
142
143 pub fn continuation_prompt(mut self, prompt: impl Into<String>) -> Self {
145 self.config.continuation_prompt = prompt.into();
146 self
147 }
148
149 pub fn color(mut self, enabled: bool) -> Self {
151 self.config.color_enabled = enabled;
152 self
153 }
154
155 pub fn edit_mode(mut self, mode: EditMode) -> Self {
157 self.config.edit_mode = mode;
158 self
159 }
160
161 pub fn build(self) -> TerminalConfig {
163 self.config
164 }
165}
166
167impl Default for TerminalConfigBuilder {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_default_config() {
179 let config = TerminalConfig::default();
180 assert_eq!(config.prompt, "oxur> ");
181 assert_eq!(config.continuation_prompt, "....> ");
182 assert!(config.color_enabled);
183 assert_eq!(config.edit_mode, EditMode::Emacs);
184 assert!(config.banner.is_some());
185 }
186
187 #[test]
188 fn test_builder() {
189 let config = TerminalConfig::builder()
190 .banner("Welcome!")
191 .prompt("λ> ")
192 .continuation_prompt(" | ")
193 .color(false)
194 .edit_mode(EditMode::Vi)
195 .build();
196
197 assert_eq!(config.banner, Some("Welcome!".to_string()));
198 assert_eq!(config.prompt, "λ> ");
199 assert_eq!(config.continuation_prompt, " | ");
200 assert!(!config.color_enabled);
201 assert_eq!(config.edit_mode, EditMode::Vi);
202 }
203
204 #[test]
205 #[serial_test::serial]
206 fn test_formatted_prompt_with_color() {
207 colored::control::set_override(true);
209
210 let config = TerminalConfig::builder().prompt("test> ").color(true).build();
212 let colored_prompt = config.formatted_prompt();
213 assert_ne!(colored_prompt, "test> ");
215 assert!(colored_prompt.contains("\x1b["));
217 assert!(colored_prompt.contains("test> "));
218
219 colored::control::unset_override();
221 }
222
223 #[test]
224 #[serial_test::serial]
225 fn test_formatted_prompt_oxur_colors() {
226 colored::control::set_override(true);
228
229 let config = TerminalConfig::builder().prompt("oxur> ").color(true).build();
231 let prompt = config.formatted_prompt();
232 assert_ne!(prompt, "oxur> ");
234 assert!(prompt.contains("\x1b["));
236 assert!(prompt.contains("o"));
238 assert!(prompt.contains("x"));
239 assert!(prompt.contains("u"));
240 assert!(prompt.contains("r"));
241 assert!(prompt.contains("> "));
242
243 colored::control::unset_override();
245 }
246
247 #[test]
248 fn test_formatted_prompt_without_color() {
249 let config = TerminalConfig::builder().prompt("test> ").color(false).build();
250 assert_eq!(config.formatted_prompt(), "test> ");
251 }
252
253 #[test]
254 fn test_serde_roundtrip() {
255 let config = TerminalConfig::builder()
256 .banner("Test Banner")
257 .prompt(">>> ")
258 .edit_mode(EditMode::Vi)
259 .build();
260
261 let toml = toml::to_string(&config).unwrap();
262 let parsed: TerminalConfig = toml::from_str(&toml).unwrap();
263
264 assert_eq!(config.banner, parsed.banner);
265 assert_eq!(config.prompt, parsed.prompt);
266 assert_eq!(config.edit_mode, parsed.edit_mode);
267 }
268
269 #[test]
270 fn test_edit_mode_serde() {
271 #[derive(Debug, serde::Deserialize)]
273 struct Wrapper {
274 mode: EditMode,
275 }
276
277 let emacs: Wrapper = toml::from_str("mode = \"emacs\"").unwrap();
278 let vi: Wrapper = toml::from_str("mode = \"vi\"").unwrap();
279
280 assert_eq!(emacs.mode, EditMode::Emacs);
281 assert_eq!(vi.mode, EditMode::Vi);
282 }
283
284 #[test]
285 fn test_default_banner_embedded() {
286 let config = TerminalConfig::default();
287 let banner = config.banner.expect("Default config should have banner");
288
289 assert!(banner.contains("Welcome to"));
291 assert!(banner.contains("oxur:"));
292 assert!(banner.contains("https://oxur.ελ/")); assert!(banner.contains("https://github.com/oxur/oxur/"));
294 assert!(banner.contains("(help)"));
295 assert!(banner.contains("(quit)"));
296
297 assert!(banner.len() > 1000);
300 assert!(banner.len() < 30000);
301 }
302}