ghostscope_ui/model/
panel_state.rs

1use crate::action::ResponseType;
2use ghostscope_protocol::ParsedTraceEvent;
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::time::Instant;
5
6/// Cached trace event with pre-formatted timestamp
7#[derive(Debug, Clone)]
8pub struct CachedTraceEvent {
9    pub event: ParsedTraceEvent,
10    pub formatted_timestamp: String,
11}
12
13/// Source panel state
14#[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    // Search state
28    pub search_query: String,
29    pub search_matches: Vec<(usize, usize, usize)>, // (line_idx, start, end)
30    pub current_match: Option<usize>,
31    pub is_searching: bool,
32
33    // File search state
34    // Note: File list is stored in command_panel.file_completion_cache (single source of truth)
35    // This panel only maintains filtering/selection state
36    pub file_search_query: String,
37    pub file_search_cursor_pos: usize, // Cursor position in the search query
38    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    // Navigation state
45    pub number_buffer: String,
46    pub expecting_g: bool,
47    pub g_pressed: bool,
48
49    // Mode state
50    pub mode: SourcePanelMode,
51
52    // Traced lines tracking
53    pub traced_lines: HashSet<usize>, // Line numbers with active traces (1-based)
54    pub disabled_lines: HashSet<usize>, // Line numbers with disabled traces (1-based)
55    pub pending_trace_line: Option<usize>, // Line waiting for trace confirmation (1-based)
56    // Map from trace_id to (file_path, line_number) for managing trace status
57    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/// eBPF panel state
110#[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, // Index of the selected trace (not line)
117    pub show_cursor: bool,         // Whether to show cursor highlighting
118    pub display_mode: DisplayMode, // Current display mode
119    pub next_message_id: u64,      // Simple counter for message numbering
120    // Numeric jump input for N+G
121    pub numeric_prefix: Option<String>,
122    pub g_pressed: bool, // whether first 'g' was pressed (for 'gg')
123    // Expanded card view state
124    pub view_mode: EbpfViewMode,
125    pub expanded_scroll: usize,   // scroll offset inside expanded card
126    pub last_inner_height: usize, // last known inner height (for half-page scroll)
127}
128
129#[derive(Debug, Clone, Copy, PartialEq)]
130pub enum DisplayMode {
131    AutoRefresh, // Default mode: always show latest trace, auto-scroll
132    Scroll,      // Manual mode: show cursor, manual navigation
133}
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        // Format timestamp once when adding the event
166        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        // Only auto-scroll in auto-refresh mode
178        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        // For now, just set scroll offset to 0 to show all messages
201        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    // Jump to first trace (gg)
232    pub fn jump_to_first(&mut self) {
233        self.enter_scroll_mode();
234        self.cursor_trace_index = 0;
235    }
236
237    // Jump to last trace (G without prefix)
238    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    // Start or append numeric prefix for N G
244    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            // typing number cancels pending 'g'
252            self.g_pressed = false;
253        }
254    }
255
256    // Confirm 'G' action: if numeric prefix present, jump to that message number; else jump to last
257    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    // Jump to specific message number (1-based). Clamp to [first,last]
268    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        // Message numbers are 1-based sequential (1, 2, 3, ...)
276        // Convert to 0-based index
277        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    // Exit to auto-refresh (ESC)
287    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    // Handle 'g' key (support 'gg')
295    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    /// Enter scroll mode and set cursor to the last trace
306    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            // Set cursor to the last trace when entering scroll mode
312            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    // Expanded view helpers
323    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/// Command panel state
378#[derive(Debug)]
379pub struct CommandPanelState {
380    // Input state
381    pub input_text: String,
382    pub cursor_position: usize,
383
384    // Mode and interaction state
385    pub mode: InteractionMode,
386    pub input_state: InputState,
387
388    // History
389    pub command_history: Vec<CommandHistoryItem>,
390    pub history_index: Option<usize>,
391    pub unsent_input_backup: Option<String>,
392
393    // Script editing
394    pub script_cache: Option<ScriptCache>,
395
396    // Command mode navigation - cursor in unified line view
397    pub command_cursor_line: usize,
398    pub command_cursor_column: usize,
399    pub cached_panel_width: u16, // Cached panel width for text wrapping calculations
400
401    // File completion cache
402    pub file_completion_cache:
403        Option<crate::components::command_panel::file_completion::FileCompletionCache>,
404
405    // Saved cursor states for mode switching
406    pub saved_input_cursor: usize, // Input mode cursor position
407    pub saved_script_cursor: Option<(usize, usize)>, // Script mode (line, col)
408    pub previous_mode: Option<InteractionMode>, // Track mode for 'i' key return
409
410    // Display
411    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    // Vim-like escape sequence
417    pub jk_escape_state: JkEscapeState,
418    pub jk_timer: Option<Instant>,
419
420    // Configuration
421    pub max_history_items: usize,
422
423    // New history management
424    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    // Batch loading state for source command
429    pub batch_loading: Option<BatchLoadingState>,
430}
431
432/// State for tracking batch trace loading (e.g., from source command)
433#[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, // TODO: Currently not used - disabled traces are ignored during loading
441    pub details: Vec<crate::events::TraceLoadDetail>,
442}
443
444#[derive(Debug, Clone, Copy, PartialEq)]
445pub enum InteractionMode {
446    Input,        // Normal input mode
447    Command,      // Command mode (previously VimCommand)
448    ScriptEditor, // Multi-line script editing mode
449}
450
451#[derive(Debug, Clone, PartialEq)]
452pub enum JkEscapeState {
453    None, // No escape sequence in progress
454    J,    // 'j' was pressed, waiting for 'k'
455}
456
457#[derive(Debug, Clone, PartialEq)]
458pub enum InputState {
459    Ready, // Normal input state, shows (ghostscope)
460    WaitingResponse {
461        // Waiting for response, completely hide input
462        command: String,
463        sent_time: Instant,
464        command_type: CommandType,
465    },
466    ScriptEditor, // Script editing mode
467}
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,     // Script is being edited
500    Submitted, // Script was submitted successfully
501    Error,     // Script had errors
502}
503
504#[derive(Debug, Clone)]
505pub struct SavedScript {
506    pub content: String,    // Script content
507    pub cursor_line: usize, // Saved cursor line
508    pub cursor_col: usize,  // Saved cursor column
509}
510
511#[derive(Debug, Clone)]
512pub struct ScriptCache {
513    pub target: String,                // Trace target (function name or file:line)
514    pub original_command: String,      // Original trace command (e.g., "trace main")
515    pub selected_index: Option<usize>, // Optional index to filter a single address (1-based)
516    pub lines: Vec<String>,            // Script lines
517    pub cursor_line: usize,            // Current cursor line (0-based)
518    pub cursor_col: usize,             // Current cursor column (0-based)
519    pub status: ScriptStatus,          // Current script status
520    pub saved_scripts: HashMap<String, SavedScript>, // target -> complete script cache
521}
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    /// Optional pre-styled content (takes precedence over content if present)
540    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, // Default width
569            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            // New history management
582            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    /// Update cached panel width for text wrapping calculations
591    pub fn update_panel_width(&mut self, width: u16) {
592        self.cached_panel_width = width;
593    }
594
595    /// Reflow command-mode cursor after width changes to keep it within valid bounds
596    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        // Clamp cursor column to the current line's length (unicode-aware by chars)
613        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    /// Remap command-mode cursor from old wrapped coordinates to new width-aware coordinates
620    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        // Build mapping from wrapped (old) to logical (line index + char offset)
626        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        // Find which logical line the current wrapped cursor line falls into (under old width)
634        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                // Sum lengths of previous wraps to get char offset, then add current column (clamped)
645                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            // Fallback to simple clamp with new width
662            self.cached_panel_width = new_width;
663            self.reflow_command_cursor_after_width_change();
664            return;
665        }
666
667        // Map back into wrapped coordinates under the new width
668        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                    // Place at end of logical line if offset exceeds
687                    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    /// Clean up file completion cache if unused for too long
699    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    /// Enter command mode from current mode, saving cursor state
709    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                // Set command cursor to the current input line (last line)
717                // Use wrapped lines to handle text that exceeds panel width
718                let wrapped_lines = self.get_command_mode_wrapped_lines(panel_width);
719
720                self.command_cursor_line = wrapped_lines.len().saturating_sub(1);
721                // Calculate column position including prompt prefix
722                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                    // Calculate which line in the unified view corresponds to current script cursor
730                    let lines = self.get_command_mode_lines();
731                    let script_start_line = self.get_script_start_line();
732                    // Add 3 for header lines (target, separator, prompt)
733                    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                // Already in command mode, no change needed
740                return;
741            }
742        }
743
744        self.mode = InteractionMode::Command;
745    }
746
747    /// Exit command mode and return to previous mode, restoring cursor state
748    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                    // This shouldn't happen, but handle gracefully
766                    self.mode = InteractionMode::Input;
767                }
768            }
769            self.previous_mode = None;
770        } else {
771            // Fallback to input mode if no previous mode
772            self.mode = InteractionMode::Input;
773        }
774    }
775
776    /// Get total number of lines in unified view (for command mode navigation)
777    pub fn get_total_lines(&self) -> usize {
778        // Use wrapped lines to get accurate line count that considers text wrapping
779        let wrapped_lines = self.get_command_mode_wrapped_lines(self.cached_panel_width);
780        wrapped_lines.len()
781    }
782
783    /// Get script start line offset in unified view
784    fn get_script_start_line(&self) -> usize {
785        // Use static_lines count (same as renderer) to ensure consistency
786        let mut offset = self.static_lines.len();
787
788        offset += 3; // Header + separator + prompt
789        offset
790    }
791
792    /// Get all lines for command mode (unified view)
793    pub fn get_command_mode_lines(&self) -> Vec<String> {
794        let mut lines = Vec::new();
795
796        // Use static_lines directly (same as renderer) to ensure consistency
797        for static_line in &self.static_lines {
798            lines.push(static_line.content.clone());
799        }
800
801        // Add current content based on mode
802        // In command mode, show content based on previous_mode
803        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                // This shouldn't happen in normal flow, but handle gracefully
831                if matches!(self.input_state, InputState::Ready) {
832                    lines.push(format!("(ghostscope) {}", self.input_text));
833                }
834            }
835        }
836
837        lines
838    }
839
840    /// Get wrapped lines for command mode navigation (considering text wrapping)
841    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        // Add current input if not already included (for navigation)
851        if matches!(self.previous_mode, Some(InteractionMode::Input)) {
852            let current_input_line = format!("(ghostscope) {}", self.input_text);
853
854            // Check if it's already included
855            let should_add = if wrapped_lines.is_empty() {
856                true
857            } else {
858                !wrapped_lines.iter().any(|line| line == &current_input_line)
859            };
860
861            if should_add {
862                let wrapped = self.wrap_text_unicode(&current_input_line, available_width);
863                wrapped_lines.extend(wrapped);
864            }
865        }
866
867        wrapped_lines
868    }
869
870    /// Wrap text considering Unicode character widths
871    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            // Calculate the actual display width using Unicode width
883            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                // Need to wrap this line
892                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                        // Start a new line
900                        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    /// Move command cursor up
923    pub fn move_command_cursor_up(&mut self) {
924        if self.command_cursor_line > 0 {
925            self.command_cursor_line -= 1;
926            // Adjust column to fit new line - use wrapped lines for accurate navigation
927            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    /// Move command cursor down
936    pub fn move_command_cursor_down(&mut self) {
937        // Use wrapped lines for accurate navigation
938        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            // Adjust column to fit new line
942            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    /// Move command cursor left (supports line wrapping and Unicode)
950    pub fn move_command_cursor_left(&mut self) {
951        if self.command_cursor_column > 0 {
952            // Move left within current line
953            self.command_cursor_column -= 1;
954        } else if self.command_cursor_line > 0 {
955            // At beginning of line, wrap to end of previous line
956            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                // Use Unicode-aware character counting
960                self.command_cursor_column = lines[self.command_cursor_line].chars().count();
961            }
962        }
963    }
964
965    /// Move command cursor right (supports line wrapping and Unicode)
966    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            // Use Unicode-aware character counting
970            let line_len = lines[self.command_cursor_line].chars().count();
971            if self.command_cursor_column < line_len {
972                // Move right within current line
973                self.command_cursor_column += 1;
974            } else if self.command_cursor_line + 1 < lines.len() {
975                // At end of line, wrap to beginning of next line
976                self.command_cursor_line += 1;
977                self.command_cursor_column = 0;
978            }
979        }
980    }
981
982    /// Navigate to previous command in history (Ctrl+p)
983    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                // Currently at input line, save current input and go to most recent command
991                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                    // Go to previous command
1001                    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                // If already at first command (index 0), do nothing
1006            }
1007        }
1008    }
1009
1010    /// Navigate to next command in history (Ctrl+n)
1011    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                // Already at current input line, do nothing
1019            }
1020            Some(current_idx) => {
1021                if current_idx + 1 < self.command_history.len() {
1022                    // Go to next command
1023                    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                    // At last command, go back to current input
1028                    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    /// Move command cursor up by half a page (fast scroll)
1041    pub fn move_command_half_page_up(&mut self) {
1042        let jump_size = 10; // Half page size
1043        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        // Adjust column to fit new line
1050        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    /// Move command cursor down by half a page (fast scroll)
1058    pub fn move_command_half_page_down(&mut self) {
1059        let jump_size = 10; // Half page size
1060        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        // Adjust column to fit new line
1069        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    // === History Management Methods ===
1077
1078    /// Update auto suggestion based on current input
1079    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    /// Accept the current auto suggestion
1093    /// This treats the suggestion as a new command entry, clearing all related state
1094    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            // Clear any search state - this is a new command entry
1111            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    /// Start history search mode
1124    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        // Clear current input when starting search
1131        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    /// Update history search query and find matches
1140    pub fn update_history_search(&mut self, query: String) {
1141        self.history_search
1142            .update_query(query, &self.command_history_manager);
1143        // Don't modify input_text here - keep it as the actual user input (search query)
1144        // The renderer will decide what to display based on search state
1145    }
1146
1147    /// Move to next history search match
1148    pub fn next_history_match(&mut self) {
1149        // Just update the match, don't modify input_text
1150        // The renderer will display the matched command
1151        self.history_search
1152            .next_match(&self.command_history_manager);
1153    }
1154
1155    /// Exit history search mode
1156    pub fn exit_history_search(&mut self) {
1157        self.history_search.clear();
1158        self.auto_suggestion.clear();
1159    }
1160
1161    /// Exit history search mode and set the selected command as new input
1162    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    /// Add command to history when executed
1170    pub fn add_command_to_history(&mut self, command: &str) {
1171        self.command_history_manager.add_command(command);
1172    }
1173
1174    /// Add command entry to both history manager and command_history, then update static_lines
1175    /// This is the unified method for adding commands that ensures data consistency
1176    pub fn add_command_entry(&mut self, command: &str) {
1177        use std::time::Instant;
1178
1179        // Add to history manager (for Ctrl+R search)
1180        self.command_history_manager.add_command(command);
1181
1182        // Add to command_history (for display)
1183        let item = CommandHistoryItem {
1184            command: command.to_string(),
1185            response: None, // Will be filled when response arrives
1186            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        // Limit history size
1200        const MAX_HISTORY: usize = 1000;
1201        if self.command_history.len() > MAX_HISTORY {
1202            self.command_history.remove(0);
1203        }
1204
1205        // Update static_lines for command mode navigation
1206        crate::components::command_panel::ResponseFormatter::update_static_lines(self);
1207    }
1208
1209    /// Check if currently in history search mode
1210    pub fn is_in_history_search(&self) -> bool {
1211        self.history_search.is_active
1212    }
1213
1214    /// Get current history search query
1215    pub fn get_history_search_query(&self) -> &str {
1216        &self.history_search.query
1217    }
1218
1219    // Removed old add_welcome_lines - now using add_styled_welcome_lines
1220
1221    /// Add styled welcome message lines directly (new simplified approach)
1222    pub fn add_styled_welcome_lines(
1223        &mut self,
1224        styled_lines: Vec<ratatui::text::Line<'static>>,
1225        response_type: ResponseType,
1226    ) {
1227        // Clear existing welcome messages to prevent duplicates
1228        self.static_lines
1229            .retain(|line| !matches!(line.line_type, LineType::Welcome));
1230
1231        for styled_line in styled_lines {
1232            // Extract plain text for content field (for compatibility)
1233            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        // Clear any cached styled buffer since we've added new content
1248        self.styled_buffer = None;
1249        self.styled_at_history_index = None;
1250    }
1251
1252    /// Get auto suggestion text for rendering
1253    pub fn get_suggestion_text(&self) -> Option<&str> {
1254        self.auto_suggestion.get_suggestion_text()
1255    }
1256
1257    /// Get the text that should be displayed in the input line
1258    /// In normal mode, this is just the input text (auto-suggestions are handled separately)
1259    /// In history search mode, this is the matched command
1260    pub fn get_display_text(&self) -> &str {
1261        if self.is_in_history_search() {
1262            // In history search mode, display the matched command if available
1263            if let Some(matched_command) = self
1264                .history_search
1265                .current_match(&self.command_history_manager)
1266            {
1267                return matched_command;
1268            }
1269        }
1270        // In normal mode, always display just the actual input text
1271        // Auto-suggestions are displayed separately in the renderer
1272        &self.input_text
1273    }
1274
1275    /// Get the position where cursor should be displayed
1276    /// This is always based on the actual input text, not auto-suggestions
1277    pub fn get_display_cursor_position(&self) -> usize {
1278        let result = if self.is_in_history_search() {
1279            // In history search mode, cursor should be at the end of the search query
1280            self.history_search.query.len()
1281        } else {
1282            // Normal mode, use actual cursor position in the input text
1283            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}