1use crate::app::AppMode;
49use crate::style::Color;
50
51#[derive(Debug, Clone)]
53pub struct ContextIndicators {
54 pub git_branch: Option<String>,
56 pub project_name: Option<String>,
58 pub mode: AppMode,
60 pub provider: Option<String>,
62 pub model: Option<String>,
64}
65
66impl ContextIndicators {
67 pub fn new() -> Self {
69 Self {
70 git_branch: None,
71 project_name: None,
72 mode: AppMode::Chat,
73 provider: None,
74 model: None,
75 }
76 }
77
78 pub fn with_git_branch(mut self, branch: impl Into<String>) -> Self {
80 self.git_branch = Some(branch.into());
81 self
82 }
83
84 pub fn with_project_name(mut self, name: impl Into<String>) -> Self {
86 self.project_name = Some(name.into());
87 self
88 }
89
90 pub fn with_provider(mut self, provider: impl Into<String>, model: impl Into<String>) -> Self {
92 self.provider = Some(provider.into());
93 self.model = Some(model.into());
94 self
95 }
96
97 pub fn format(&self) -> String {
99 let mut parts = Vec::new();
100
101 if let Some(branch) = &self.git_branch {
102 parts.push(format!("({})", branch));
103 }
104
105 if let Some(project) = &self.project_name {
106 parts.push(project.clone());
107 }
108
109 let mode_str = match self.mode {
110 AppMode::Chat => "💬",
111 AppMode::Command => "⚙️",
112 AppMode::Diff => "📝",
113 AppMode::Help => "❓",
114 };
115 parts.push(mode_str.to_string());
116
117 if let (Some(provider), Some(model)) = (&self.provider, &self.model) {
118 parts.push(format!("[{}/{}]", provider, model));
119 }
120
121 parts.join(" ")
122 }
123}
124
125impl Default for ContextIndicators {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct PromptConfig {
134 pub prefix: String,
136 pub suffix: String,
138 pub fg_color: Color,
140 pub bg_color: Option<Color>,
142 pub show_context: bool,
144}
145
146impl PromptConfig {
147 pub fn new() -> Self {
149 Self {
150 prefix: "❯".to_string(),
151 suffix: " ".to_string(),
152 fg_color: Color::new(0, 122, 255),
153 bg_color: None,
154 show_context: true,
155 }
156 }
157
158 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
160 self.prefix = prefix.into();
161 self
162 }
163
164 pub fn with_suffix(mut self, suffix: impl Into<String>) -> Self {
166 self.suffix = suffix.into();
167 self
168 }
169
170 pub fn with_fg_color(mut self, color: Color) -> Self {
172 self.fg_color = color;
173 self
174 }
175
176 pub fn with_bg_color(mut self, color: Color) -> Self {
178 self.bg_color = Some(color);
179 self
180 }
181
182 pub fn with_show_context(mut self, show: bool) -> Self {
184 self.show_context = show;
185 self
186 }
187}
188
189impl Default for PromptConfig {
190 fn default() -> Self {
191 Self::new()
192 }
193}
194
195pub struct PromptWidget {
197 pub input: String,
199 pub cursor: usize,
201 pub context: ContextIndicators,
203 pub config: PromptConfig,
205 pub history: Vec<String>,
207 pub history_index: Option<usize>,
209}
210
211impl PromptWidget {
212 pub fn new() -> Self {
214 Self {
215 input: String::new(),
216 cursor: 0,
217 context: ContextIndicators::new(),
218 config: PromptConfig::new(),
219 history: Vec::new(),
220 history_index: None,
221 }
222 }
223
224 pub fn insert_char(&mut self, ch: char) {
226 self.input.insert(self.cursor, ch);
227 self.cursor += 1;
228 }
229
230 pub fn backspace(&mut self) {
232 if self.cursor > 0 {
233 self.input.remove(self.cursor - 1);
234 self.cursor -= 1;
235 }
236 }
237
238 pub fn delete(&mut self) {
240 if self.cursor < self.input.len() {
241 self.input.remove(self.cursor);
242 }
243 }
244
245 pub fn move_left(&mut self) {
247 if self.cursor > 0 {
248 self.cursor -= 1;
249 }
250 }
251
252 pub fn move_right(&mut self) {
254 if self.cursor < self.input.len() {
255 self.cursor += 1;
256 }
257 }
258
259 pub fn move_start(&mut self) {
261 self.cursor = 0;
262 }
263
264 pub fn move_end(&mut self) {
266 self.cursor = self.input.len();
267 }
268
269 pub fn submit(&mut self) -> String {
271 let input = self.input.clone();
272 self.history.push(input.clone());
273 self.input.clear();
274 self.cursor = 0;
275 self.history_index = None;
276 input
277 }
278
279 pub fn history_up(&mut self) {
281 if self.history.is_empty() {
282 return;
283 }
284
285 match self.history_index {
286 None => {
287 self.history_index = Some(self.history.len() - 1);
288 self.input = self.history[self.history.len() - 1].clone();
289 }
290 Some(idx) if idx > 0 => {
291 self.history_index = Some(idx - 1);
292 self.input = self.history[idx - 1].clone();
293 }
294 _ => {}
295 }
296
297 self.cursor = self.input.len();
298 }
299
300 pub fn history_down(&mut self) {
302 match self.history_index {
303 Some(idx) if idx < self.history.len() - 1 => {
304 self.history_index = Some(idx + 1);
305 self.input = self.history[idx + 1].clone();
306 self.cursor = self.input.len();
307 }
308 Some(_) => {
309 self.history_index = None;
310 self.input.clear();
311 self.cursor = 0;
312 }
313 None => {}
314 }
315 }
316
317 pub fn format_prompt(&self) -> String {
319 let mut prompt = String::new();
320
321 if self.config.show_context {
322 prompt.push_str(&self.context.format());
323 prompt.push(' ');
324 }
325
326 prompt.push_str(&self.config.prefix);
327 prompt.push_str(&self.config.suffix);
328
329 prompt
330 }
331
332 pub fn display_line(&self) -> String {
334 format!("{}{}", self.format_prompt(), self.input)
335 }
336}
337
338impl Default for PromptWidget {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_context_indicators() {
350 let context = ContextIndicators::new()
351 .with_git_branch("main")
352 .with_project_name("ricecoder")
353 .with_provider("openai", "gpt-4");
354
355 let formatted = context.format();
356 assert!(formatted.contains("main"));
357 assert!(formatted.contains("ricecoder"));
358 assert!(formatted.contains("openai"));
359 }
360
361 #[test]
362 fn test_prompt_config() {
363 let config = PromptConfig::new().with_prefix("$").with_suffix(" ");
364
365 assert_eq!(config.prefix, "$");
366 assert_eq!(config.suffix, " ");
367 }
368
369 #[test]
370 fn test_prompt_widget_creation() {
371 let widget = PromptWidget::new();
372 assert!(widget.input.is_empty());
373 assert_eq!(widget.cursor, 0);
374 }
375
376 #[test]
377 fn test_prompt_widget_input() {
378 let mut widget = PromptWidget::new();
379 widget.insert_char('h');
380 widget.insert_char('i');
381
382 assert_eq!(widget.input, "hi");
383 assert_eq!(widget.cursor, 2);
384 }
385
386 #[test]
387 fn test_prompt_widget_backspace() {
388 let mut widget = PromptWidget::new();
389 widget.input = "hello".to_string();
390 widget.cursor = 5;
391
392 widget.backspace();
393 assert_eq!(widget.input, "hell");
394 assert_eq!(widget.cursor, 4);
395 }
396
397 #[test]
398 fn test_prompt_widget_cursor_movement() {
399 let mut widget = PromptWidget::new();
400 widget.input = "hello".to_string();
401 widget.cursor = 2;
402
403 widget.move_left();
404 assert_eq!(widget.cursor, 1);
405
406 widget.move_right();
407 assert_eq!(widget.cursor, 2);
408
409 widget.move_start();
410 assert_eq!(widget.cursor, 0);
411
412 widget.move_end();
413 assert_eq!(widget.cursor, 5);
414 }
415
416 #[test]
417 fn test_prompt_widget_submit() {
418 let mut widget = PromptWidget::new();
419 widget.input = "test command".to_string();
420
421 let submitted = widget.submit();
422 assert_eq!(submitted, "test command");
423 assert!(widget.input.is_empty());
424 assert_eq!(widget.history.len(), 1);
425 }
426
427 #[test]
428 fn test_prompt_widget_history() {
429 let mut widget = PromptWidget::new();
430
431 widget.input = "first".to_string();
432 widget.submit();
433
434 widget.input = "second".to_string();
435 widget.submit();
436
437 widget.history_up();
438 assert_eq!(widget.input, "second");
439
440 widget.history_up();
441 assert_eq!(widget.input, "first");
442
443 widget.history_down();
444 assert_eq!(widget.input, "second");
445
446 widget.history_down();
447 assert!(widget.input.is_empty());
448 }
449
450 #[test]
451 fn test_prompt_formatting() {
452 let mut widget = PromptWidget::new();
453 widget.context = ContextIndicators::new()
454 .with_git_branch("main")
455 .with_project_name("ricecoder");
456
457 let prompt = widget.format_prompt();
458 assert!(prompt.contains("main"));
459 assert!(prompt.contains("ricecoder"));
460 assert!(prompt.contains("❯"));
461 }
462
463 #[test]
464 fn test_display_line() {
465 let mut widget = PromptWidget::new();
466 widget.input = "hello".to_string();
467
468 let display = widget.display_line();
469 assert!(display.contains("hello"));
470 assert!(display.contains("❯"));
471 }
472}