1use crate::action::{Action, ResponseType, RuntimeCommand};
2use crate::model::panel_state::{CommandPanelState, CommandType, InputState};
3use crate::ui::strings::UIStrings;
4use ratatui::style::Modifier;
5use std::time::Instant;
6
7pub struct CommandParser;
9
10impl CommandParser {
11 pub fn parse_command(state: &mut CommandPanelState, command: &str) -> Vec<Action> {
13 let cmd = command.trim();
14
15 if cmd == "help" {
17 let plain = Self::format_help_message();
18 let styled = Self::format_help_message_styled();
19 return vec![Action::AddResponseWithStyle {
20 content: plain,
21 styled_lines: Some(styled),
22 response_type: ResponseType::Info,
23 }];
24 }
25 if cmd == "help srcpath" {
26 let plain = Self::format_srcpath_help();
27 let styled = Self::format_srcpath_help_styled();
28 return vec![Action::AddResponseWithStyle {
29 content: plain,
30 styled_lines: Some(styled),
31 response_type: ResponseType::Info,
32 }];
33 }
34
35 if let Some(actions) = Self::parse_ui_command(cmd) {
37 return actions;
38 }
39
40 if let Some(actions) = Self::parse_sync_command(state, cmd) {
42 return actions;
43 }
44
45 if cmd.starts_with("trace ") {
47 return vec![Action::EnterScriptMode(cmd.to_string())];
48 }
49 if cmd.starts_with("t ") {
50 let target = cmd.strip_prefix("t ").unwrap();
52 let full_command = format!("trace {target}");
53 return vec![Action::EnterScriptMode(full_command)];
54 }
55
56 if let Some(actions) = Self::parse_shortcut_command(state, cmd) {
58 return actions;
59 }
60
61 if let Some(actions) = Self::parse_info_command(state, cmd) {
63 return actions;
64 }
65
66 if let Some(actions) = Self::parse_save_command(state, cmd) {
68 return actions;
69 }
70
71 if let Some(actions) = Self::parse_stop_command(cmd) {
73 return actions;
74 }
75
76 if let Some(actions) = Self::parse_source_command(state, cmd) {
78 return actions;
79 }
80
81 if let Some(actions) = Self::parse_srcpath_command(state, cmd) {
83 return actions;
84 }
85
86 if cmd == "quit" || cmd == "exit" {
88 return vec![Action::Quit];
89 }
90
91 if cmd == "clear" {
93 state.command_history.clear();
95 let plain = "✅ Command history cleared.".to_string();
96 let styled = vec![
97 crate::components::command_panel::style_builder::StyledLineBuilder::new()
98 .styled(
99 plain.clone(),
100 crate::components::command_panel::style_builder::StylePresets::SUCCESS,
101 )
102 .build(),
103 ];
104 return vec![Action::AddResponseWithStyle {
105 content: plain,
106 styled_lines: Some(styled),
107 response_type: ResponseType::Info,
108 }];
109 }
110
111 {
113 let plain = format!("{} {}", UIStrings::ERROR_PREFIX, UIStrings::UNKNOWN_COMMAND);
114 let styled =
115 crate::components::command_panel::ResponseFormatter::style_generic_message_lines(
116 &plain,
117 );
118 vec![Action::AddResponseWithStyle {
119 content: plain,
120 styled_lines: Some(styled),
121 response_type: ResponseType::Error,
122 }]
123 }
124 }
125
126 fn format_srcpath_help() -> String {
128 [
129 "📘 Source Path Command - Detailed Help",
130 "",
131 "The 'srcpath' command helps resolve source files when DWARF debug info contains",
132 "compilation-time paths that differ from runtime paths (e.g., compiled on CI server).",
133 "",
134 "Commands:",
135 " srcpath - Show current path mappings and search directories",
136 " srcpath map <from> <to> - Map DWARF compilation directory to local path (⭐ Recommended)",
137 " srcpath add <dir> - Add search directory (fallback, non-recursive)",
138 " srcpath remove <path> - Remove a mapping or search directory",
139 " srcpath clear - Clear all runtime rules (keep config file rules)",
140 " srcpath reset - Reset to config file rules only",
141 "",
142 "Resolution Strategy:",
143 " 1. Try exact path from DWARF",
144 " 2. Apply path substitutions (runtime rules first, then config file)",
145 " 3. Search by filename in additional directories (root only, non-recursive)",
146 "",
147 "⭐ Recommended Usage:",
148 " Use 'srcpath map' to map DWARF compilation directory:",
149 " srcpath map /home/build/nginx-1.27.1 /home/user/nginx-1.27.1",
150 " This maps ALL relative paths automatically.",
151 "",
152 "Examples:",
153 " srcpath map /build/project /home/user/project # Map compilation directory",
154 " srcpath add /usr/local/include # Add search directory (fallback)",
155 " srcpath remove /build/project # Remove a rule",
156 "",
157 "Configuration:",
158 " Rules can be persisted in config.toml under [source] section.",
159 " Runtime rules (via commands) take priority over config file rules.",
160 "",
161 "💡 Tip: Check file loading errors for 'DWARF Directory', then map it directly.",
162 " Type 'help srcpath' for more details.",
163 ]
164 .join("\n")
165 }
166
167 fn format_help_message() -> String {
169 format!(
170 "📘 Ghostscope Commands:\n\n{}\n\n{}\n\n{}\n\n{}\n\n{}\n\n{}\n\n{}",
171 Self::format_tracing_commands(),
172 Self::format_info_commands(),
173 Self::format_srcpath_commands(),
174 Self::format_ui_commands(),
175 Self::format_control_commands(),
176 Self::format_navigation_commands(),
177 Self::format_general_commands()
178 )
179 }
180
181 fn format_help_message_styled() -> Vec<ratatui::text::Line<'static>> {
183 use crate::components::command_panel::style_builder::StyledLineBuilder;
184 use ratatui::text::Line;
185
186 let mut lines = Vec::new();
187 lines.push(
189 StyledLineBuilder::new()
190 .title("📘 Ghostscope Commands:")
191 .build(),
192 );
193 lines.push(Line::from(""));
194
195 lines.extend(Self::format_section_styled(&Self::format_tracing_commands()));
197 lines.push(Line::from(""));
198 lines.extend(Self::format_section_styled(&Self::format_info_commands()));
199 lines.push(Line::from(""));
200 lines.extend(Self::format_section_styled(&Self::format_srcpath_commands()));
201 lines.push(Line::from(""));
202 lines.extend(Self::format_section_styled(&Self::format_ui_commands()));
203 lines.push(Line::from(""));
204 lines.extend(Self::format_section_styled(&Self::format_control_commands()));
205 lines.push(Line::from(""));
206 lines.extend(Self::format_section_styled(
207 &Self::format_navigation_commands(),
208 ));
209 lines.push(Line::from(""));
210 lines.extend(Self::format_section_styled(&Self::format_general_commands()));
211 lines
212 }
213
214 fn format_section_styled(section_text: &str) -> Vec<ratatui::text::Line<'static>> {
216 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
217 let mut out = Vec::new();
218 for raw in section_text.lines() {
219 if raw.is_empty() {
220 out.push(ratatui::text::Line::from(""));
221 continue;
222 }
223 if matches!(
224 raw.chars().next(),
225 Some('📊' | '🔍' | '🗂' | '⚙' | '🧭' | '🔧' | '🖥')
226 ) {
227 out.push(
228 StyledLineBuilder::new()
229 .styled(raw, StylePresets::SECTION)
230 .build(),
231 );
232 continue;
233 }
234 if raw.starts_with(" ") {
235 out.push(Self::build_help_command_line(raw));
236 continue;
237 }
238 if raw.contains("💡") {
239 out.push(
240 StyledLineBuilder::new()
241 .styled(raw, StylePresets::TIP)
242 .build(),
243 );
244 continue;
245 }
246 out.push(StyledLineBuilder::new().value(raw).build());
247 }
248 out
249 }
250
251 fn format_srcpath_help_styled() -> Vec<ratatui::text::Line<'static>> {
253 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
254 use ratatui::text::Line;
255
256 let mut lines = Vec::new();
257 lines.push(
258 StyledLineBuilder::new()
259 .title("📘 Source Path Command - Detailed Help")
260 .build(),
261 );
262 lines.push(Line::from(""));
263
264 for raw in Self::format_srcpath_help().lines() {
265 if raw.is_empty() {
266 lines.push(Line::from(""));
267 continue;
268 }
269 if raw.starts_with("📘") {
270 continue; }
272 if matches!(
273 raw,
274 "Commands:"
275 | "Resolution Strategy:"
276 | "⭐ Recommended Usage:"
277 | "Examples:"
278 | "Configuration:"
279 ) {
280 lines.push(
281 StyledLineBuilder::new()
282 .styled(raw, StylePresets::SECTION)
283 .build(),
284 );
285 continue;
286 }
287 if raw.starts_with(" ") {
288 lines.push(Self::build_help_command_line(raw));
289 continue;
290 }
291 if raw.contains("💡") {
292 lines.push(
293 StyledLineBuilder::new()
294 .styled(raw, StylePresets::TIP)
295 .build(),
296 );
297 continue;
298 }
299 lines.push(StyledLineBuilder::new().value(raw).build());
300 }
301
302 lines
303 }
304
305 fn format_info_help_styled() -> Vec<ratatui::text::Line<'static>> {
307 use crate::components::command_panel::style_builder::{StylePresets, StyledLineBuilder};
308 let mut lines = Vec::new();
309 for raw in Self::format_info_help().lines() {
310 if raw.is_empty() {
311 lines.push(ratatui::text::Line::from(""));
312 continue;
313 }
314 if raw.starts_with("🔍 Info") {
315 lines.push(StyledLineBuilder::new().title(raw).build());
316 continue;
317 }
318 if raw.starts_with(" ") {
319 lines.push(Self::build_help_command_line(raw));
320 continue;
321 }
322 if raw.contains("💡") {
323 lines.push(
324 StyledLineBuilder::new()
325 .styled(raw, StylePresets::TIP)
326 .build(),
327 );
328 continue;
329 }
330 lines.push(StyledLineBuilder::new().value(raw).build());
331 }
332 lines
333 }
334
335 fn build_help_command_line(line: &str) -> ratatui::text::Line<'static> {
337 use ratatui::{
338 style::{Color, Style},
339 text::{Line, Span},
340 };
341
342 let mut spans: Vec<Span<'static>> = Vec::new();
344 let trimmed = line.trim_start();
345 let leading = line.len() - trimmed.len();
346 if leading > 0 {
347 spans.push(Span::raw(" ".repeat(leading)));
348 }
349
350 let (cmd_part, desc_part) = match trimmed.find(" - ") {
352 Some(pos) => (&trimmed[..pos], Some(&trimmed[pos..])),
353 None => (trimmed, None),
354 };
355
356 let mut current = String::new();
358 let mut in_param = false;
359 for ch in cmd_part.chars() {
360 match ch {
361 '<' => {
362 if !current.is_empty() {
363 spans.push(Span::styled(
364 current.clone(),
365 Style::default()
366 .fg(Color::White)
367 .add_modifier(Modifier::BOLD),
368 ));
369 current.clear();
370 }
371 in_param = true;
372 current.push('<');
373 }
374 '>' => {
375 current.push('>');
376 spans.push(Span::styled(
377 current.clone(),
378 Style::default().fg(Color::Yellow),
379 ));
380 current.clear();
381 in_param = false;
382 }
383 _ => current.push(ch),
384 }
385 }
386 if !current.is_empty() {
387 if in_param {
388 spans.push(Span::styled(current, Style::default().fg(Color::Yellow)));
389 } else {
390 spans.push(Span::styled(
391 current,
392 Style::default()
393 .fg(Color::White)
394 .add_modifier(Modifier::BOLD),
395 ));
396 }
397 }
398
399 if let Some(desc) = desc_part {
401 spans.push(Span::styled(
402 desc.to_string(),
403 Style::default().fg(Color::DarkGray),
404 ));
405 }
406
407 Line::from(spans)
408 }
409
410 fn styled_usage(usage: &str) -> Vec<ratatui::text::Line<'static>> {
412 use ratatui::{
413 style::{Color, Style},
414 text::{Line, Span},
415 };
416
417 let mut spans: Vec<Span<'static>> = Vec::new();
418 let trimmed = usage.trim_start();
419 let leading = usage.len() - trimmed.len();
421 if leading > 0 {
422 spans.push(Span::raw(" ".repeat(leading)));
423 }
424
425 let prefix = "Usage:";
426 if let Some(rest) = trimmed.strip_prefix(prefix) {
427 spans.push(Span::styled(
428 prefix.to_string(),
429 Style::default().fg(Color::Cyan),
430 ));
431 spans.push(Span::raw(" "));
432 let mut current = String::new();
434 let mut in_param = false;
435 for ch in rest.chars() {
436 match ch {
437 '<' => {
438 if !current.is_empty() {
439 spans.push(Span::styled(
440 current.clone(),
441 Style::default().fg(Color::White),
442 ));
443 current.clear();
444 }
445 in_param = true;
446 current.push('<');
447 }
448 '>' => {
449 current.push('>');
450 spans.push(Span::styled(
451 current.clone(),
452 Style::default().fg(Color::Yellow),
453 ));
454 current.clear();
455 in_param = false;
456 }
457 _ => current.push(ch),
458 }
459 }
460 if !current.is_empty() {
461 let style = if in_param {
462 Style::default().fg(Color::Yellow)
463 } else {
464 Style::default().fg(Color::White)
465 };
466 spans.push(Span::styled(current, style));
467 }
468 } else {
469 spans.push(Span::styled(
471 trimmed.to_string(),
472 Style::default().fg(Color::White),
473 ));
474 }
475
476 vec![Line::from(spans)]
477 }
478
479 fn format_tracing_commands() -> String {
481 [
482 "📊 Tracing Commands:",
483 " trace <target> - Start tracing a function/line/address (t)",
484 " - target: function_name | file:line | 0xADDR | module_suffix:0xADDR",
485 " enable <id|all> - Enable specific trace or all traces (en)",
486 " disable <id|all> - Disable specific trace or all traces (dis)",
487 " delete <id|all> - Delete specific trace or all traces (del)",
488 " save traces [file] - Save all traces to file (s t)",
489 " save traces enabled [file] - Save only enabled traces",
490 " save traces disabled [file]- Save only disabled traces",
491 " save output [file] - Start realtime eBPF output logging (s o)",
492 " save session [file] - Start realtime session logging (s s)",
493 " stop output - Stop realtime eBPF output logging",
494 " stop session - Stop realtime session logging",
495 " source <file> - Load traces from file (s)",
496 ]
497 .join("\n")
498 }
499
500 fn format_info_commands() -> String {
502 [
503 "🔍 Information Commands:",
504 " info - Show available info commands",
505 " info file - Show executable file info and sections (i f, i file)",
506 " info trace [id] - Show trace status (i t [id])",
507 " info source - Show all source files (i s)",
508 " info share - Show shared libraries WITH debug info (i sh)",
509 " info share all - Show ALL loaded shared libraries (i sh all)",
510 " info function <name> [verbose|v] - Show debug info for function (i f <name> [v])",
511 " info line <file:line> [verbose|v] - Show debug info for line (i l <file:line> [v])",
512 " info address <addr> [verbose|v] - Show debug info for address (i a <addr> [v]) [TODO]",
513 ]
514 .join("\n")
515 }
516
517 fn format_srcpath_commands() -> String {
519 [
520 "🗂️ Source Path Commands:",
521 " srcpath - Show current path mappings and search directories",
522 " srcpath map <from> <to> - Map DWARF compilation directory (⭐ Recommended)",
523 " srcpath add <dir> - Add search directory (fallback, non-recursive)",
524 " srcpath remove <path> - Remove a mapping or search directory",
525 " srcpath clear - Clear all runtime rules",
526 " srcpath reset - Reset to config file rules",
527 "",
528 " 💡 Tip: Use 'help srcpath' for detailed usage and best practices",
529 ]
530 .join("\n")
531 }
532
533 fn format_control_commands() -> String {
535 [
536 "⚙️ Control Commands:",
537 " clear - Clear command history",
538 " quit, exit - Exit ghostscope",
539 ]
540 .join("\n")
541 }
542
543 fn format_ui_commands() -> String {
545 [
546 "🖥 UI Controls:",
547 " ui source on - Enable source panel",
548 " ui source off - Disable source panel (keep eBPF & command panels)",
549 "",
550 " 💡 No source available? Use 'ui source off', or start with --no-source-panel,",
551 " or set [ui].show_source_panel=false in config.",
552 ]
553 .join("\n")
554 }
555
556 fn format_navigation_commands() -> String {
558 [
559 "🧭 Navigation & Input:",
560 "Input Mode:",
561 " Tab - Command completion",
562 " Right/Ctrl+E - Accept auto-suggestion",
563 " Ctrl+P/N - Navigate command history",
564 " Ctrl+A/E - Move to beginning/end of line (emacs)",
565 " Ctrl+B/F - Move cursor left/right (emacs)",
566 " Ctrl+H - Delete previous character (emacs)",
567 " Ctrl+W - Delete previous word (emacs)",
568 "",
569 "Command Mode (vim-style):",
570 " jk/Escape - Enter command mode",
571 " hjkl - Navigate (left/down/up/right)",
572 " i - Return to input mode",
573 ]
574 .join("\n")
575 }
576
577 fn format_general_commands() -> String {
579 [
580 "🔧 General:",
581 " help - Show this help message",
582 "",
583 "💡 Input: Tab=completion, Right/Ctrl+E=auto-suggest, emacs keys | Command: jk/Esc enter, i exit, hjkl move",
584 ]
585 .join("\n")
586 }
587
588 pub fn get_command_completion(input: &str) -> Option<String> {
590 let input = input.trim();
591
592 if let Some(verbose_completion) = Self::complete_verbose_parameter(input) {
595 return Some(verbose_completion);
596 }
597
598 let commands = [
600 "trace",
602 "enable",
603 "disable",
604 "delete",
605 "save",
606 "source",
607 "ui",
608 "info",
609 "help",
610 "clear",
611 "quit",
612 "exit",
613 "srcpath",
614 "t",
616 "en",
617 "dis",
618 "del",
619 "save traces",
621 "save traces enabled",
622 "save traces disabled",
623 "save output",
624 "save session",
625 "stop output",
627 "stop session",
628 "ui source on",
630 "ui source off",
631 "info file",
633 "info trace",
634 "info source",
635 "info share",
636 "info share all",
637 "info function",
638 "info line",
639 "info address",
640 "srcpath",
642 "srcpath add",
643 "srcpath map",
644 "srcpath remove",
645 "srcpath clear",
646 "srcpath reset",
647 "i",
649 "i file",
650 "i s",
651 "i sh",
652 "i sh all",
653 "i t",
654 "i f",
655 "i l",
656 "i a",
657 "s t", "s o", "s s", ];
661
662 let matches: Vec<&str> = commands
664 .iter()
665 .filter(|cmd| cmd.starts_with(input) && cmd.len() > input.len())
666 .cloned()
667 .collect();
668
669 match matches.len() {
670 0 => None, 1 => {
672 let full_command = matches[0];
674 Some(full_command[input.len()..].to_string())
675 }
676 _ => {
677 Self::find_common_prefix(&matches, input.len())
679 }
680 }
681 }
682
683 fn complete_verbose_parameter(input: &str) -> Option<String> {
686 let parts: Vec<&str> = input.split_whitespace().collect();
688
689 if parts.len() < 3 {
691 return None;
692 }
693
694 let is_info_cmd = matches!(
696 (parts[0], parts.get(1)),
697 ("info", Some(&"function"))
698 | ("info", Some(&"line"))
699 | ("info", Some(&"address"))
700 | ("i", Some(&"f"))
701 | ("i", Some(&"l"))
702 | ("i", Some(&"a"))
703 );
704
705 if !is_info_cmd {
706 return None;
707 }
708
709 let last_part = parts.last()?;
711
712 if *last_part == "verbose" || *last_part == "v" {
714 return None;
715 }
716
717 if "verbose".starts_with(last_part) && last_part.len() < "verbose".len() {
719 let remaining = &"verbose"[last_part.len()..];
721 return Some(remaining.to_string());
722 }
723
724 None
725 }
726
727 fn find_common_prefix(matches: &[&str], input_len: usize) -> Option<String> {
729 if matches.is_empty() {
730 return None;
731 }
732
733 let first = &matches[0][input_len..];
734 let mut common_len = first.len();
735
736 for &cmd in &matches[1..] {
737 let suffix = &cmd[input_len..];
738 common_len = first
739 .chars()
740 .zip(suffix.chars())
741 .take_while(|(a, b)| a == b)
742 .count()
743 .min(common_len);
744 }
745
746 if common_len > 0 {
747 let common_prefix = &first[..common_len];
748 if common_prefix.trim().is_empty() {
750 None
751 } else {
752 Some(common_prefix.to_string())
753 }
754 } else {
755 None
756 }
757 }
758
759 fn parse_sync_command(state: &mut CommandPanelState, command: &str) -> Option<Vec<Action>> {
761 if command.starts_with("disable ") {
763 let target = command.strip_prefix("disable ").unwrap().trim();
764 return Some(Self::parse_disable_command(state, target));
765 }
766 if command.starts_with("dis ") {
767 let target = command.strip_prefix("dis ").unwrap().trim();
768 return Some(Self::parse_disable_command(state, target));
769 }
770
771 if command.starts_with("enable ") {
773 let target = command.strip_prefix("enable ").unwrap().trim();
774 return Some(Self::parse_enable_command(state, target));
775 }
776 if command.starts_with("en ") {
777 let target = command.strip_prefix("en ").unwrap().trim();
778 return Some(Self::parse_enable_command(state, target));
779 }
780
781 if command.starts_with("delete ") {
783 let target = command.strip_prefix("delete ").unwrap().trim();
784 return Some(Self::parse_delete_command(state, target));
785 }
786 if command.starts_with("del ") {
787 let target = command.strip_prefix("del ").unwrap().trim();
788 return Some(Self::parse_delete_command(state, target));
789 }
790
791 None
792 }
793
794 fn parse_disable_command(state: &mut CommandPanelState, target: &str) -> Vec<Action> {
796 if target == "all" {
797 state.input_state = InputState::WaitingResponse {
798 command: format!("disable {target}"),
799 sent_time: Instant::now(),
800 command_type: CommandType::DisableAll,
801 };
802 vec![Action::SendRuntimeCommand(RuntimeCommand::DisableAllTraces)]
803 } else if let Ok(trace_id) = target.parse::<u32>() {
804 state.input_state = InputState::WaitingResponse {
805 command: format!("disable {target}"),
806 sent_time: Instant::now(),
807 command_type: CommandType::Disable { trace_id },
808 };
809 vec![Action::SendRuntimeCommand(RuntimeCommand::DisableTrace(
810 trace_id,
811 ))]
812 } else {
813 let plain = "Usage: disable <trace_id|all>".to_string();
814 let styled = Self::styled_usage(&plain);
815 vec![Action::AddResponseWithStyle {
816 content: plain,
817 styled_lines: Some(styled),
818 response_type: ResponseType::Error,
819 }]
820 }
821 }
822
823 fn parse_enable_command(state: &mut CommandPanelState, target: &str) -> Vec<Action> {
825 if target == "all" {
826 state.input_state = InputState::WaitingResponse {
827 command: format!("enable {target}"),
828 sent_time: Instant::now(),
829 command_type: CommandType::EnableAll,
830 };
831 vec![Action::SendRuntimeCommand(RuntimeCommand::EnableAllTraces)]
832 } else if let Ok(trace_id) = target.parse::<u32>() {
833 state.input_state = InputState::WaitingResponse {
834 command: format!("enable {target}"),
835 sent_time: Instant::now(),
836 command_type: CommandType::Enable { trace_id },
837 };
838 vec![Action::SendRuntimeCommand(RuntimeCommand::EnableTrace(
839 trace_id,
840 ))]
841 } else {
842 let plain = "Usage: enable <trace_id|all>".to_string();
843 let styled = Self::styled_usage(&plain);
844 vec![Action::AddResponseWithStyle {
845 content: plain,
846 styled_lines: Some(styled),
847 response_type: ResponseType::Error,
848 }]
849 }
850 }
851
852 fn parse_delete_command(state: &mut CommandPanelState, target: &str) -> Vec<Action> {
854 if target == "all" {
855 state.input_state = InputState::WaitingResponse {
856 command: format!("delete {target}"),
857 sent_time: Instant::now(),
858 command_type: CommandType::DeleteAll,
859 };
860 vec![Action::SendRuntimeCommand(RuntimeCommand::DeleteAllTraces)]
861 } else if let Ok(trace_id) = target.parse::<u32>() {
862 state.input_state = InputState::WaitingResponse {
863 command: format!("delete {target}"),
864 sent_time: Instant::now(),
865 command_type: CommandType::Delete { trace_id },
866 };
867 vec![Action::SendRuntimeCommand(RuntimeCommand::DeleteTrace(
868 trace_id,
869 ))]
870 } else {
871 let plain = "Usage: delete <trace_id|all>".to_string();
872 let styled = Self::styled_usage(&plain);
873 vec![Action::AddResponseWithStyle {
874 content: plain,
875 styled_lines: Some(styled),
876 response_type: ResponseType::Error,
877 }]
878 }
879 }
880
881 fn parse_info_command(state: &mut CommandPanelState, command: &str) -> Option<Vec<Action>> {
883 if command == "info" {
884 return Some(vec![Action::AddResponseWithStyle {
885 content: Self::format_info_help(),
886 styled_lines: Some(Self::format_info_help_styled()),
887 response_type: ResponseType::Info,
888 }]);
889 }
890
891 if command == "info file" {
892 state.input_state = InputState::WaitingResponse {
893 command: command.to_string(),
894 sent_time: Instant::now(),
895 command_type: CommandType::InfoFile,
896 };
897 return Some(vec![Action::SendRuntimeCommand(RuntimeCommand::InfoFile)]);
898 }
899
900 if command == "info source" {
901 state.input_state = InputState::WaitingResponse {
902 command: command.to_string(),
903 sent_time: Instant::now(),
904 command_type: CommandType::InfoSource,
905 };
906 return Some(vec![Action::SendRuntimeCommand(RuntimeCommand::InfoSource)]);
907 }
908
909 if command == "info share" {
910 state.input_state = InputState::WaitingResponse {
911 command: command.to_string(),
912 sent_time: Instant::now(),
913 command_type: CommandType::InfoShare,
914 };
915 return Some(vec![Action::SendRuntimeCommand(RuntimeCommand::InfoShare)]);
916 }
917
918 if command == "info share all" {
919 state.input_state = InputState::WaitingResponse {
920 command: command.to_string(),
921 sent_time: Instant::now(),
922 command_type: CommandType::InfoShareAll,
923 };
924 return Some(vec![Action::SendRuntimeCommand(RuntimeCommand::InfoShare)]);
926 }
927
928 if command == "info trace" {
929 return Some(Self::parse_info_trace_command(state, None));
930 }
931
932 if command.starts_with("info trace ") {
933 let id_str = command.strip_prefix("info trace ").unwrap().trim();
934 if let Ok(trace_id) = id_str.parse::<u32>() {
935 return Some(Self::parse_info_trace_command(state, Some(trace_id)));
936 } else {
937 let plain = "Usage: info trace [trace_id]".to_string();
938 let styled = Self::styled_usage(&plain);
939 return Some(vec![Action::AddResponseWithStyle {
940 content: plain,
941 styled_lines: Some(styled),
942 response_type: ResponseType::Error,
943 }]);
944 }
945 }
946
947 if command.starts_with("info function ") || command.starts_with("i f ") {
949 let args = if command.starts_with("info function ") {
950 command.strip_prefix("info function ").unwrap()
951 } else {
952 command.strip_prefix("i f ").unwrap()
953 };
954
955 let parts: Vec<&str> = args.split_whitespace().collect();
956 if parts.is_empty() {
957 let plain = "Usage: info function <function_name> [verbose|v]".to_string();
958 let styled = Self::styled_usage(&plain);
959 return Some(vec![Action::AddResponseWithStyle {
960 content: plain,
961 styled_lines: Some(styled),
962 response_type: ResponseType::Error,
963 }]);
964 }
965
966 let target = parts[0].to_string();
967 let verbose = parts.len() > 1 && (parts[1] == "verbose" || parts[1] == "v");
968
969 state.input_state = InputState::WaitingResponse {
970 command: command.to_string(),
971 sent_time: Instant::now(),
972 command_type: CommandType::InfoFunction {
973 target: target.clone(),
974 verbose,
975 },
976 };
977 return Some(vec![Action::SendRuntimeCommand(
978 RuntimeCommand::InfoFunction { target, verbose },
979 )]);
980 }
981
982 if command.starts_with("info line ") || command.starts_with("i l ") {
984 let args = if command.starts_with("info line ") {
985 command.strip_prefix("info line ").unwrap()
986 } else {
987 command.strip_prefix("i l ").unwrap()
988 };
989
990 let parts: Vec<&str> = args.split_whitespace().collect();
991 if parts.is_empty() {
992 let plain = "Usage: info line <file:line> [verbose|v]".to_string();
993 let styled = Self::styled_usage(&plain);
994 return Some(vec![Action::AddResponseWithStyle {
995 content: plain,
996 styled_lines: Some(styled),
997 response_type: ResponseType::Error,
998 }]);
999 }
1000
1001 let target = parts[0].to_string();
1002 let verbose = parts.len() > 1 && (parts[1] == "verbose" || parts[1] == "v");
1003
1004 state.input_state = InputState::WaitingResponse {
1005 command: command.to_string(),
1006 sent_time: Instant::now(),
1007 command_type: CommandType::InfoLine {
1008 target: target.clone(),
1009 verbose,
1010 },
1011 };
1012 return Some(vec![Action::SendRuntimeCommand(RuntimeCommand::InfoLine {
1013 target,
1014 verbose,
1015 })]);
1016 }
1017
1018 if command.starts_with("info address ") || command.starts_with("i a ") {
1020 let args = if command.starts_with("info address ") {
1021 command.strip_prefix("info address ").unwrap()
1022 } else {
1023 command.strip_prefix("i a ").unwrap()
1024 };
1025
1026 let parts: Vec<&str> = args.split_whitespace().collect();
1027 if parts.is_empty() {
1028 let plain =
1029 "Usage: info address <0xADDR | module_suffix:0xADDR> [verbose|v]".to_string();
1030 let styled = Self::styled_usage(&plain);
1031 return Some(vec![Action::AddResponseWithStyle {
1032 content: plain,
1033 styled_lines: Some(styled),
1034 response_type: ResponseType::Error,
1035 }]);
1036 }
1037
1038 let target = parts[0].to_string();
1039 let verbose = parts.len() > 1 && (parts[1] == "verbose" || parts[1] == "v");
1040
1041 state.input_state = InputState::WaitingResponse {
1042 command: command.to_string(),
1043 sent_time: Instant::now(),
1044 command_type: CommandType::InfoAddress {
1045 target: target.clone(),
1046 verbose,
1047 },
1048 };
1049
1050 return Some(vec![Action::SendRuntimeCommand(
1051 RuntimeCommand::InfoAddress { target, verbose },
1052 )]);
1053 }
1054
1055 None
1056 }
1057
1058 fn parse_save_command(state: &mut CommandPanelState, command: &str) -> Option<Vec<Action>> {
1060 let parts: Vec<&str> = command.split_whitespace().collect();
1061
1062 if parts.is_empty() || parts[0] != "save" {
1063 return None;
1064 }
1065
1066 if parts.len() < 2 {
1067 let plain = "Usage: save <traces|output|session> [filename]".to_string();
1068 let styled = Self::styled_usage(&plain);
1069 return Some(vec![Action::AddResponseWithStyle {
1070 content: plain,
1071 styled_lines: Some(styled),
1072 response_type: ResponseType::Error,
1073 }]);
1074 }
1075
1076 match parts[1] {
1077 "traces" => Self::parse_save_traces_command(state, command),
1078 "output" => Self::parse_save_output_command(state, command),
1079 "session" => Self::parse_save_session_command(state, command),
1080 _ => {
1081 Some(vec![{
1082 let plain = format!(
1083 "Unknown save target: '{}'. Use 'save traces', 'save output', or 'save session'",
1084 parts[1]
1085 );
1086 let styled = vec![
1087 crate::components::command_panel::style_builder::StyledLineBuilder::new()
1088 .styled(plain.clone(), crate::components::command_panel::style_builder::StylePresets::ERROR)
1089 .build(),
1090 ];
1091 Action::AddResponseWithStyle {
1092 content: plain,
1093 styled_lines: Some(styled),
1094 response_type: ResponseType::Error,
1095 }
1096 }])
1097 }
1098 }
1099 }
1100
1101 fn parse_save_traces_command(
1103 state: &mut CommandPanelState,
1104 command: &str,
1105 ) -> Option<Vec<Action>> {
1106 use crate::components::command_panel::trace_persistence::CommandParser as TraceCmdParser;
1107
1108 if let Some((filename, filter)) = command.parse_save_traces_command() {
1110 state.input_state = InputState::WaitingResponse {
1111 command: command.to_string(),
1112 sent_time: Instant::now(),
1113 command_type: CommandType::SaveTraces,
1114 };
1115
1116 return Some(vec![Action::SendRuntimeCommand(
1117 RuntimeCommand::SaveTraces { filename, filter },
1118 )]);
1119 }
1120
1121 None
1122 }
1123
1124 fn parse_save_output_command(
1126 _state: &mut CommandPanelState,
1127 command: &str,
1128 ) -> Option<Vec<Action>> {
1129 let parts: Vec<&str> = command.split_whitespace().collect();
1130
1131 let filename = if parts.len() > 2 {
1133 Some(parts[2..].join(" "))
1134 } else {
1135 None
1136 };
1137
1138 Some(vec![Action::SaveEbpfOutput { filename }])
1139 }
1140
1141 fn parse_save_session_command(
1143 _state: &mut CommandPanelState,
1144 command: &str,
1145 ) -> Option<Vec<Action>> {
1146 let parts: Vec<&str> = command.split_whitespace().collect();
1147
1148 let filename = if parts.len() > 2 {
1150 Some(parts[2..].join(" "))
1151 } else {
1152 None
1153 };
1154
1155 Some(vec![Action::SaveCommandSession { filename }])
1156 }
1157
1158 fn parse_stop_command(command: &str) -> Option<Vec<Action>> {
1160 let parts: Vec<&str> = command.split_whitespace().collect();
1161
1162 if parts.is_empty() || parts[0] != "stop" {
1163 return None;
1164 }
1165
1166 if parts.len() < 2 {
1167 let plain = "Usage: stop <output|session>".to_string();
1168 let styled = Self::styled_usage(&plain);
1169 return Some(vec![Action::AddResponseWithStyle {
1170 content: plain,
1171 styled_lines: Some(styled),
1172 response_type: ResponseType::Error,
1173 }]);
1174 }
1175
1176 match parts[1] {
1177 "output" => Some(vec![Action::StopSaveOutput]),
1178 "session" => Some(vec![Action::StopSaveSession]),
1179 _ => {
1180 let plain = format!(
1181 "Unknown stop target: '{}'. Use 'stop output' or 'stop session'",
1182 parts[1]
1183 );
1184 let styled = vec![
1185 crate::components::command_panel::style_builder::StyledLineBuilder::new()
1186 .styled(
1187 plain.clone(),
1188 crate::components::command_panel::style_builder::StylePresets::ERROR,
1189 )
1190 .build(),
1191 ];
1192 Some(vec![Action::AddResponseWithStyle {
1193 content: plain,
1194 styled_lines: Some(styled),
1195 response_type: ResponseType::Error,
1196 }])
1197 }
1198 }
1199 }
1200
1201 fn parse_ui_command(command: &str) -> Option<Vec<Action>> {
1203 if !command.starts_with("ui ") {
1204 return None;
1205 }
1206
1207 let parts: Vec<&str> = command.split_whitespace().collect();
1208 if parts.len() < 3 {
1209 let plain = "Usage: ui source <on|off>".to_string();
1210 let styled = Self::styled_usage(&plain);
1211 return Some(vec![Action::AddResponseWithStyle {
1212 content: plain,
1213 styled_lines: Some(styled),
1214 response_type: ResponseType::Error,
1215 }]);
1216 }
1217
1218 match (parts[1], parts[2]) {
1219 ("source", "on") => Some(vec![Action::SetSourcePanelVisibility(true)]),
1220 ("source", "off") => Some(vec![Action::SetSourcePanelVisibility(false)]),
1221 _ => {
1222 let plain = "Usage: ui source <on|off>".to_string();
1223 let styled = Self::styled_usage(&plain);
1224 Some(vec![Action::AddResponseWithStyle {
1225 content: plain,
1226 styled_lines: Some(styled),
1227 response_type: ResponseType::Error,
1228 }])
1229 }
1230 }
1231 }
1232
1233 fn parse_source_command(state: &mut CommandPanelState, command: &str) -> Option<Vec<Action>> {
1235 use crate::components::command_panel::trace_persistence::TracePersistence;
1236
1237 let filename = if command.starts_with("source ") {
1239 command.strip_prefix("source ").unwrap().trim()
1240 } else if command.starts_with("s ")
1241 && !command.starts_with("s t")
1242 && !command.starts_with("save")
1243 {
1244 command.strip_prefix("s ").unwrap().trim()
1246 } else {
1247 return None;
1248 };
1249
1250 if filename.is_empty() {
1251 let plain = "Usage: source <filename>".to_string();
1252 let styled = Self::styled_usage(&plain);
1253 return Some(vec![Action::AddResponseWithStyle {
1254 content: plain,
1255 styled_lines: Some(styled),
1256 response_type: ResponseType::Error,
1257 }]);
1258 }
1259
1260 match TracePersistence::load_traces_from_file(filename) {
1262 Ok(traces) => {
1263 if traces.is_empty() {
1264 let plain = format!("No traces found in {filename}");
1265 let styled = vec![
1266 crate::components::command_panel::style_builder::StyledLineBuilder::new()
1267 .styled(plain.clone(), crate::components::command_panel::style_builder::StylePresets::WARNING)
1268 .build(),
1269 ];
1270 return Some(vec![Action::AddResponseWithStyle {
1271 content: plain,
1272 styled_lines: Some(styled),
1273 response_type: ResponseType::Warning,
1274 }]);
1275 }
1276
1277 state.batch_loading = Some(crate::model::panel_state::BatchLoadingState {
1279 filename: filename.to_string(),
1280 total_count: traces.len(),
1281 completed_count: 0,
1282 success_count: 0,
1283 failed_count: 0,
1284 disabled_count: 0,
1285 details: Vec::new(),
1286 });
1287
1288 state.input_state = InputState::WaitingResponse {
1289 command: command.to_string(),
1290 sent_time: Instant::now(),
1291 command_type: CommandType::LoadTraces,
1292 };
1293
1294 Some(vec![Action::SendRuntimeCommand(
1295 RuntimeCommand::LoadTraces {
1296 filename: filename.to_string(),
1297 traces,
1298 },
1299 )])
1300 }
1301 Err(e) => {
1302 Some(vec![{
1303 let plain = format!("✗ Failed to load {filename}: {e}");
1304 let styled = vec![
1305 crate::components::command_panel::style_builder::StyledLineBuilder::new()
1306 .styled(plain.clone(), crate::components::command_panel::style_builder::StylePresets::ERROR)
1307 .build(),
1308 ];
1309 Action::AddResponseWithStyle {
1310 content: plain,
1311 styled_lines: Some(styled),
1312 response_type: ResponseType::Error,
1313 }
1314 }])
1315 }
1316 }
1317 }
1318
1319 fn parse_srcpath_command(state: &mut CommandPanelState, command: &str) -> Option<Vec<Action>> {
1321 if !command.starts_with("srcpath") {
1322 return None;
1323 }
1324
1325 let parts: Vec<&str> = command.split_whitespace().collect();
1326
1327 if parts.len() == 1 {
1328 state.input_state = InputState::WaitingResponse {
1330 command: command.to_string(),
1331 sent_time: Instant::now(),
1332 command_type: CommandType::SrcPath,
1333 };
1334 return Some(vec![Action::SendRuntimeCommand(
1335 RuntimeCommand::SrcPathList,
1336 )]);
1337 }
1338
1339 match parts.get(1) {
1340 Some(&"add") if parts.len() == 3 => {
1341 let dir = parts[2].to_string();
1343 state.input_state = InputState::WaitingResponse {
1344 command: command.to_string(),
1345 sent_time: Instant::now(),
1346 command_type: CommandType::SrcPathAdd,
1347 };
1348 Some(vec![Action::SendRuntimeCommand(
1349 RuntimeCommand::SrcPathAddDir { dir },
1350 )])
1351 }
1352 Some(&"map") if parts.len() == 4 => {
1353 let from = parts[2].to_string();
1355 let to = parts[3].to_string();
1356 state.input_state = InputState::WaitingResponse {
1357 command: command.to_string(),
1358 sent_time: Instant::now(),
1359 command_type: CommandType::SrcPathMap,
1360 };
1361 Some(vec![Action::SendRuntimeCommand(
1362 RuntimeCommand::SrcPathAddMap { from, to },
1363 )])
1364 }
1365 Some(&"remove") if parts.len() == 3 => {
1366 let pattern = parts[2].to_string();
1368 state.input_state = InputState::WaitingResponse {
1369 command: command.to_string(),
1370 sent_time: Instant::now(),
1371 command_type: CommandType::SrcPathRemove,
1372 };
1373 Some(vec![Action::SendRuntimeCommand(
1374 RuntimeCommand::SrcPathRemove { pattern },
1375 )])
1376 }
1377 Some(&"clear") if parts.len() == 2 => {
1378 state.input_state = InputState::WaitingResponse {
1380 command: command.to_string(),
1381 sent_time: Instant::now(),
1382 command_type: CommandType::SrcPathClear,
1383 };
1384 Some(vec![Action::SendRuntimeCommand(
1385 RuntimeCommand::SrcPathClear,
1386 )])
1387 }
1388 Some(&"reset") if parts.len() == 2 => {
1389 state.input_state = InputState::WaitingResponse {
1391 command: command.to_string(),
1392 sent_time: Instant::now(),
1393 command_type: CommandType::SrcPathReset,
1394 };
1395 Some(vec![Action::SendRuntimeCommand(
1396 RuntimeCommand::SrcPathReset,
1397 )])
1398 }
1399 _ => {
1400 Some(vec![{
1401 let plain = "Usage: srcpath [add <dir> | map <from> <to> | remove <path> | clear | reset]".to_string();
1402 let styled = Self::styled_usage(&plain);
1403 Action::AddResponseWithStyle {
1404 content: plain,
1405 styled_lines: Some(styled),
1406 response_type: ResponseType::Error,
1407 }
1408 }])
1409 }
1410 }
1411 }
1412
1413 fn parse_shortcut_command(state: &mut CommandPanelState, command: &str) -> Option<Vec<Action>> {
1415 if command == "s t" {
1417 return Self::parse_save_traces_command(state, "save traces");
1418 }
1419
1420 if command.starts_with("s t ") {
1422 let rest = command.strip_prefix("s t ").unwrap();
1423 let full_command = format!("save traces {rest}");
1424 return Self::parse_save_traces_command(state, &full_command);
1425 }
1426
1427 if command == "s o" {
1429 return Self::parse_save_output_command(state, "save output");
1430 }
1431
1432 if command.starts_with("s o ") {
1434 let rest = command.strip_prefix("s o ").unwrap();
1435 let full_command = format!("save output {rest}");
1436 return Self::parse_save_output_command(state, &full_command);
1437 }
1438
1439 if command == "s s" {
1441 return Self::parse_save_session_command(state, "save session");
1442 }
1443
1444 if command.starts_with("s s ") {
1446 let rest = command.strip_prefix("s s ").unwrap();
1447 let full_command = format!("save session {rest}");
1448 return Self::parse_save_session_command(state, &full_command);
1449 }
1450
1451 if command.starts_with("s ")
1453 && !command.starts_with("s t")
1454 && !command.starts_with("s o")
1455 && !command.starts_with("s s")
1456 {
1457 return Self::parse_source_command(state, command);
1458 }
1459
1460 if command == "i s" {
1462 state.input_state = InputState::WaitingResponse {
1463 command: "info source".to_string(),
1464 sent_time: Instant::now(),
1465 command_type: CommandType::InfoSource,
1466 };
1467 return Some(vec![Action::SendRuntimeCommand(RuntimeCommand::InfoSource)]);
1468 }
1469
1470 if command == "i sh" {
1472 state.input_state = InputState::WaitingResponse {
1473 command: "info share".to_string(),
1474 sent_time: Instant::now(),
1475 command_type: CommandType::InfoShare,
1476 };
1477 return Some(vec![Action::SendRuntimeCommand(RuntimeCommand::InfoShare)]);
1478 }
1479
1480 if command == "i sh all" {
1482 state.input_state = InputState::WaitingResponse {
1483 command: "info share all".to_string(),
1484 sent_time: Instant::now(),
1485 command_type: CommandType::InfoShareAll,
1486 };
1487 return Some(vec![Action::SendRuntimeCommand(RuntimeCommand::InfoShare)]);
1488 }
1489
1490 if command == "i file" || command == "i f" {
1492 state.input_state = InputState::WaitingResponse {
1493 command: "info file".to_string(),
1494 sent_time: Instant::now(),
1495 command_type: CommandType::InfoFile,
1496 };
1497 return Some(vec![Action::SendRuntimeCommand(RuntimeCommand::InfoFile)]);
1498 }
1499
1500 if command == "i t" {
1502 return Some(Self::parse_info_trace_command(state, None));
1503 }
1504
1505 if command.starts_with("i t ") {
1507 let id_str = command.strip_prefix("i t ").unwrap().trim();
1508 if let Ok(trace_id) = id_str.parse::<u32>() {
1509 return Some(Self::parse_info_trace_command(state, Some(trace_id)));
1510 } else {
1511 let plain = "Usage: i t [trace_id]".to_string();
1512 let styled = Self::styled_usage(&plain);
1513 return Some(vec![Action::AddResponseWithStyle {
1514 content: plain,
1515 styled_lines: Some(styled),
1516 response_type: ResponseType::Error,
1517 }]);
1518 }
1519 }
1520
1521 None
1522 }
1523
1524 fn parse_info_trace_command(
1526 state: &mut CommandPanelState,
1527 trace_id: Option<u32>,
1528 ) -> Vec<Action> {
1529 state.input_state = InputState::WaitingResponse {
1530 command: if let Some(id) = trace_id {
1531 format!("info trace {id}")
1532 } else {
1533 "info trace".to_string()
1534 },
1535 sent_time: Instant::now(),
1536 command_type: CommandType::InfoTrace { trace_id },
1537 };
1538
1539 if trace_id.is_some() {
1540 vec![Action::SendRuntimeCommand(RuntimeCommand::InfoTrace {
1541 trace_id,
1542 })]
1543 } else {
1544 vec![Action::SendRuntimeCommand(RuntimeCommand::InfoTraceAll)]
1545 }
1546 }
1547
1548 fn format_info_help() -> String {
1550 [
1551 "🔍 Info Commands Usage:",
1552 "",
1553 " info - Show this help message",
1554 " info file - Show executable file info and sections (i f, i file)",
1555 " info trace [id] - Show trace status (i t [id])",
1556 " info source - Show all source files by module (i s)",
1557 " info share - Show shared libraries WITH debug info (i sh)",
1558 " info share all - Show ALL loaded shared libraries (i sh all)",
1559 " info function <name> - Show debug info for function (i f <name>)",
1560 " info line <file:line> - Show debug info for source line (i l <file:line>)",
1561 " info address <addr> - Show debug info for address (i a <addr>) [TODO]",
1562 "",
1563 "💡 Shortcuts:",
1564 " i f / i file - Same as 'info file'",
1565 " i s - Same as 'info source'",
1566 " i sh - Same as 'info share'",
1567 " i sh all - Same as 'info share all'",
1568 " i t [id] - Same as 'info trace [id]'",
1569 " i f <name> - Same as 'info function <name>'",
1570 " i l <file:line> - Same as 'info line <file:line>'",
1571 " i a <addr> - Same as 'info address <addr>' [TODO]",
1572 "",
1573 "Examples:",
1574 " info file - Show executable file information",
1575 " info trace - Show all traces",
1576 " i t 1 - Show specific trace info",
1577 " i f main - Show debug info for 'main' function",
1578 " i l file.c:42 - Show debug info for source line",
1579 "",
1580 "💡 Use 'help' for complete command reference.",
1581 ]
1582 .join("\n")
1583 }
1584
1585 pub fn should_show_input_prompt(state: &CommandPanelState) -> bool {
1587 matches!(state.input_state, InputState::Ready)
1588 }
1589
1590 pub fn get_prompt(state: &CommandPanelState) -> String {
1592 if !Self::should_show_input_prompt(state) {
1593 return String::new();
1594 }
1595
1596 match state.mode {
1597 crate::model::panel_state::InteractionMode::Input => {
1598 UIStrings::GHOSTSCOPE_PROMPT.to_string()
1599 }
1600 crate::model::panel_state::InteractionMode::Command => {
1601 UIStrings::GHOSTSCOPE_PROMPT.to_string()
1602 }
1603 crate::model::panel_state::InteractionMode::ScriptEditor => {
1604 UIStrings::GHOSTSCOPE_PROMPT.to_string()
1605 }
1606 }
1607 }
1608}
1609
1610#[cfg(test)]
1611mod tests {
1612 use super::*;
1613
1614 #[test]
1615 fn test_command_completion_exact_match() {
1616 assert_eq!(
1618 CommandParser::get_command_completion("tr"),
1619 Some("ace".to_string())
1620 );
1621 assert_eq!(
1622 CommandParser::get_command_completion("hel"),
1623 Some("p".to_string())
1624 );
1625 assert_eq!(
1626 CommandParser::get_command_completion("clea"),
1627 Some("r".to_string())
1628 );
1629 }
1630
1631 #[test]
1632 fn test_command_completion_multiple_matches() {
1633 assert_eq!(CommandParser::get_command_completion("d"), None); assert_eq!(CommandParser::get_command_completion("e"), None); assert_eq!(
1639 CommandParser::get_command_completion("de"),
1640 Some("l".to_string())
1641 ); assert_eq!(CommandParser::get_command_completion("info "), None); }
1644
1645 #[test]
1646 fn test_command_completion_no_match() {
1647 assert_eq!(CommandParser::get_command_completion("xyz"), None);
1649 assert_eq!(CommandParser::get_command_completion("unknown"), None);
1650 }
1651
1652 #[test]
1653 fn test_command_completion_exact_command() {
1654 assert_eq!(CommandParser::get_command_completion("trace"), None);
1656 assert_eq!(CommandParser::get_command_completion("help"), None);
1657 }
1658
1659 #[test]
1660 fn test_command_completion_abbreviations() {
1661 assert_eq!(
1663 CommandParser::get_command_completion("en"),
1664 Some("able".to_string())
1665 ); assert_eq!(
1667 CommandParser::get_command_completion("di"),
1668 Some("s".to_string())
1669 ); }
1671}