1use crate::action::ResponseType;
2use ghostscope_protocol::ParsedTraceEvent;
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::time::Instant;
5
6#[derive(Debug, Clone)]
8pub struct CachedTraceEvent {
9 pub event: ParsedTraceEvent,
10 pub formatted_timestamp: String,
11}
12
13#[derive(Debug, Clone)]
15pub struct SourcePanelState {
16 pub content: Vec<String>,
17 pub current_line: Option<usize>,
18 pub cursor_line: usize,
19 pub cursor_col: usize,
20 pub scroll_offset: usize,
21 pub horizontal_scroll_offset: usize,
22 pub file_path: Option<String>,
23 pub language: String,
24 pub area_height: u16,
25 pub area_width: u16,
26
27 pub search_query: String,
29 pub search_matches: Vec<(usize, usize, usize)>, pub current_match: Option<usize>,
31 pub is_searching: bool,
32
33 pub file_search_query: String,
37 pub file_search_cursor_pos: usize, pub file_search_filtered_indices: Vec<usize>,
39 pub file_search_selected: usize,
40 pub file_search_scroll: usize,
41 pub file_search_message: Option<String>,
42 pub is_file_searching: bool,
43
44 pub number_buffer: String,
46 pub expecting_g: bool,
47 pub g_pressed: bool,
48
49 pub mode: SourcePanelMode,
51
52 pub traced_lines: HashSet<usize>, pub disabled_lines: HashSet<usize>, pub pending_trace_line: Option<usize>, pub trace_locations: HashMap<u32, (String, usize)>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum SourcePanelMode {
62 Normal,
63 TextSearch,
64 FileSearch,
65}
66
67impl SourcePanelState {
68 pub fn new() -> Self {
69 Self {
70 content: vec!["// No source code loaded".to_string()],
71 current_line: None,
72 cursor_line: 0,
73 cursor_col: 0,
74 scroll_offset: 0,
75 horizontal_scroll_offset: 0,
76 file_path: None,
77 language: "c".to_string(),
78 area_height: 10,
79 area_width: 80,
80 search_query: String::new(),
81 search_matches: Vec::new(),
82 current_match: None,
83 is_searching: false,
84 file_search_query: String::new(),
85 file_search_cursor_pos: 0,
86 file_search_filtered_indices: Vec::new(),
87 file_search_selected: 0,
88 file_search_scroll: 0,
89 file_search_message: None,
90 is_file_searching: false,
91 number_buffer: String::new(),
92 expecting_g: false,
93 g_pressed: false,
94 mode: SourcePanelMode::Normal,
95 traced_lines: HashSet::new(),
96 disabled_lines: HashSet::new(),
97 pending_trace_line: None,
98 trace_locations: HashMap::new(),
99 }
100 }
101}
102
103impl Default for SourcePanelState {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109#[derive(Debug)]
111pub struct EbpfPanelState {
112 pub trace_events: VecDeque<CachedTraceEvent>,
113 pub scroll_offset: usize,
114 pub max_messages: usize,
115 pub auto_scroll: bool,
116 pub cursor_trace_index: usize, pub show_cursor: bool, pub display_mode: DisplayMode, pub next_message_id: u64, pub numeric_prefix: Option<String>,
122 pub g_pressed: bool, pub view_mode: EbpfViewMode,
125 pub expanded_scroll: usize, pub last_inner_height: usize, }
128
129#[derive(Debug, Clone, Copy, PartialEq)]
130pub enum DisplayMode {
131 AutoRefresh, Scroll, }
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum EbpfViewMode {
137 List,
138 Expanded { index: usize, scroll: usize },
139}
140
141impl EbpfPanelState {
142 pub fn new() -> Self {
143 Self::new_with_max_messages(2000)
144 }
145
146 pub fn new_with_max_messages(max_messages: usize) -> Self {
147 Self {
148 trace_events: VecDeque::new(),
149 scroll_offset: 0,
150 max_messages,
151 auto_scroll: true,
152 cursor_trace_index: 0,
153 show_cursor: false,
154 display_mode: DisplayMode::AutoRefresh,
155 next_message_id: 1,
156 numeric_prefix: None,
157 g_pressed: false,
158 view_mode: EbpfViewMode::List,
159 expanded_scroll: 0,
160 last_inner_height: 0,
161 }
162 }
163
164 pub fn add_trace_event(&mut self, trace_event: ParsedTraceEvent) {
165 let formatted_timestamp = crate::utils::format_timestamp_ns(trace_event.timestamp);
167 let cached_event = CachedTraceEvent {
168 event: trace_event,
169 formatted_timestamp,
170 };
171
172 self.trace_events.push_back(cached_event);
173 if self.trace_events.len() > self.max_messages {
174 self.trace_events.pop_front();
175 }
176
177 if self.display_mode == DisplayMode::AutoRefresh {
179 self.scroll_to_bottom();
180 }
181 }
182
183 pub fn scroll_up(&mut self) {
184 if self.scroll_offset > 0 {
185 self.scroll_offset -= 1;
186 self.auto_scroll = false;
187 }
188 }
189
190 pub fn scroll_down(&mut self) {
191 let total_lines = self.trace_events.len();
192 if self.scroll_offset + 1 < total_lines {
193 self.scroll_offset += 1;
194 } else {
195 self.auto_scroll = true;
196 }
197 }
198
199 pub fn scroll_to_bottom(&mut self) {
200 self.scroll_offset = 0;
202 self.auto_scroll = true;
203 self.show_cursor = false;
204 }
205
206 pub fn move_cursor_up(&mut self) {
207 self.enter_scroll_mode();
208 if self.cursor_trace_index > 0 {
209 self.cursor_trace_index -= 1;
210 }
211 }
212
213 pub fn move_cursor_down(&mut self) {
214 self.enter_scroll_mode();
215 if self.cursor_trace_index + 1 < self.trace_events.len() {
216 self.cursor_trace_index += 1;
217 }
218 }
219
220 pub fn move_cursor_up_10(&mut self) {
221 self.enter_scroll_mode();
222 self.cursor_trace_index = self.cursor_trace_index.saturating_sub(10);
223 }
224
225 pub fn move_cursor_down_10(&mut self) {
226 self.enter_scroll_mode();
227 let max_index = self.trace_events.len().saturating_sub(1);
228 self.cursor_trace_index = (self.cursor_trace_index + 10).min(max_index);
229 }
230
231 pub fn jump_to_first(&mut self) {
233 self.enter_scroll_mode();
234 self.cursor_trace_index = 0;
235 }
236
237 pub fn jump_to_last(&mut self) {
239 self.enter_scroll_mode();
240 self.cursor_trace_index = self.trace_events.len().saturating_sub(1);
241 }
242
243 pub fn push_numeric_digit(&mut self, ch: char) {
245 if ch.is_ascii_digit() {
246 self.enter_scroll_mode();
247 let s = self.numeric_prefix.get_or_insert_with(String::new);
248 if s.len() < 9 {
249 s.push(ch);
250 }
251 self.g_pressed = false;
253 }
254 }
255
256 pub fn confirm_goto(&mut self) {
258 if let Some(s) = self.numeric_prefix.take() {
259 if let Ok(num) = s.parse::<u64>() {
260 self.jump_to_message_number(num);
261 return;
262 }
263 }
264 self.jump_to_last();
265 }
266
267 pub fn jump_to_message_number(&mut self, message_number: u64) {
269 self.enter_scroll_mode();
270 if self.trace_events.is_empty() {
271 self.cursor_trace_index = 0;
272 return;
273 }
274
275 if message_number == 0 {
278 self.cursor_trace_index = 0;
279 return;
280 }
281
282 let target_index = (message_number - 1) as usize;
283 self.cursor_trace_index = target_index.min(self.trace_events.len().saturating_sub(1));
284 }
285
286 pub fn exit_to_auto_refresh(&mut self) {
288 self.numeric_prefix = None;
289 self.g_pressed = false;
290 self.hide_cursor();
291 self.scroll_to_bottom();
292 }
293
294 pub fn handle_g_key(&mut self) {
296 self.enter_scroll_mode();
297 if self.g_pressed {
298 self.g_pressed = false;
299 self.jump_to_first();
300 } else {
301 self.g_pressed = true;
302 }
303 }
304
305 fn enter_scroll_mode(&mut self) {
307 if self.display_mode != DisplayMode::Scroll {
308 self.display_mode = DisplayMode::Scroll;
309 self.show_cursor = true;
310 self.auto_scroll = false;
311 self.cursor_trace_index = self.trace_events.len().saturating_sub(1);
313 }
314 }
315
316 pub fn hide_cursor(&mut self) {
317 self.display_mode = DisplayMode::AutoRefresh;
318 self.show_cursor = false;
319 self.auto_scroll = true;
320 }
321
322 pub fn open_expanded_current(&mut self) {
324 if self.cursor_trace_index < self.trace_events.len() {
325 self.view_mode = EbpfViewMode::Expanded {
326 index: self.cursor_trace_index,
327 scroll: 0,
328 };
329 self.expanded_scroll = 0;
330 }
331 }
332
333 pub fn close_expanded(&mut self) {
334 self.view_mode = EbpfViewMode::List;
335 self.expanded_scroll = 0;
336 }
337
338 pub fn is_expanded(&self) -> bool {
339 matches!(self.view_mode, EbpfViewMode::Expanded { .. })
340 }
341
342 pub fn expanded_index(&self) -> Option<usize> {
343 if let EbpfViewMode::Expanded { index, .. } = self.view_mode {
344 Some(index)
345 } else {
346 None
347 }
348 }
349
350 pub fn set_expanded_scroll(&mut self, value: usize) {
351 self.expanded_scroll = value;
352 if let EbpfViewMode::Expanded { index, .. } = self.view_mode {
353 self.view_mode = EbpfViewMode::Expanded {
354 index,
355 scroll: self.expanded_scroll,
356 };
357 }
358 }
359
360 pub fn scroll_expanded_up(&mut self, lines: usize) {
361 let new_off = self.expanded_scroll.saturating_sub(lines);
362 self.set_expanded_scroll(new_off);
363 }
364
365 pub fn scroll_expanded_down(&mut self, lines: usize, max_lines: usize) {
366 let new_off = (self.expanded_scroll + lines).min(max_lines);
367 self.set_expanded_scroll(new_off);
368 }
369}
370
371impl Default for EbpfPanelState {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377#[derive(Debug)]
379pub struct CommandPanelState {
380 pub input_text: String,
382 pub cursor_position: usize,
383
384 pub mode: InteractionMode,
386 pub input_state: InputState,
387
388 pub command_history: Vec<CommandHistoryItem>,
390 pub history_index: Option<usize>,
391 pub unsent_input_backup: Option<String>,
392
393 pub script_cache: Option<ScriptCache>,
395
396 pub command_cursor_line: usize,
398 pub command_cursor_column: usize,
399 pub cached_panel_width: u16, pub file_completion_cache:
403 Option<crate::components::command_panel::file_completion::FileCompletionCache>,
404
405 pub saved_input_cursor: usize, pub saved_script_cursor: Option<(usize, usize)>, pub previous_mode: Option<InteractionMode>, pub static_lines: Vec<StaticTextLine>,
412 pub scroll_offset: usize,
413 pub styled_buffer: Option<Vec<ratatui::text::Line<'static>>>,
414 pub styled_at_history_index: Option<usize>,
415
416 pub jk_escape_state: JkEscapeState,
418 pub jk_timer: Option<Instant>,
419
420 pub max_history_items: usize,
422
423 pub command_history_manager: crate::components::command_panel::CommandHistory,
425 pub history_search: crate::components::command_panel::HistorySearchState,
426 pub auto_suggestion: crate::components::command_panel::AutoSuggestionState,
427
428 pub batch_loading: Option<BatchLoadingState>,
430}
431
432#[derive(Debug, Clone)]
434pub struct BatchLoadingState {
435 pub filename: String,
436 pub total_count: usize,
437 pub completed_count: usize,
438 pub success_count: usize,
439 pub failed_count: usize,
440 pub disabled_count: usize, pub details: Vec<crate::events::TraceLoadDetail>,
442}
443
444#[derive(Debug, Clone, Copy, PartialEq)]
445pub enum InteractionMode {
446 Input, Command, ScriptEditor, }
450
451#[derive(Debug, Clone, PartialEq)]
452pub enum JkEscapeState {
453 None, J, }
456
457#[derive(Debug, Clone, PartialEq)]
458pub enum InputState {
459 Ready, WaitingResponse {
461 command: String,
463 sent_time: Instant,
464 command_type: CommandType,
465 },
466 ScriptEditor, }
468
469#[derive(Debug, Clone, PartialEq)]
470pub enum CommandType {
471 Script,
472 Enable { trace_id: u32 },
473 Disable { trace_id: u32 },
474 Delete { trace_id: u32 },
475 EnableAll,
476 DisableAll,
477 DeleteAll,
478 InfoFunction { target: String, verbose: bool },
479 InfoLine { target: String, verbose: bool },
480 InfoAddress { target: String, verbose: bool },
481 InfoTrace { trace_id: Option<u32> },
482 InfoTraceAll,
483 InfoSource,
484 InfoShare,
485 InfoShareAll,
486 InfoFile,
487 SaveTraces,
488 LoadTraces,
489 SrcPath,
490 SrcPathAdd,
491 SrcPathMap,
492 SrcPathRemove,
493 SrcPathClear,
494 SrcPathReset,
495}
496
497#[derive(Debug, Clone, Copy, PartialEq)]
498pub enum ScriptStatus {
499 Draft, Submitted, Error, }
503
504#[derive(Debug, Clone)]
505pub struct SavedScript {
506 pub content: String, pub cursor_line: usize, pub cursor_col: usize, }
510
511#[derive(Debug, Clone)]
512pub struct ScriptCache {
513 pub target: String, pub original_command: String, pub selected_index: Option<usize>, pub lines: Vec<String>, pub cursor_line: usize, pub cursor_col: usize, pub status: ScriptStatus, pub saved_scripts: HashMap<String, SavedScript>, }
522
523#[derive(Debug, Clone)]
524pub struct CommandHistoryItem {
525 pub command: String,
526 pub response: Option<String>,
527 pub response_styled: Option<Vec<ratatui::text::Line<'static>>>,
528 pub timestamp: std::time::Instant,
529 pub prompt: String,
530 pub response_type: Option<ResponseType>,
531}
532
533#[derive(Debug, Clone)]
534pub struct StaticTextLine {
535 pub content: String,
536 pub line_type: LineType,
537 pub history_index: Option<usize>,
538 pub response_type: Option<ResponseType>,
539 pub styled_content: Option<ratatui::text::Line<'static>>,
541}
542
543#[derive(Debug, Clone, Copy, PartialEq)]
544pub enum LineType {
545 Command,
546 Response,
547 CurrentInput,
548 Welcome,
549}
550
551impl CommandPanelState {
552 pub fn new() -> Self {
553 Self::new_with_config(&crate::model::ui_state::HistoryConfig::default())
554 }
555
556 pub fn new_with_config(history_config: &crate::model::ui_state::HistoryConfig) -> Self {
557 Self {
558 input_text: String::new(),
559 cursor_position: 0,
560 mode: InteractionMode::Input,
561 input_state: InputState::Ready,
562 command_history: Vec::new(),
563 history_index: None,
564 unsent_input_backup: None,
565 script_cache: None,
566 command_cursor_line: 0,
567 command_cursor_column: 0,
568 cached_panel_width: 80, file_completion_cache: None,
570 saved_input_cursor: 0,
571 saved_script_cursor: None,
572 previous_mode: None,
573 static_lines: Vec::new(),
574 scroll_offset: 0,
575 styled_buffer: None,
576 styled_at_history_index: None,
577 jk_escape_state: JkEscapeState::None,
578 jk_timer: None,
579 max_history_items: history_config.max_entries,
580
581 command_history_manager:
583 crate::components::command_panel::CommandHistory::new_with_config(history_config),
584 history_search: crate::components::command_panel::HistorySearchState::new(),
585 auto_suggestion: crate::components::command_panel::AutoSuggestionState::new(),
586 batch_loading: None,
587 }
588 }
589
590 pub fn update_panel_width(&mut self, width: u16) {
592 self.cached_panel_width = width;
593 }
594
595 pub fn reflow_command_cursor_after_width_change(&mut self) {
597 if self.mode != InteractionMode::Command {
598 return;
599 }
600
601 let wrapped = self.get_command_mode_wrapped_lines(self.cached_panel_width);
602 if wrapped.is_empty() {
603 self.command_cursor_line = 0;
604 self.command_cursor_column = 0;
605 return;
606 }
607
608 if self.command_cursor_line >= wrapped.len() {
609 self.command_cursor_line = wrapped.len().saturating_sub(1);
610 }
611
612 let line_len = wrapped[self.command_cursor_line].chars().count();
614 if self.command_cursor_column > line_len {
615 self.command_cursor_column = line_len;
616 }
617 }
618
619 pub fn remap_command_cursor_on_width_change(&mut self, old_width: u16, new_width: u16) {
621 if self.mode != InteractionMode::Command {
622 return;
623 }
624
625 let logical_lines = self.get_command_mode_lines();
627 if logical_lines.is_empty() {
628 self.command_cursor_line = 0;
629 self.command_cursor_column = 0;
630 return;
631 }
632
633 let mut acc_old = 0usize;
635 let mut target_logical_idx = 0usize;
636 let mut logical_char_offset = 0usize;
637 let mut found = false;
638
639 for (i, line) in logical_lines.iter().enumerate() {
640 let wraps_old = self.wrap_text_unicode(line, old_width);
641 let wrap_count = wraps_old.len();
642 if self.command_cursor_line < acc_old + wrap_count {
643 let within = self.command_cursor_line - acc_old;
644 let offset: usize = wraps_old
646 .iter()
647 .take(within)
648 .map(|s| s.chars().count())
649 .sum();
650 let this_len = wraps_old[within].chars().count();
651 let col = self.command_cursor_column.min(this_len);
652 logical_char_offset = offset + col;
653 target_logical_idx = i;
654 found = true;
655 break;
656 }
657 acc_old += wrap_count;
658 }
659
660 if !found {
661 self.cached_panel_width = new_width;
663 self.reflow_command_cursor_after_width_change();
664 return;
665 }
666
667 let mut acc_new = 0usize;
669 for (i, line) in logical_lines.iter().enumerate() {
670 let wraps_new = self.wrap_text_unicode(line, new_width);
671 if i == target_logical_idx {
672 let mut remaining = logical_char_offset;
673 let mut widx = 0usize;
674 while widx < wraps_new.len() {
675 let seg_len = wraps_new[widx].chars().count();
676 if remaining <= seg_len {
677 self.command_cursor_line = acc_new + widx;
678 self.command_cursor_column = remaining.min(seg_len);
679 break;
680 }
681 remaining -= seg_len;
682 widx += 1;
683 }
684
685 if widx >= wraps_new.len() {
686 self.command_cursor_line = acc_new + wraps_new.len().saturating_sub(1);
688 self.command_cursor_column =
689 wraps_new.last().map(|s| s.chars().count()).unwrap_or(0);
690 }
691 break;
692 } else {
693 acc_new += wraps_new.len();
694 }
695 }
696 }
697
698 pub fn cleanup_file_completion_cache(&mut self) {
700 if let Some(cache) = &self.file_completion_cache {
701 if cache.should_cleanup() {
702 tracing::debug!("Cleaning up unused file completion cache");
703 self.file_completion_cache = None;
704 }
705 }
706 }
707
708 pub fn enter_command_mode(&mut self, panel_width: u16) {
710 self.cached_panel_width = panel_width;
711 self.previous_mode = Some(self.mode);
712
713 match self.mode {
714 InteractionMode::Input => {
715 self.saved_input_cursor = self.cursor_position;
716 let wrapped_lines = self.get_command_mode_wrapped_lines(panel_width);
719
720 self.command_cursor_line = wrapped_lines.len().saturating_sub(1);
721 let prompt = crate::ui::strings::UIStrings::GHOSTSCOPE_PROMPT;
723 self.command_cursor_column = prompt.chars().count() + self.cursor_position;
724 }
725 InteractionMode::ScriptEditor => {
726 if let Some(ref script_cache) = self.script_cache {
727 self.saved_script_cursor =
728 Some((script_cache.cursor_line, script_cache.cursor_col));
729 let lines = self.get_command_mode_lines();
731 let script_start_line = self.get_script_start_line();
732 self.command_cursor_line = (script_start_line + 3 + script_cache.cursor_line)
734 .min(lines.len().saturating_sub(1));
735 self.command_cursor_column = script_cache.cursor_col;
736 }
737 }
738 InteractionMode::Command => {
739 return;
741 }
742 }
743
744 self.mode = InteractionMode::Command;
745 }
746
747 pub fn exit_command_mode(&mut self) {
749 if let Some(previous_mode) = self.previous_mode {
750 match previous_mode {
751 InteractionMode::Input => {
752 self.cursor_position = self.saved_input_cursor;
753 self.mode = InteractionMode::Input;
754 }
755 InteractionMode::ScriptEditor => {
756 if let Some((line, col)) = self.saved_script_cursor {
757 if let Some(ref mut script_cache) = self.script_cache {
758 script_cache.cursor_line = line;
759 script_cache.cursor_col = col;
760 }
761 }
762 self.mode = InteractionMode::ScriptEditor;
763 }
764 InteractionMode::Command => {
765 self.mode = InteractionMode::Input;
767 }
768 }
769 self.previous_mode = None;
770 } else {
771 self.mode = InteractionMode::Input;
773 }
774 }
775
776 pub fn get_total_lines(&self) -> usize {
778 let wrapped_lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
780 wrapped_lines.len()
781 }
782
783 fn get_script_start_line(&self) -> usize {
785 let mut offset = self.static_lines.len();
787
788 offset += 3; offset
790 }
791
792 pub fn get_command_mode_lines(&self) -> Vec<String> {
794 let mut lines = Vec::new();
795
796 for static_line in &self.static_lines {
798 lines.push(static_line.content.clone());
799 }
800
801 let display_mode = if self.mode == InteractionMode::Command {
804 self.previous_mode.unwrap_or(InteractionMode::Input)
805 } else {
806 self.mode
807 };
808
809 match display_mode {
810 InteractionMode::Input => {
811 if matches!(self.input_state, InputState::Ready) {
812 lines.push(format!("(ghostscope) {}", self.input_text));
813 }
814 }
815 InteractionMode::ScriptEditor => {
816 if let Some(ref script_cache) = self.script_cache {
817 lines.push(format!(
818 "🔨 Entering script mode for target: {}",
819 script_cache.target
820 ));
821 lines.push("─".repeat(50));
822 lines.push("Script Editor (Ctrl+s to submit, Esc to cancel):".to_string());
823
824 for (idx, line) in script_cache.lines.iter().enumerate() {
825 lines.push(format!("{:3} │ {}", idx + 1, line));
826 }
827 }
828 }
829 InteractionMode::Command => {
830 if matches!(self.input_state, InputState::Ready) {
832 lines.push(format!("(ghostscope) {}", self.input_text));
833 }
834 }
835 }
836
837 lines
838 }
839
840 pub fn get_command_mode_wrapped_lines(&self, available_width: u16) -> Vec<String> {
842 let logical_lines = self.get_command_mode_lines();
843 let mut wrapped_lines = Vec::new();
844
845 for logical_line in logical_lines {
846 let wrapped = self.wrap_text_unicode(&logical_line, available_width);
847 wrapped_lines.extend(wrapped);
848 }
849
850 if matches!(self.previous_mode, Some(InteractionMode::Input)) {
852 let current_input_line = format!("(ghostscope) {}", self.input_text);
853
854 let should_add = if wrapped_lines.is_empty() {
856 true
857 } else {
858 !wrapped_lines.iter().any(|line| line == ¤t_input_line)
859 };
860
861 if should_add {
862 let wrapped = self.wrap_text_unicode(¤t_input_line, available_width);
863 wrapped_lines.extend(wrapped);
864 }
865 }
866
867 wrapped_lines
868 }
869
870 fn wrap_text_unicode(&self, text: &str, width: u16) -> Vec<String> {
872 use unicode_width::UnicodeWidthChar;
873
874 if width <= 2 {
875 return vec![text.to_string()];
876 }
877
878 let max_width = width as usize;
879 let mut lines = Vec::new();
880
881 for line in text.lines() {
882 let line_width: usize = line
884 .chars()
885 .map(|c| UnicodeWidthChar::width(c).unwrap_or(0))
886 .sum();
887
888 if line_width <= max_width {
889 lines.push(line.to_string());
890 } else {
891 let mut current_line = String::new();
893 let mut current_width = 0;
894
895 for ch in line.chars() {
896 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
897
898 if current_width + char_width > max_width && !current_line.is_empty() {
899 lines.push(current_line);
901 current_line = ch.to_string();
902 current_width = char_width;
903 } else {
904 current_line.push(ch);
905 current_width += char_width;
906 }
907 }
908
909 if !current_line.is_empty() {
910 lines.push(current_line);
911 }
912 }
913 }
914
915 if lines.is_empty() {
916 lines.push(String::new());
917 }
918
919 lines
920 }
921
922 pub fn move_command_cursor_up(&mut self) {
924 if self.command_cursor_line > 0 {
925 self.command_cursor_line -= 1;
926 let lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
928 if self.command_cursor_line < lines.len() {
929 let line_len = lines[self.command_cursor_line].chars().count();
930 self.command_cursor_column = self.command_cursor_column.min(line_len);
931 }
932 }
933 }
934
935 pub fn move_command_cursor_down(&mut self) {
937 let lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
939 if self.command_cursor_line + 1 < lines.len() {
940 self.command_cursor_line += 1;
941 if self.command_cursor_line < lines.len() {
943 let line_len = lines[self.command_cursor_line].chars().count();
944 self.command_cursor_column = self.command_cursor_column.min(line_len);
945 }
946 }
947 }
948
949 pub fn move_command_cursor_left(&mut self) {
951 if self.command_cursor_column > 0 {
952 self.command_cursor_column -= 1;
954 } else if self.command_cursor_line > 0 {
955 self.command_cursor_line -= 1;
957 let lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
958 if self.command_cursor_line < lines.len() {
959 self.command_cursor_column = lines[self.command_cursor_line].chars().count();
961 }
962 }
963 }
964
965 pub fn move_command_cursor_right(&mut self) {
967 let lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
968 if self.command_cursor_line < lines.len() {
969 let line_len = lines[self.command_cursor_line].chars().count();
971 if self.command_cursor_column < line_len {
972 self.command_cursor_column += 1;
974 } else if self.command_cursor_line + 1 < lines.len() {
975 self.command_cursor_line += 1;
977 self.command_cursor_column = 0;
978 }
979 }
980 }
981
982 pub fn history_previous(&mut self) {
984 if self.command_history.is_empty() {
985 return;
986 }
987
988 match self.history_index {
989 None => {
990 self.unsent_input_backup = Some(self.input_text.clone());
992 self.history_index = Some(self.command_history.len() - 1);
993 self.input_text = self.command_history[self.command_history.len() - 1]
994 .command
995 .clone();
996 self.cursor_position = self.input_text.len();
997 }
998 Some(current_idx) => {
999 if current_idx > 0 {
1000 self.history_index = Some(current_idx - 1);
1002 self.input_text = self.command_history[current_idx - 1].command.clone();
1003 self.cursor_position = self.input_text.len();
1004 }
1005 }
1007 }
1008 }
1009
1010 pub fn history_next(&mut self) {
1012 if self.command_history.is_empty() {
1013 return;
1014 }
1015
1016 match self.history_index {
1017 None => {
1018 }
1020 Some(current_idx) => {
1021 if current_idx + 1 < self.command_history.len() {
1022 self.history_index = Some(current_idx + 1);
1024 self.input_text = self.command_history[current_idx + 1].command.clone();
1025 self.cursor_position = self.input_text.len();
1026 } else {
1027 self.history_index = None;
1029 if let Some(backup) = self.unsent_input_backup.take() {
1030 self.input_text = backup;
1031 } else {
1032 self.input_text.clear();
1033 }
1034 self.cursor_position = self.input_text.len();
1035 }
1036 }
1037 }
1038 }
1039
1040 pub fn move_command_half_page_up(&mut self) {
1042 let jump_size = 10; if self.command_cursor_line >= jump_size {
1044 self.command_cursor_line -= jump_size;
1045 } else {
1046 self.command_cursor_line = 0;
1047 }
1048
1049 let lines = self.get_command_mode_lines();
1051 if self.command_cursor_line < lines.len() {
1052 let line_len = lines[self.command_cursor_line].chars().count();
1053 self.command_cursor_column = self.command_cursor_column.min(line_len);
1054 }
1055 }
1056
1057 pub fn move_command_half_page_down(&mut self) {
1059 let jump_size = 10; let total_lines = self.get_total_lines();
1061
1062 if self.command_cursor_line + jump_size < total_lines {
1063 self.command_cursor_line += jump_size;
1064 } else {
1065 self.command_cursor_line = total_lines.saturating_sub(1);
1066 }
1067
1068 let lines = self.get_command_mode_lines();
1070 if self.command_cursor_line < lines.len() {
1071 let line_len = lines[self.command_cursor_line].chars().count();
1072 self.command_cursor_column = self.command_cursor_column.min(line_len);
1073 }
1074 }
1075
1076 pub fn update_auto_suggestion(&mut self) {
1080 tracing::debug!("update_auto_suggestion: input_text='{}'", self.input_text);
1081
1082 self.auto_suggestion
1083 .update(&self.input_text, &self.command_history_manager);
1084
1085 if let Some(suggestion) = self.auto_suggestion.get_full_suggestion() {
1086 tracing::debug!("update_auto_suggestion: found suggestion='{}'", suggestion);
1087 } else {
1088 tracing::debug!("update_auto_suggestion: no suggestion found");
1089 }
1090 }
1091
1092 pub fn accept_auto_suggestion(&mut self) {
1095 if let Some(suggestion) = self.auto_suggestion.get_full_suggestion() {
1096 tracing::debug!(
1097 "accept_auto_suggestion: before - input_text='{}', cursor_position={}",
1098 self.input_text,
1099 self.cursor_position
1100 );
1101 tracing::debug!(
1102 "accept_auto_suggestion: accepting suggestion='{}'",
1103 suggestion
1104 );
1105
1106 self.input_text = suggestion.to_string();
1107 self.cursor_position = self.input_text.len();
1108 self.auto_suggestion.clear();
1109
1110 self.history_search.clear();
1112
1113 tracing::debug!(
1114 "accept_auto_suggestion: after - input_text='{}', cursor_position={}",
1115 self.input_text,
1116 self.cursor_position
1117 );
1118 } else {
1119 tracing::debug!("accept_auto_suggestion: no suggestion available");
1120 }
1121 }
1122
1123 pub fn start_history_search(&mut self) {
1125 tracing::debug!(
1126 "start_history_search: command_history.len()={}",
1127 self.command_history.len()
1128 );
1129 self.history_search.start_search();
1130 self.input_text.clear();
1132 self.cursor_position = 0;
1133 tracing::debug!(
1134 "start_history_search: after clear - command_history.len()={}",
1135 self.command_history.len()
1136 );
1137 }
1138
1139 pub fn update_history_search(&mut self, query: String) {
1141 self.history_search
1142 .update_query(query, &self.command_history_manager);
1143 }
1146
1147 pub fn next_history_match(&mut self) {
1149 self.history_search
1152 .next_match(&self.command_history_manager);
1153 }
1154
1155 pub fn exit_history_search(&mut self) {
1157 self.history_search.clear();
1158 self.auto_suggestion.clear();
1159 }
1160
1161 pub fn exit_history_search_with_selection(&mut self, selected_command: &str) {
1163 self.input_text = selected_command.to_string();
1164 self.cursor_position = self.input_text.len();
1165 self.history_search.clear();
1166 self.auto_suggestion.clear();
1167 }
1168
1169 pub fn add_command_to_history(&mut self, command: &str) {
1171 self.command_history_manager.add_command(command);
1172 }
1173
1174 pub fn add_command_entry(&mut self, command: &str) {
1177 use std::time::Instant;
1178
1179 self.command_history_manager.add_command(command);
1181
1182 let item = CommandHistoryItem {
1184 command: command.to_string(),
1185 response: None, response_styled: None,
1187 timestamp: Instant::now(),
1188 prompt: "(ghostscope) ".to_string(),
1189 response_type: None,
1190 };
1191 self.command_history.push(item);
1192
1193 tracing::debug!(
1194 "add_command_entry: Added command '{}', history length: {}",
1195 command,
1196 self.command_history.len()
1197 );
1198
1199 const MAX_HISTORY: usize = 1000;
1201 if self.command_history.len() > MAX_HISTORY {
1202 self.command_history.remove(0);
1203 }
1204
1205 crate::components::command_panel::ResponseFormatter::update_static_lines(self);
1207 }
1208
1209 pub fn is_in_history_search(&self) -> bool {
1211 self.history_search.is_active
1212 }
1213
1214 pub fn get_history_search_query(&self) -> &str {
1216 &self.history_search.query
1217 }
1218
1219 pub fn add_styled_welcome_lines(
1223 &mut self,
1224 styled_lines: Vec<ratatui::text::Line<'static>>,
1225 response_type: ResponseType,
1226 ) {
1227 self.static_lines
1229 .retain(|line| !matches!(line.line_type, LineType::Welcome));
1230
1231 for styled_line in styled_lines {
1232 let content: String = styled_line
1234 .spans
1235 .iter()
1236 .map(|span| span.content.as_ref())
1237 .collect();
1238
1239 self.static_lines.push(StaticTextLine {
1240 content,
1241 line_type: LineType::Welcome,
1242 history_index: None,
1243 response_type: Some(response_type),
1244 styled_content: Some(styled_line),
1245 });
1246 }
1247 self.styled_buffer = None;
1249 self.styled_at_history_index = None;
1250 }
1251
1252 pub fn get_suggestion_text(&self) -> Option<&str> {
1254 self.auto_suggestion.get_suggestion_text()
1255 }
1256
1257 pub fn get_display_text(&self) -> &str {
1261 if self.is_in_history_search() {
1262 if let Some(matched_command) = self
1264 .history_search
1265 .current_match(&self.command_history_manager)
1266 {
1267 return matched_command;
1268 }
1269 }
1270 &self.input_text
1273 }
1274
1275 pub fn get_display_cursor_position(&self) -> usize {
1278 let result = if self.is_in_history_search() {
1279 self.history_search.query.len()
1281 } else {
1282 self.cursor_position
1284 };
1285
1286 tracing::debug!("get_display_cursor_position: is_in_history_search={}, input_text='{}', cursor_position={}, result={}",
1287 self.is_in_history_search(), self.input_text, self.cursor_position, result);
1288 result
1289 }
1290}
1291
1292impl Default for CommandPanelState {
1293 fn default() -> Self {
1294 Self::new()
1295 }
1296}