diff --git a/sql-cli/docs/analysis/CURRENT_STATE_ANALYSIS.md b/sql-cli/docs/analysis/CURRENT_STATE_ANALYSIS.md new file mode 100644 index 00000000..4744da61 --- /dev/null +++ b/sql-cli/docs/analysis/CURRENT_STATE_ANALYSIS.md @@ -0,0 +1,184 @@ +# Current State Analysis: Mapping the Chaos + +## Executive Summary + +Our TUI has **complex nested state** that's currently scattered across multiple components. Before designing the centralized state manager, we need to precisely map: +- All modes and substates +- Where state currently lives +- State transition triggers +- Side effects that need coordination + +## Primary Mode Hierarchy + +### 1. **INPUT MODE** (Command/Query entry) +**Primary State**: User typing SQL queries +``` +AppMode::Command +├── Normal typing +├── Tab completion active +├── History search (Ctrl+R) +└── Cursor positioning +``` + +**Current State Locations**: +- `Buffer.mode` - Primary mode flag +- `Buffer.input_text` - Query text +- `Buffer.cursor_pos` - Cursor position +- `CompletionState` - Tab completion +- `HistorySearchState` - History search + +### 2. **RESULTS MODE** (Navigation & Operations) +**Primary State**: User navigating/operating on query results +``` +AppMode::Results +├── Normal navigation (hjkl, arrows) +├── Search substates: +│ ├── VIM search (/ key) - text search in data +│ ├── Column search (search key) - column name search +│ └── Fuzzy filter - live data filtering +├── Operations: +│ ├── Column operations (pin, hide, sort) +│ ├── Selection mode +│ └── Edit mode (future) +└── View states: + ├── Normal view + ├── Debug view (F5) + └── Pretty query view +``` + +**Current State Locations**: +- `Buffer.mode` - Primary mode +- `VimSearchManager` - Vim search state +- `ColumnSearchState` - Column search +- `FilterState` - Fuzzy filtering +- `NavigationState` - Cursor/viewport position +- `SortState` - Column sorting +- `SelectionState` - Row/cell selection + +## State Transition Triggers + +### Entering Results Mode +``` +Trigger: User presses Enter on query +Effects needed: +├── Execute query → dataview +├── Switch to Results mode +├── Clear all search states +├── Reset viewport to (0,0) +├── Update status line +└── Set navigation context +``` + +### Search Mode Transitions +``` +Trigger: User presses '/' (vim search) +Effects needed: +├── Enter VimSearch::Typing state +├── Show search input in status +├── Capture keystrokes for pattern +└── Clear other search states + +Trigger: User presses Escape in search +Effects needed: +├── Exit search state +├── Restore normal navigation +├── Update key mapping context +├── Clear search display +└── Reset status line +``` + +### Mode Restoration +``` +Trigger: User runs new query while in search +Effects needed: +├── Clear ALL search states +├── Reset to normal Results mode +├── Update viewport +├── Restore navigation keys +└── Update status line +``` + +## Current State Scatter Points + +### 1. **Search State** (The 'N' key bug source) +```rust +// Scattered across 3+ locations: +buffer.get_search_pattern() // Regular search +vim_search_manager.is_active() // Vim search +state_container.column_search().is_active // Column search +// Plus fuzzy filter, history search... +``` + +### 2. **Navigation Context** +```rust +// Scattered across: +buffer.get_mode() // Primary mode +navigation_state.selected_row/column // Position +viewport_manager.viewport_position // Scroll state +selection_state.mode // Selection type +``` + +### 3. **UI Coordination** +```rust +// Status line needs: +mode + search_state + navigation + results + help_text + +// Key mapping needs: +mode + search_active + has_results + selection_mode + +// Viewport needs: +navigation_state + search_matches + selection +``` + +## Side Effects That Need Coordination + +### When Exiting Search Mode +1. **Key Mapping**: 'N' should map to toggle_line_numbers, not search navigation +2. **Status Line**: Remove search pattern display, show normal mode info +3. **Viewport**: May need to restore previous position +4. **Input State**: Clear search input buffers + +### When Entering Results Mode +1. **Navigation**: Initialize cursor position +2. **Viewport**: Set to show results from top +3. **Search States**: Clear all previous search state +4. **Key Context**: Enable results navigation keys +5. **Status**: Show results info (row count, etc.) + +### When Switching Between Search Types +1. **Previous Search**: Clear state from previous search type +2. **Key Mapping**: Update context for new search type +3. **Status Display**: Show appropriate search UI +4. **Input Capture**: Redirect keystrokes appropriately + +## The Core Problem + +**Multiple sources of truth** lead to **inconsistent state**: +- Action system checks 3+ search state sources +- Key mapping depends on scattered boolean flags +- UI components each manage their own state +- State transitions don't automatically coordinate side effects + +## Requirements for State Manager + +### Must Handle +1. **Hierarchical States**: Mode → Submode → Operation state +2. **Automatic Side Effects**: Status line, viewport, key context updates +3. **State Validation**: Prevent impossible state combinations +4. **Transition Safety**: Ensure clean exits from all substates +5. **Debugging**: Central logging of all state changes + +### Must Avoid +1. **Big Bang Migration**: Change everything at once +2. **Over-Engineering**: Complex state machines for simple cases +3. **Performance Issues**: State checks are in hot paths +4. **Breaking Changes**: Maintain current TUI behavior during migration + +## Next Steps + +1. **Design Precise State Model**: Define enum hierarchy for all states +2. **Identify Transition Points**: Map all places that trigger state changes +3. **Plan Migration Order**: Start with search state (highest pain point) +4. **Build Incrementally**: One state type at a time, maintain compatibility + +This analysis shows why the 'N' key bug exists - we have at least 3 different "search active" states that aren't coordinated! \ No newline at end of file diff --git a/sql-cli/docs/analysis/STATE_TRANSITION_MAPPING.md b/sql-cli/docs/analysis/STATE_TRANSITION_MAPPING.md new file mode 100644 index 00000000..cfe21b98 --- /dev/null +++ b/sql-cli/docs/analysis/STATE_TRANSITION_MAPPING.md @@ -0,0 +1,193 @@ +# State Transition Mapping: The Current Chaos + +## Overview + +Found **57 set_mode() calls** and **15+ search state operations** scattered across the codebase. This documents the current transition triggers and their required side effects. + +## Mode Transition Triggers (57 locations!) + +### 1. **Command → Results** (Query Execution) +**Trigger Locations**: +- `ui/enhanced_tui.rs:2251` - Execute query from command mode +- `ui/enhanced_tui.rs:2914` - Resume from results +- `ui/enhanced_tui.rs:2986` - Various result transitions +- `action_handler.rs:65` - Action system result mode +- Multiple other locations... + +**Required Side Effects**: +```rust +// CURRENT: Scattered manual coordination +self.buffer_mut().set_mode(AppMode::Results); +// Missing: Clear search states, reset viewport, update status + +// NEEDED: Coordinated transition +state_manager.transition(StateTransition::ExecuteQuery { + query: query_text +}); +// Should automatically: +// - Clear all search states (vim, column, fuzzy) +// - Reset viewport to (0,0) +// - Update key mapping context +// - Reset status line to results mode +// - Initialize navigation state +``` + +### 2. **Results → Command** (Back to Input) +**Trigger Locations**: +- `ui/enhanced_tui.rs:463,771` - Escape key handlers +- `action_handler.rs:60,69,99` - Action system +- Multiple exit paths... + +**Required Side Effects**: +```rust +// CURRENT: Only mode change +self.buffer_mut().set_mode(AppMode::Command); + +// NEEDED: Full restoration +state_manager.transition(StateTransition::ReturnToCommand); +// Should restore: +// - Previous query text in input +// - Cursor position in input +// - Clear all results-mode state +// - Update status line +``` + +### 3. **Search Mode Entries** (Multiple Types) +**Trigger Locations**: +- `ui/enhanced_tui.rs:2706,3000,3019` - Column search entries +- `action_handler.rs:202` - General search +- `vim_search_manager.rs:45` - Vim search start + +**Current Problems**: +```rust +// SCATTERED: Each search type managed separately +buffer.set_mode(AppMode::ColumnSearch); // Column search +vim_search_manager.start_search(); // Vim search +state_container.start_search(pattern); // Regular search + +// CONFLICTS: Multiple search states can be active! +// RESULT: 'N' key bug - system doesn't know which search is active +``` + +## Search State Transition Chaos + +### Current Search State Locations +```rust +// 1. VimSearchManager - /search functionality +enum VimSearchState { + Inactive, + Typing { pattern: String }, + Navigating { matches, current_index }, +} + +// 2. ColumnSearchState - Column name search +struct ColumnSearchState { + is_active: bool, + pattern: String, + matching_columns: Vec<(usize, String)>, +} + +// 3. Regular SearchState - Data search +struct SearchState { + is_active: bool, + pattern: String, + matches: Vec, +} + +// 4. FilterState - Fuzzy filtering +struct FilterState { + is_active: bool, + pattern: String, + // ... filter logic +} + +// PROBLEM: All can be active simultaneously! +``` + +### Search Transition Problems +```rust +// TRIGGER: User presses '/' for vim search +vim_search_manager.start_search(); +// MISSING: Clear other search states! + +// TRIGGER: User presses Escape +vim_search_manager.cancel_search(); +// MISSING: Update action context, key mappings, status line + +// TRIGGER: Execute new query +self.state_container.clear_search(); +self.state_container.clear_column_search(); +self.vim_search_manager.borrow_mut().cancel_search(); +// FRAGILE: Manual coordination, easy to miss one +``` + +## Required State Coordination Matrix + +| Transition | Buffer Mode | Search States | Viewport | Keys | Status | +|-----------|-------------|---------------|----------|------|--------| +| Execute Query | → Results | Clear ALL | Reset (0,0) | Navigation | Results info | +| Enter Vim Search | Same | Clear others → Vim | Preserve | Search nav | Search UI | +| Exit Search | Same | Clear current | Restore | Restore nav | Normal mode | +| Return to Command | → Command | Clear ALL | N/A | Input keys | Command UI | +| Switch Search Type | Same | Clear old → New | Adjust | New search | New search UI | + +## Critical Coordination Points + +### 1. **Search State Conflicts** (The 'N' key bug) +```rust +// CURRENT PROBLEM: Action context checks all sources +has_search: !buffer.get_search_pattern().is_empty() + || self.vim_search_manager.borrow().is_active() + || self.state_container.column_search().is_active + +// SOLUTION NEEDED: Single source of truth +app_state.current_search_type() -> Option +``` + +### 2. **Mode Transition Side Effects** +```rust +// CURRENT: Manual, inconsistent +set_mode(AppMode::Results); +// Sometimes clears search, sometimes doesn't +// Sometimes updates viewport, sometimes doesn't + +// NEEDED: Automatic side effects +state_manager.transition(EnterResultsMode); +// Always clears search, always resets viewport, always updates UI +``` + +### 3. **Key Mapping Context** +```rust +// CURRENT: Complex boolean logic in action context +ActionContext { + has_search: /* 3-way check */, + mode: /* buffer mode */, + has_results: /* buffer check */, + // ... more scattered flags +} + +// NEEDED: Derived from central state +ActionContext::from_app_state(&state_manager.current_state()) +``` + +## Implementation Strategy + +### Phase 1: Search State Unification +1. Create `enum SearchType { None, Vim, Column, Data, Fuzzy }` +2. Single `current_search: SearchType` in state manager +3. Replace all search state checks with single source +4. **Fix 'N' key bug immediately** + +### Phase 2: Mode Transition Coordination +1. Replace direct `set_mode()` calls with state transitions +2. Implement automatic side effects for each transition +3. Add state validation (prevent impossible combinations) + +### Phase 3: Full State Centralization +1. Move all navigation, selection, filter state to manager +2. Implement complete state history and debugging +3. Remove all scattered state management + +## Next Step: Start Small + +Begin with **search state unification** - it's the highest pain point and most isolated. The 'N' key bug fix will validate the approach before expanding to full mode management. \ No newline at end of file diff --git a/sql-cli/docs/design/CENTRALIZED_STATE_MANAGEMENT.md b/sql-cli/docs/design/CENTRALIZED_STATE_MANAGEMENT.md new file mode 100644 index 00000000..35957d07 --- /dev/null +++ b/sql-cli/docs/design/CENTRALIZED_STATE_MANAGEMENT.md @@ -0,0 +1,148 @@ +# Centralized State Management Design + +## Problem Statement + +Currently, application state is scattered across multiple components: +- Search state: Buffer, VimSearchManager, ColumnSearchState +- Navigation state: ViewportManager, Buffer navigation +- Mode state: Buffer mode, various widget states +- Action context: Multiple boolean flags computed ad-hoc + +This leads to: +- **Inconsistent state**: 'N' key stuck in search mode after clearing search +- **Complex coordination**: Action context needs to check multiple sources +- **State synchronization bugs**: Components get out of sync +- **Hard to debug**: State changes happen in many places + +## Proposed Solution: Redux-Style State Manager + +Create a central `AppStateManager` that owns all application state and publishes state transitions. + +### Architecture + +```rust +pub struct AppStateManager { + // Core state + mode: AppMode, + search_state: SearchState, + navigation_state: NavigationState, + + // Subscribers that get notified of state changes + subscribers: Vec>, +} + +pub enum StateTransition { + EnterSearchMode { search_type: SearchType }, + ExitSearchMode, + ExecuteQuery { query: String }, + NavigateToCell { row: usize, col: usize }, + // ... other transitions +} + +pub trait StateSubscriber { + fn on_state_change(&mut self, old_state: &AppState, new_state: &AppState); +} +``` + +### Consolidated State + +Instead of checking multiple sources: + +```rust +// BEFORE: Scattered checks +has_search: !buffer.get_search_pattern().is_empty() + || self.vim_search_manager.borrow().is_active() + || self.state_container.column_search().is_active + +// AFTER: Single source of truth +has_search: app_state_manager.is_search_active() +``` + +### State Transitions + +All state changes flow through the central manager: + +```rust +impl AppStateManager { + pub fn transition(&mut self, transition: StateTransition) -> Result<()> { + let old_state = self.current_state.clone(); + + // Apply transition + match transition { + StateTransition::EnterSearchMode { search_type } => { + self.search_state = SearchState::Active { search_type }; + self.mode = AppMode::Search; + } + StateTransition::ExitSearchMode => { + self.search_state = SearchState::Inactive; + self.mode = self.previous_mode; + } + // ... other transitions + } + + // Notify all subscribers + let new_state = &self.current_state; + for subscriber in &mut self.subscribers { + subscriber.on_state_change(&old_state, new_state); + } + + Ok(()) + } +} +``` + +### Integration Points + +1. **Action System**: Gets state from single source +2. **TUI Components**: Subscribe to relevant state changes +3. **Key Mapping**: Context computed from central state +4. **Render Pipeline**: Single state source for all rendering decisions + +## Implementation Plan + +### Phase 1: Core Infrastructure +- [ ] Create `AppStateManager` struct +- [ ] Define `StateTransition` enum +- [ ] Implement subscriber pattern +- [ ] Add basic search state consolidation + +### Phase 2: Migration +- [ ] Migrate search state from scattered locations +- [ ] Update action context to use central state +- [ ] Convert key handlers to use state transitions +- [ ] Update TUI rendering to subscribe to state + +### Phase 3: Expansion +- [ ] Add navigation state management +- [ ] Add mode transition management +- [ ] Add filter/sort state management +- [ ] Remove obsolete state coordination code + +## Benefits + +1. **Single Source of Truth**: All state in one place +2. **Predictable Updates**: All changes go through transition system +3. **Easy Debugging**: State history and logging in one place +4. **Consistent Behavior**: No more state synchronization bugs +5. **Testable**: Easy to unit test state transitions + +## Example: Search State Fix + +The current bug where 'N' key stays in search mode would be fixed because: + +```rust +// When executing a query +app_state_manager.transition(StateTransition::ExecuteQuery { query }); + +// StateManager automatically: +// 1. Clears all search states +// 2. Notifies action system +// 3. Updates key mapping context +// 4. Ensures 'N' key maps to toggle_line_numbers +``` + +This ensures the action system always has the correct context without manual coordination. + +## Next Steps + +Start with Phase 1 in a new branch after completing current Phase 2 refactoring work. \ No newline at end of file diff --git a/sql-cli/docs/design/STATE_COMBINATION_EXAMPLES.md b/sql-cli/docs/design/STATE_COMBINATION_EXAMPLES.md new file mode 100644 index 00000000..ebba34df --- /dev/null +++ b/sql-cli/docs/design/STATE_COMBINATION_EXAMPLES.md @@ -0,0 +1,280 @@ +# State Combination: How It Actually Works + +## The Complete State Definition + +Our **total state** is a single enum value that includes everything. No tuples needed - the enum hierarchy handles it all. + +## Concrete Examples + +### Example 1: User typing a query +```rust +// The COMPLETE state is: +let state = AppState::Command(CommandSubState::Normal); + +// This single value tells us: +// - Main mode: Command +// - Sub state: Normal (just typing) +// - NOT in results, NOT searching, NOT in help +``` + +### Example 2: User doing vim search in results +```rust +// The COMPLETE state is: +let state = AppState::Results( + ResultsSubState::VimSearch( + VimSearchState::Typing { + pattern: "active".to_string() + } + ) +); + +// This single value tells us: +// - Main mode: Results +// - Sub state: VimSearch +// - Vim search state: Currently typing the pattern "active" +``` + +### Example 3: User navigating search results +```rust +// The COMPLETE state is: +let state = AppState::Results( + ResultsSubState::VimSearch( + VimSearchState::Navigating { + pattern: "active".to_string(), + current_match: 2, + total_matches: 10, + } + ) +); + +// This single value tells us: +// - Main mode: Results +// - Sub state: VimSearch +// - Vim search state: Navigating, on match 3 of 10 +``` + +## How We Check State + +### Pattern Matching - The Rust Way +```rust +impl StateManager { + pub fn get_mode_info(&self) -> String { + match &self.current_state { + // Command mode cases + AppState::Command(sub) => { + match sub { + CommandSubState::Normal => + "Typing SQL query".to_string(), + CommandSubState::TabCompletion => + "Tab completion active".to_string(), + CommandSubState::HistorySearch { pattern } => + format!("Searching history for: {}", pattern), + } + }, + + // Results mode cases + AppState::Results(sub) => { + match sub { + ResultsSubState::Normal => + "Navigating results".to_string(), + + ResultsSubState::VimSearch(vim_state) => { + match vim_state { + VimSearchState::Typing { pattern } => + format!("Typing search: /{}", pattern), + VimSearchState::Navigating { current_match, total_matches, .. } => + format!("Search match {}/{}", current_match + 1, total_matches), + } + }, + + ResultsSubState::ColumnSearch { pattern } => + format!("Finding column: {}", pattern), + + ResultsSubState::FuzzyFilter { pattern } => + format!("Filtering: {}", pattern), + + _ => "Results mode".to_string(), + } + }, + + // Other modes + AppState::Help => "Reading help".to_string(), + AppState::Debug => "Debug view".to_string(), + AppState::PrettyQuery => "Pretty SQL view".to_string(), + } + } +} +``` + +## Convenience Methods for Common Checks + +```rust +impl StateManager { + /// Are we in command mode at all? + pub fn is_command_mode(&self) -> bool { + matches!(self.current_state, AppState::Command(_)) + } + + /// Are we in results mode at all? + pub fn is_results_mode(&self) -> bool { + matches!(self.current_state, AppState::Results(_)) + } + + /// Are we in ANY search mode? + pub fn is_search_active(&self) -> bool { + match &self.current_state { + AppState::Results(sub) => { + matches!(sub, + ResultsSubState::VimSearch(_) | + ResultsSubState::ColumnSearch { .. } | + ResultsSubState::DataSearch { .. } | + ResultsSubState::FuzzyFilter { .. } + ) + }, + _ => false, + } + } + + /// Get the search pattern if we're searching + pub fn get_search_pattern(&self) -> Option { + match &self.current_state { + AppState::Results(ResultsSubState::VimSearch(vim)) => { + match vim { + VimSearchState::Typing { pattern } | + VimSearchState::Navigating { pattern, .. } => + Some(pattern.clone()), + } + }, + AppState::Results(ResultsSubState::ColumnSearch { pattern }) | + AppState::Results(ResultsSubState::DataSearch { pattern }) | + AppState::Results(ResultsSubState::FuzzyFilter { pattern }) => + Some(pattern.clone()), + _ => None, + } + } + + /// Check if 'N' key should navigate search or toggle line numbers + pub fn should_n_key_navigate_search(&self) -> bool { + // Only if we're in vim search navigation mode + matches!( + self.current_state, + AppState::Results(ResultsSubState::VimSearch(VimSearchState::Navigating { .. })) + ) + } +} +``` + +## Setting State - Real Examples + +```rust +// Example: User presses '/' to start vim search +self.state_manager.set_state( + AppState::Results( + ResultsSubState::VimSearch( + VimSearchState::Typing { pattern: String::new() } + ) + ), + "user_pressed_slash" +); + +// Example: User presses Enter to execute query +self.state_manager.set_state( + AppState::Results(ResultsSubState::Normal), + "execute_query" +); + +// Example: User presses Escape while searching +self.state_manager.set_state( + AppState::Results(ResultsSubState::Normal), + "escape_from_search" +); + +// Example: User types in search +if let AppState::Results(ResultsSubState::VimSearch(VimSearchState::Typing { pattern })) = + &self.state_manager.current_state { + + let new_pattern = format!("{}a", pattern); // User typed 'a' + self.state_manager.set_state( + AppState::Results( + ResultsSubState::VimSearch( + VimSearchState::Typing { pattern: new_pattern } + ) + ), + "search_input_char" + ); +} +``` + +## The Power of This Approach + +### 1. **Single Source of Truth** +```rust +// Our ENTIRE application state is ONE value: +let complete_state: AppState = self.state_manager.current_state; + +// Not multiple booleans: +// ❌ is_command_mode && !is_searching && !has_completion && ... + +// Just one enum: +// ✅ AppState::Command(CommandSubState::Normal) +``` + +### 2. **Impossible States Can't Exist** +```rust +// This is IMPOSSIBLE to represent: +// ❌ Command mode AND VimSearch active +// The type system prevents it! + +// You can only have valid states: +// ✅ AppState::Command(CommandSubState::Normal) +// ✅ AppState::Results(ResultsSubState::VimSearch(...)) +``` + +### 3. **Clear State Transitions** +```rust +// When user presses 'N' key: +match self.state_manager.current_state { + AppState::Results(ResultsSubState::VimSearch(VimSearchState::Navigating { .. })) => { + // We're navigating search results, so N = next match + self.next_search_match(); + }, + _ => { + // We're NOT in search navigation, so N = toggle line numbers + self.toggle_line_numbers(); + } +} +// The 'N' key bug is FIXED by design! +``` + +## Visual Representation + +``` +AppState (Total State) +├── Command +│ ├── Normal ← Complete state: AppState::Command(CommandSubState::Normal) +│ ├── TabCompletion ← Complete state: AppState::Command(CommandSubState::TabCompletion) +│ └── HistorySearch { pattern } ← Complete state: AppState::Command(CommandSubState::HistorySearch { .. }) +│ +├── Results +│ ├── Normal ← Complete state: AppState::Results(ResultsSubState::Normal) +│ ├── VimSearch +│ │ ├── Typing { pattern } ← Complete state: AppState::Results(ResultsSubState::VimSearch(VimSearchState::Typing { .. })) +│ │ └── Navigating { pattern, current, total } ← Complete state: AppState::Results(ResultsSubState::VimSearch(VimSearchState::Navigating { .. })) +│ ├── ColumnSearch { pattern } ← Complete state: AppState::Results(ResultsSubState::ColumnSearch { .. }) +│ ├── DataSearch { pattern } ← Complete state: AppState::Results(ResultsSubState::DataSearch { .. }) +│ └── FuzzyFilter { pattern } ← Complete state: AppState::Results(ResultsSubState::FuzzyFilter { .. }) +│ +├── Help ← Complete state: AppState::Help +├── Debug ← Complete state: AppState::Debug +└── PrettyQuery ← Complete state: AppState::PrettyQuery +``` + +Each path from root to leaf is ONE complete state value! + +## Summary + +- **No tuples needed** - The enum hierarchy IS the complete state +- **One variable** - `self.current_state` contains EVERYTHING +- **Type safe** - Can't have invalid state combinations +- **Pattern matching** - Rust's match makes state checks elegant +- **Single source of truth** - Check one place for any state question \ No newline at end of file diff --git a/sql-cli/docs/design/STATE_MANAGER_INCREMENTAL.md b/sql-cli/docs/design/STATE_MANAGER_INCREMENTAL.md new file mode 100644 index 00000000..3baf9dee --- /dev/null +++ b/sql-cli/docs/design/STATE_MANAGER_INCREMENTAL.md @@ -0,0 +1,340 @@ +# Incremental State Manager Design + +## Phase 1: Shell Struct with Centralized Flow + +Start with a simple shell that **all state changes flow through**, with comprehensive logging and status line rendering. + +## State Definition: Hierarchical Enums + +```rust +/// Primary application mode +#[derive(Debug, Clone, PartialEq)] +pub enum AppState { + /// User typing SQL queries + Command(CommandSubState), + + /// Navigating query results + Results(ResultsSubState), + + /// Special modes + Help, + Debug, + PrettyQuery, +} + +/// Command mode substates +#[derive(Debug, Clone, PartialEq)] +pub enum CommandSubState { + Normal, // Regular typing + TabCompletion, // Tab completion active + HistorySearch { pattern: String }, // Ctrl+R search +} + +/// Results mode substates +#[derive(Debug, Clone, PartialEq)] +pub enum ResultsSubState { + Normal, // Regular navigation + VimSearch(VimSearchState), // / search + ColumnSearch { pattern: String }, // Column name search + DataSearch { pattern: String }, // Data content search + FuzzyFilter { pattern: String }, // Live filtering + Selection(SelectionMode), // Cell/row selection + JumpToRow, // Jump to row number +} + +/// Vim search specific states +#[derive(Debug, Clone, PartialEq)] +pub enum VimSearchState { + Typing { pattern: String }, + Navigating { + pattern: String, + current_match: usize, + total_matches: usize, + }, +} + +/// Selection modes +#[derive(Debug, Clone, PartialEq)] +pub enum SelectionMode { + Cell, + Row, + Column, + Range, +} +``` + +## Shell State Manager + +```rust +use tracing::{info, debug}; + +pub struct StateManager { + current_state: AppState, + previous_state: Option, + + // State history for debugging + state_history: VecDeque<(Instant, AppState, String)>, // (when, state, trigger) + + // Transition counter for debugging + transition_count: usize, +} + +impl StateManager { + pub fn new() -> Self { + let initial = AppState::Command(CommandSubState::Normal); + info!(target: "state", "StateManager initialized with {:?}", initial); + + Self { + current_state: initial.clone(), + previous_state: None, + state_history: VecDeque::with_capacity(100), + transition_count: 0, + } + } + + /// Central state transition point - EVERYTHING flows through here + pub fn set_state(&mut self, new_state: AppState, trigger: &str) { + let old_state = self.current_state.clone(); + + // Log every transition + info!(target: "state", + "[#{}] State transition: {:?} -> {:?} (trigger: {})", + self.transition_count, old_state, new_state, trigger + ); + + // Update state + self.previous_state = Some(old_state.clone()); + self.current_state = new_state.clone(); + self.transition_count += 1; + + // Keep history (last 100 transitions) + self.state_history.push_back(( + Instant::now(), + new_state.clone(), + trigger.to_string() + )); + if self.state_history.len() > 100 { + self.state_history.pop_front(); + } + + // Log side effects needed + self.log_required_side_effects(&old_state, &new_state); + } + + /// Get current state + pub fn current(&self) -> &AppState { + &self.current_state + } + + /// Check if we're in any search mode + pub fn is_search_active(&self) -> bool { + match &self.current_state { + AppState::Results(sub) => match sub { + ResultsSubState::VimSearch(_) | + ResultsSubState::ColumnSearch { .. } | + ResultsSubState::DataSearch { .. } | + ResultsSubState::FuzzyFilter { .. } => true, + _ => false, + }, + _ => false, + } + } + + /// Get search type if active + pub fn active_search_type(&self) -> Option { + match &self.current_state { + AppState::Results(ResultsSubState::VimSearch(_)) => Some(SearchType::Vim), + AppState::Results(ResultsSubState::ColumnSearch { .. }) => Some(SearchType::Column), + AppState::Results(ResultsSubState::DataSearch { .. }) => Some(SearchType::Data), + AppState::Results(ResultsSubState::FuzzyFilter { .. }) => Some(SearchType::Fuzzy), + _ => None, + } + } + + /// Get display string for status line + pub fn status_display(&self) -> String { + match &self.current_state { + AppState::Command(sub) => match sub { + CommandSubState::Normal => "COMMAND".to_string(), + CommandSubState::TabCompletion => "COMMAND [Tab]".to_string(), + CommandSubState::HistorySearch { pattern } => + format!("COMMAND [History: {}]", pattern), + }, + AppState::Results(sub) => match sub { + ResultsSubState::Normal => "RESULTS".to_string(), + ResultsSubState::VimSearch(vim) => match vim { + VimSearchState::Typing { pattern } => + format!("RESULTS [/{}]", pattern), + VimSearchState::Navigating { current_match, total_matches, .. } => + format!("RESULTS [Search {}/{}]", current_match + 1, total_matches), + }, + ResultsSubState::ColumnSearch { pattern } => + format!("RESULTS [Col: {}]", pattern), + ResultsSubState::DataSearch { pattern } => + format!("RESULTS [Find: {}]", pattern), + ResultsSubState::FuzzyFilter { pattern } => + format!("RESULTS [Filter: {}]", pattern), + ResultsSubState::Selection(mode) => + format!("RESULTS [Select: {:?}]", mode), + ResultsSubState::JumpToRow => "RESULTS [Jump]".to_string(), + }, + AppState::Help => "HELP".to_string(), + AppState::Debug => "DEBUG".to_string(), + AppState::PrettyQuery => "PRETTY SQL".to_string(), + } + } + + /// Get debug dump of state history + pub fn debug_history(&self) -> String { + let mut output = format!("State History (last 10 transitions):\n"); + for (time, state, trigger) in self.state_history.iter().rev().take(10) { + output.push_str(&format!( + " {:?} ago: {:?} ({})\n", + time.elapsed(), + state, + trigger + )); + } + output.push_str(&format!("\nTotal transitions: {}\n", self.transition_count)); + output + } + + fn log_required_side_effects(&self, old: &AppState, new: &AppState) { + debug!(target: "state", "Side effects needed:"); + + // Entering Results mode + if !matches!(old, AppState::Results(_)) && matches!(new, AppState::Results(_)) { + debug!(target: "state", " - Clear all search states"); + debug!(target: "state", " - Reset viewport to (0,0)"); + debug!(target: "state", " - Update key mapping to navigation"); + } + + // Exiting any search + if self.was_search_active(old) && !self.is_state_search(new) { + debug!(target: "state", " - Clear search UI"); + debug!(target: "state", " - Restore normal key mappings"); + debug!(target: "state", " - Update status line"); + } + + // Entering search + if !self.was_search_active(old) && self.is_state_search(new) { + debug!(target: "state", " - Clear other search states"); + debug!(target: "state", " - Setup search UI"); + debug!(target: "state", " - Capture input for search"); + } + } + + fn was_search_active(&self, state: &AppState) -> bool { + match state { + AppState::Results(sub) => matches!(sub, + ResultsSubState::VimSearch(_) | + ResultsSubState::ColumnSearch { .. } | + ResultsSubState::DataSearch { .. } | + ResultsSubState::FuzzyFilter { .. } + ), + _ => false, + } + } + + fn is_state_search(&self, state: &AppState) -> bool { + self.was_search_active(state) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum SearchType { + Vim, + Column, + Data, + Fuzzy, +} +``` + +## Integration Points + +### 1. Replace all `set_mode()` calls: + +```rust +// BEFORE: Direct mode setting +self.buffer_mut().set_mode(AppMode::Results); + +// AFTER: Through state manager +self.state_manager.set_state( + AppState::Results(ResultsSubState::Normal), + "execute_query" +); +``` + +### 2. Update action context: + +```rust +// BEFORE: Complex multi-source check +has_search: !buffer.get_search_pattern().is_empty() + || self.vim_search_manager.borrow().is_active() + || self.state_container.column_search().is_active + +// AFTER: Single source +has_search: self.state_manager.is_search_active() +``` + +### 3. Status line rendering: + +```rust +// Add state display to status line +let state_display = self.state_manager.status_display(); +spans.push(Span::styled( + format!(" [{}] ", state_display), + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) +)); +``` + +### 4. Debug view (F5): + +```rust +// Show state history in debug view +if self.show_debug { + let history = self.state_manager.debug_history(); + // Render history in debug panel +} +``` + +## Migration Strategy + +### Step 1: Add StateManager to EnhancedTuiApp +```rust +pub struct EnhancedTuiApp { + // ... existing fields + state_manager: StateManager, // NEW +} +``` + +### Step 2: Replace one set_mode at a time +Start with the most common transitions: +1. Command → Results (execute query) +2. Results → Search modes +3. Search → Normal Results (escape) + +### Step 3: Add logging and observe +With comprehensive logging, we can see: +- All state transitions +- Missing side effects +- Conflicting states +- Patterns to optimize + +### Step 4: Fix issues incrementally +As we observe the logs: +- Add missing side effects +- Prevent invalid transitions +- Consolidate duplicate logic + +## Benefits of This Approach + +1. **Immediate Visibility**: See every state change in logs and status line +2. **Incremental Migration**: Replace set_mode() calls one at a time +3. **No Big Bang**: Existing code continues to work +4. **Debug-Friendly**: Complete state history for troubleshooting +5. **Single Source**: Even as a shell, provides single source of truth + +## Next Step + +Implement the basic StateManager struct and replace just ONE set_mode() call to validate the approach. Once we see it working with comprehensive logging, we can gradually migrate all 57 locations. \ No newline at end of file diff --git a/sql-cli/docs/design/STATE_MANAGER_PARALLEL.md b/sql-cli/docs/design/STATE_MANAGER_PARALLEL.md new file mode 100644 index 00000000..52e8d681 --- /dev/null +++ b/sql-cli/docs/design/STATE_MANAGER_PARALLEL.md @@ -0,0 +1,249 @@ +# Parallel State Manager: Add Without Breaking + +## The Problem +Even a "shell" state manager requires changing all 57 `set_mode()` calls. That's a big bang! + +## The Solution: Run in Parallel +Add the StateManager **alongside** existing state, not replacing it. The existing code continues to work while we observe and learn. + +## Phase 1: Shadow State Manager + +```rust +/// Shadows the existing state system - doesn't control anything yet +pub struct ShadowStateManager { + state: AppState, + history: VecDeque<(Instant, AppState, String)>, + + // Track discrepancies between our state and actual state + discrepancies: Vec, +} + +impl ShadowStateManager { + /// Called AFTER the existing set_mode() - just observes + pub fn observe_mode_change(&mut self, mode: AppMode, trigger: &str) { + let new_state = self.mode_to_state(mode); + + info!(target: "shadow_state", + "Observed: {:?} -> {:?} ({})", + self.state, new_state, trigger + ); + + self.state = new_state; + self.history.push_back((Instant::now(), self.state.clone(), trigger.to_string())); + } + + /// Called when we observe search state changes + pub fn observe_search_start(&mut self, search_type: &str) { + info!(target: "shadow_state", "Observed search start: {}", search_type); + // Update our shadow state based on observation + } + + /// Check if our state matches reality + pub fn verify_state(&mut self, actual: &ActualState) { + if !self.matches_actual(actual) { + let msg = format!( + "State mismatch! Shadow: {:?}, Actual: {:?}", + self.state, actual + ); + warn!(target: "shadow_state", "{}", msg); + self.discrepancies.push(msg); + } + } + + /// Get what we THINK the state should be + pub fn predicted_state(&self) -> &AppState { + &self.state + } +} +``` + +## Integration: Minimal Touch Points + +### Step 1: Add to EnhancedTuiApp +```rust +pub struct EnhancedTuiApp { + // ... existing fields unchanged + + #[cfg(feature = "shadow-state")] + shadow_state: ShadowStateManager, // NEW - only when testing +} +``` + +### Step 2: Add observation calls (not replacement!) +```rust +// EXISTING CODE UNCHANGED: +self.buffer_mut().set_mode(AppMode::Results); + +// ADD AFTER (doesn't affect existing): +#[cfg(feature = "shadow-state")] +self.shadow_state.observe_mode_change(AppMode::Results, "execute_query"); +``` + +### Step 3: Add verification in render +```rust +fn render_status_line(&self, f: &mut Frame, area: Rect) { + // ... existing rendering code ... + + #[cfg(feature = "shadow-state")] + { + // Show shadow state in status line for comparison + let shadow_display = format!("[Shadow: {}]", + self.shadow_state.predicted_state()); + // Render it in corner for debugging + } +} +``` + +## Even More Incremental: Wrapper Pattern + +```rust +/// Wraps existing buffer to intercept state changes +pub struct StateTrackingBuffer<'a> { + inner: &'a mut Buffer, + state_tracker: &'a mut ShadowStateManager, +} + +impl<'a> StateTrackingBuffer<'a> { + pub fn set_mode(&mut self, mode: AppMode) { + // Call the real implementation + self.inner.set_mode(mode.clone()); + + // Track the state change + self.state_tracker.observe_mode_change(mode, "wrapped_call"); + } + + // Delegate everything else unchanged + pub fn get_mode(&self) -> AppMode { + self.inner.get_mode() + } +} + +// Usage - wrap only where we want to observe: +let mut tracking_buffer = StateTrackingBuffer { + inner: self.buffer_mut(), + state_tracker: &mut self.shadow_state, +}; +tracking_buffer.set_mode(AppMode::Results); +``` + +## Incremental Migration Path + +### Phase 1: Pure Observation (No Risk!) +1. Add `ShadowStateManager` with feature flag +2. Add ~5 observation points at key locations: + - Execute query + - Start search + - Exit search + - Return to command + - Switch modes +3. Run and observe logs - learn the patterns +4. No functionality changes - can't break anything! + +### Phase 2: Verification (Find Discrepancies) +1. Add verification checks +2. Compare shadow state with actual state +3. Log mismatches to understand missing transitions +4. Still no functionality changes + +### Phase 3: Single Source Experiment +1. Pick ONE feature (like the N key mapping) +2. Use shadow state for just that decision: +```rust +// Just ONE place uses the new state: +let should_search = if cfg!(feature = "use-shadow-state") { + self.shadow_state.is_search_active() // NEW +} else { + // Existing 3-way check + !buffer.get_search_pattern().is_empty() + || self.vim_search_manager.borrow().is_active() + || self.state_container.column_search().is_active +}; +``` +3. If it works, expand usage +4. If not, feature flag off! + +### Phase 4: Gradual Takeover +1. One by one, switch decisions to use shadow state +2. Each behind a feature flag initially +3. Once all decisions use shadow state, make it primary +4. Remove old state code + +## Actual Starting Code + +```rust +// src/ui/shadow_state.rs - NEW FILE +use crate::buffer::AppMode; + +#[derive(Debug, Clone)] +pub enum AppState { + Command, + Results, + Search, + // Start simple! +} + +pub struct ShadowStateManager { + state: AppState, + transition_count: usize, +} + +impl ShadowStateManager { + pub fn new() -> Self { + Self { + state: AppState::Command, + transition_count: 0, + } + } + + pub fn observe(&mut self, mode: AppMode) { + let new_state = match mode { + AppMode::Command => AppState::Command, + AppMode::Results => AppState::Results, + AppMode::Search | AppMode::ColumnSearch => AppState::Search, + _ => return, // Ignore others for now + }; + + if !matches!((&self.state, &new_state), (AppState::Search, AppState::Search)) { + self.transition_count += 1; + info!(target: "shadow", + "[#{}] {} -> {}", + self.transition_count, + format!("{:?}", self.state), + format!("{:?}", new_state) + ); + } + + self.state = new_state; + } + + pub fn is_search(&self) -> bool { + matches!(self.state, AppState::Search) + } +} +``` + +Then add just ONE line after existing code: +```rust +self.buffer_mut().set_mode(AppMode::Results); +#[cfg(feature = "shadow-state")] +self.shadow_state.observe(AppMode::Results); // Just observe! +``` + +## Benefits of This Approach + +1. **Zero Risk**: Observation doesn't change behavior +2. **Learn First**: Understand patterns before changing +3. **Feature Flags**: Turn off instantly if issues +4. **Incremental**: Each step is tiny and reversible +5. **Parallel Running**: Old and new side by side + +## The Absolutely Minimal Start + +1. Create `shadow_state.rs` with basic enum +2. Add `ShadowStateManager` to app struct (feature flagged) +3. Add ONE observe call after execute_query +4. Log and watch +5. Add one more observe call +6. Repeat + +This way we never need a big bang - we're just adding logging alongside existing code! \ No newline at end of file diff --git a/sql-cli/docs/refactoring/phase2_tui_simplification_plan.md b/sql-cli/docs/refactoring/phase2_tui_simplification_plan.md new file mode 100644 index 00000000..b29dd749 --- /dev/null +++ b/sql-cli/docs/refactoring/phase2_tui_simplification_plan.md @@ -0,0 +1,168 @@ +# Phase 2 TUI Simplification Plan + +## Overview + +After completing Phase 1 (low-hanging fruit cleanup), we're entering **Phase 2: Core Function Decomposition**. The goal is to transform the TUI from a massive monolithic handler into a simple dispatcher that orchestrates smaller, focused sub-functions. + +## Current State (Post Phase 1) + +### ✅ Completed in Phase 1 +- **Redux-style pattern established** (TableWidgetManager, RenderState) +- **Dead code eliminated** (duplicate function key handlers) +- **Search navigation centralized** (SearchManager, VimSearchManager integration) +- **Action system foundation** laid for simple cases +- **Navigation pipeline unified** (hjkl, search all use TableWidgetManager) + +### ❌ Remaining Challenges +- **Massive functions still exist** (`handle_command_input`, `handle_results_input`) +- **Complex action system cases** not yet migrated +- **Vim search still mixed** with regular search logic +- **Widget state management** still ad-hoc (not Redux) + +## Phase 2 Strategy: Iterative Function Decomposition + +### Core Principle +**"Simplify until TUI becomes a simple dispatcher"** + +Each refactoring iteration should: +1. **Identify common behavior patterns** in massive functions +2. **Extract to focused sub-functions** (following `try_handle_*` pattern) +3. **Reduce main function to orchestration only** +4. **Reveal new refactoring opportunities** for next iteration + +### Target Architecture +```rust +fn handle_command_input(&mut self, key: KeyEvent) -> Result { + // Try specialized handlers first + if let Some(result) = self.try_handle_buffer_operations(&key)? { return Ok(result); } + if let Some(result) = self.try_handle_function_keys(&key)? { return Ok(result); } + if let Some(result) = self.try_handle_history_navigation(&key)? { return Ok(result); } + if let Some(result) = self.try_handle_text_editing(&key)? { return Ok(result); } + if let Some(result) = self.try_handle_completion(&key)? { return Ok(result); } + if let Some(result) = self.try_handle_mode_transitions(&key)? { return Ok(result); } + + // Minimal fallback handling + self.handle_remaining_input(key) +} +``` + +## Phase 2 Branch Roadmap + +### **Branch 1: `tui_function_decomposition_v1`** +**Target:** `handle_command_input` function decomposition + +**Current State:** ~200+ lines of mixed responsibilities +**Goal:** ~50 lines of orchestration + focused sub-functions + +**Extraction Candidates:** +- `try_handle_history_navigation` - Ctrl+P/N, Alt+Up/Down history commands +- `try_handle_text_editing` - Kill line, word movement, clipboard operations +- `try_handle_completion` - Tab completion, suggestion logic +- `try_handle_mode_transitions` - Enter key, mode switching logic + +**Success Criteria:** +- Main function reduced to orchestration pattern +- Each sub-function handles single responsibility +- No behavior changes (same functionality) +- Clear separation of concerns + +### **Branch 2: `tui_results_decomposition_v1`** +**Target:** `handle_results_input` function decomposition + +**Current State:** ~300+ lines handling all results mode input +**Goal:** ~75 lines of orchestration + focused sub-functions + +**Extraction Candidates:** +- `try_handle_navigation_keys` - hjkl, page up/down, g/G movements +- `try_handle_column_operations` - pin, hide, sort, move operations +- `try_handle_search_operations` - /, ?, n/N search navigation +- `try_handle_yank_operations` - y-prefix chord sequences +- `try_handle_mode_exits` - Escape, q, return to command mode + +### **Branch 3: `tui_action_system_completion_v1`** +**Target:** Migrate remaining complex action system cases + +**Focus Areas:** +- Text editing operations still in switch statements +- Complex mode transition logic +- Buffer management direct calls +- Completion system integration + +### **Branch 4: `tui_vim_extraction_v1`** +**Target:** Extract vim search as independent component + +**Prerequisites:** TUI must be simplified enough to see clean extraction points + +**Goals:** +- Create self-contained `VimSearchWidget` +- Remove vim logic from `SearchModesWidget` +- Apply Redux pattern to vim search state +- Plugin-like architecture (vim as optional component) + +### **Branch 5+: Widget Redux Migration** +**Target:** Apply Redux pattern to all remaining widgets + +**Candidates:** +- HelpWidget → Redux state management +- StatsWidget → Centralized state +- DebugWidget → State management +- InputWidget → Redux integration + +## Implementation Guidelines + +### Function Extraction Pattern +Follow the established `try_handle_*` pattern: + +```rust +fn try_handle_category(&mut self, key: &KeyEvent) -> Result> { + match key.code { + // Handle specific keys for this category + KeyCode::SpecificKey => { + // Focused logic for this behavior + Ok(Some(false)) + } + _ => Ok(None), // Not handled by this category + } +} +``` + +### Redux Pattern Application +For state-heavy components, follow TableWidgetManager model: + +```rust +pub struct WidgetManager { + state: WidgetState, + render_state: RenderState, +} + +impl WidgetManager { + pub fn handle_action(&mut self, action: WidgetAction) { + // Centralized state updates + self.render_state.mark_dirty(RenderReason::StateChange); + } +} +``` + +## Success Metrics + +### Per-Branch Metrics +- **Lines of code reduction** in main functions +- **Cyclomatic complexity** decrease +- **Single responsibility** adherence +- **Zero behavior regression** (tests pass) + +### Overall Phase 2 Success +- **TUI becomes simple dispatcher** (~200 total lines in main handlers) +- **Each function has single responsibility** +- **New refactoring opportunities revealed** for Phase 3 +- **Redux pattern ready for widget migration** + +## Phase 3 Preview + +After Phase 2 completion: +- **Micro-refactoring opportunities** will be visible +- **Widget boundaries** will be clearer +- **State management patterns** will be established +- **Plugin architecture** will be feasible + +The iterative approach ensures each simplification reveals the next logical step, maintaining momentum toward the ultimate goal of a clean, maintainable TUI architecture. \ No newline at end of file diff --git a/sql-cli/src/app_state_container.rs b/sql-cli/src/app_state_container.rs index d7c4311b..bf9060f7 100644 --- a/sql-cli/src/app_state_container.rs +++ b/sql-cli/src/app_state_container.rs @@ -14,11 +14,10 @@ use arboard::Clipboard; use chrono::{DateTime, Local}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::cell::RefCell; -use std::cmp::Ordering; use std::collections::{HashMap, VecDeque}; use std::fmt; use std::time::{Duration, Instant}; -use tracing::{debug, error, info, trace}; +use tracing::{info, trace}; /// Platform type for key handling #[derive(Debug, Clone, PartialEq)] diff --git a/sql-cli/src/data/data_view.rs b/sql-cli/src/data/data_view.rs index b08981a1..ffa7ed85 100644 --- a/sql-cli/src/data/data_view.rs +++ b/sql-cli/src/data/data_view.rs @@ -7,7 +7,7 @@ use tracing::{debug, info}; use crate::data::data_provider::DataProvider; use crate::data::datatable::{DataRow, DataTable, DataValue}; -use crate::data::datavalue_compare::{compare_datavalues, compare_optional_datavalues}; +use crate::data::datavalue_compare::compare_optional_datavalues; /// Sort order for columns #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/sql-cli/src/debug/buffer_debug.rs b/sql-cli/src/debug/buffer_debug.rs index 0ed6b409..bd293713 100644 --- a/sql-cli/src/debug/buffer_debug.rs +++ b/sql-cli/src/debug/buffer_debug.rs @@ -1,4 +1,4 @@ -use crate::buffer::{AppMode, BufferAPI}; +use crate::buffer::BufferAPI; use crate::debug::debug_trace::{DebugSection, DebugSectionBuilder, DebugTrace, Priority}; use std::sync::Arc; diff --git a/sql-cli/src/ui/enhanced_tui.rs b/sql-cli/src/ui/enhanced_tui.rs index 49e944cc..0af14111 100644 --- a/sql-cli/src/ui/enhanced_tui.rs +++ b/sql-cli/src/ui/enhanced_tui.rs @@ -32,9 +32,7 @@ use crate::ui::table_widget_manager::TableWidgetManager; use crate::ui::traits::{ BufferManagementBehavior, ColumnBehavior, InputBehavior, NavigationBehavior, YankBehavior, }; -use crate::ui::viewport_manager::{ - ColumnPackingMode, NavigationResult, ViewportEfficiency, ViewportManager, -}; +use crate::ui::viewport_manager::{ColumnPackingMode, ViewportEfficiency, ViewportManager}; use crate::utils::logging::LogRingBuffer; use crate::widget_traits::DebugInfoProvider; use crate::widgets::debug_widget::DebugWidget; @@ -180,7 +178,9 @@ impl EnhancedTuiApp { has_results: buffer.get_dataview().is_some(), has_filter: !buffer.get_filter_pattern().is_empty() || !buffer.get_fuzzy_filter_pattern().is_empty(), - has_search: !buffer.get_search_pattern().is_empty(), + has_search: !buffer.get_search_pattern().is_empty() + || self.vim_search_manager.borrow().is_active() + || self.state_container.column_search().is_active, row_count: buffer.get_dataview().map_or(0, |v| v.row_count()), column_count: buffer.get_dataview().map_or(0, |v| v.column_count()), current_row: nav.selected_row, @@ -1381,198 +1381,234 @@ impl EnhancedTuiApp { } } - fn run_app(&mut self, terminal: &mut Terminal) -> Result<()> { - // Initialize viewport size before first draw + /// Initialize viewport and perform initial draw + fn initialize_viewport(&mut self, terminal: &mut Terminal) -> Result<()> { self.update_viewport_size(); info!(target: "navigation", "Initial viewport size update completed"); - - // Initial draw terminal.draw(|f| self.ui(f))?; + Ok(()) + } - loop { - // Check for debounced actions from search modes widget (handles all search modes including vim search) - if self.search_modes_widget.is_active() { - if let Some(action) = self.search_modes_widget.check_debounce() { - let mut needs_redraw = false; - match action { - SearchModesAction::ExecuteDebounced(mode, pattern) => { - info!(target: "search", "=== DEBOUNCED SEARCH EXECUTING ==="); - info!(target: "search", "Mode: {:?}, Pattern: '{}', AppMode: {:?}", - mode, pattern, self.buffer().get_mode()); - - // Log current position before search - { - let nav = self.state_container.navigation(); - info!(target: "search", "BEFORE: nav.selected_row={}, nav.selected_column={}", - nav.selected_row, nav.selected_column); - info!(target: "search", "BEFORE: buffer.selected_row={:?}, buffer.current_column={}", - self.buffer().get_selected_row(), self.buffer().get_current_column()); - } + /// Handle debounced search actions, returns true if exit is requested + fn try_handle_debounced_actions( + &mut self, + terminal: &mut Terminal, + ) -> Result { + if !self.search_modes_widget.is_active() { + return Ok(false); + } - self.execute_search_action(mode, pattern); + if let Some(action) = self.search_modes_widget.check_debounce() { + let mut needs_redraw = false; + match action { + SearchModesAction::ExecuteDebounced(mode, pattern) => { + info!(target: "search", "=== DEBOUNCED SEARCH EXECUTING ==="); + info!(target: "search", "Mode: {:?}, Pattern: '{}', AppMode: {:?}", + mode, pattern, self.buffer().get_mode()); - // Log position after search - { - let nav = self.state_container.navigation(); - info!(target: "search", "AFTER: nav.selected_row={}, nav.selected_column={}", - nav.selected_row, nav.selected_column); - info!(target: "search", "AFTER: buffer.selected_row={:?}, buffer.current_column={}", - self.buffer().get_selected_row(), self.buffer().get_current_column()); - - // Check ViewportManager state - let viewport_manager = self.viewport_manager.borrow(); - if let Some(ref vm) = *viewport_manager { - info!(target: "search", "AFTER: ViewportManager crosshair=({}, {})", - vm.get_crosshair_row(), vm.get_crosshair_col()); - } - } + // Log current position before search + { + let nav = self.state_container.navigation(); + info!(target: "search", "BEFORE: nav.selected_row={}, nav.selected_column={}", + nav.selected_row, nav.selected_column); + info!(target: "search", "BEFORE: buffer.selected_row={:?}, buffer.current_column={}", + self.buffer().get_selected_row(), self.buffer().get_current_column()); + } - info!(target: "search", "=== FORCING REDRAW ==="); - // CRITICAL: Force immediate redraw after search navigation - needs_redraw = true; + self.execute_search_action(mode, pattern); + + // Log position after search + { + let nav = self.state_container.navigation(); + info!(target: "search", "AFTER: nav.selected_row={}, nav.selected_column={}", + nav.selected_row, nav.selected_column); + info!(target: "search", "AFTER: buffer.selected_row={:?}, buffer.current_column={}", + self.buffer().get_selected_row(), self.buffer().get_current_column()); + + // Check ViewportManager state + let viewport_manager = self.viewport_manager.borrow(); + if let Some(ref vm) = *viewport_manager { + info!(target: "search", "AFTER: ViewportManager crosshair=({}, {})", + vm.get_crosshair_row(), vm.get_crosshair_col()); } - _ => {} } - // Redraw immediately if search moved the cursor OR if TableWidgetManager needs render - if needs_redraw || self.table_widget_manager.borrow().needs_render() { - info!(target: "search", "Triggering redraw: needs_redraw={}, table_needs_render={}", - needs_redraw, self.table_widget_manager.borrow().needs_render()); - terminal.draw(|f| self.ui(f))?; - self.table_widget_manager.borrow_mut().rendered(); - } + info!(target: "search", "=== FORCING REDRAW ==="); + // CRITICAL: Force immediate redraw after search navigation + needs_redraw = true; } + _ => {} } - // Use poll with timeout to allow checking for debounced actions - if event::poll(std::time::Duration::from_millis(50))? { - match event::read()? { - Event::Key(key) => { - // On Windows, filter out key release events - only handle key press - // This prevents double-triggering of toggles - if key.kind != crossterm::event::KeyEventKind::Press { - continue; - } + // Redraw immediately if search moved the cursor OR if TableWidgetManager needs render + if needs_redraw || self.table_widget_manager.borrow().needs_render() { + info!(target: "search", "Triggering redraw: needs_redraw={}, table_needs_render={}", + needs_redraw, self.table_widget_manager.borrow().needs_render()); + terminal.draw(|f| self.ui(f))?; + self.table_widget_manager.borrow_mut().rendered(); + } + } + Ok(false) + } - // SAFETY: Always allow Ctrl-C to exit, regardless of app state - // This prevents getting stuck in unresponsive states - if key.code == KeyCode::Char('c') - && key.modifiers.contains(KeyModifiers::CONTROL) - { - info!(target: "app", "Ctrl-C detected, forcing exit"); - break; - } + /// Handle chord processing for Results mode, returns true if exit is requested + fn try_handle_chord_processing(&mut self, key: crossterm::event::KeyEvent) -> Result { + let chord_result = self.key_chord_handler.process_key(key); + debug!("Chord handler returned: {:?}", chord_result); + + match chord_result { + ChordResult::CompleteChord(action) => { + // Handle completed chord actions through the action system + debug!("Chord completed: {:?}", action); + // Clear chord mode in renderer + self.key_sequence_renderer.clear_chord_mode(); + // Use the action system to handle the chord action + self.try_handle_action( + action, + &ActionContext { + mode: self.buffer().get_mode(), + selection_mode: self.state_container.get_selection_mode(), + has_results: self.buffer().get_dataview().is_some(), + has_filter: false, + has_search: false, + row_count: self.get_row_count(), + column_count: self.get_column_count(), + current_row: self.state_container.get_table_selected_row().unwrap_or(0), + current_column: self.buffer().get_current_column(), + }, + )?; + Ok(false) + } + ChordResult::PartialChord(description) => { + // Update status to show chord mode + self.buffer_mut().set_status_message(description.clone()); + // Update chord mode in renderer with available completions + // Extract the completions from the description + if description.contains("y=row") { + self.key_sequence_renderer + .set_chord_mode(Some("y(a,c,q,r,v)".to_string())); + } else { + self.key_sequence_renderer + .set_chord_mode(Some(description.clone())); + } + Ok(false) // Don't exit, waiting for more keys + } + ChordResult::Cancelled => { + self.buffer_mut() + .set_status_message("Chord cancelled".to_string()); + // Clear chord mode in renderer + self.key_sequence_renderer.clear_chord_mode(); + Ok(false) + } + ChordResult::SingleKey(single_key) => { + // Not a chord, process normally + self.handle_results_input(single_key) + } + } + } - // Record key press for visual indicator - let key_display = format_key_for_display(&key); - self.key_indicator.record_key(key_display.clone()); - self.key_sequence_renderer.record_key(key_display); - - // CRITICAL: Process through chord handler FIRST for Results mode - // This allows chord sequences like 'yv' to work correctly - let should_exit = if self.buffer().get_mode() == AppMode::Results { - // In Results mode, check chord handler first - let chord_result = self.key_chord_handler.process_key(key); - debug!("Chord handler returned: {:?}", chord_result); - - match chord_result { - ChordResult::CompleteChord(action) => { - // Handle completed chord actions through the action system - debug!("Chord completed: {:?}", action); - // Clear chord mode in renderer - self.key_sequence_renderer.clear_chord_mode(); - // Use the action system to handle the chord action - self.try_handle_action( - action, - &ActionContext { - mode: self.buffer().get_mode(), - selection_mode: self - .state_container - .get_selection_mode(), - has_results: self.buffer().get_dataview().is_some(), - has_filter: false, - has_search: false, - row_count: self.get_row_count(), - column_count: self.get_column_count(), - current_row: self - .state_container - .get_table_selected_row() - .unwrap_or(0), - current_column: self.buffer().get_current_column(), - }, - )?; - false - } - ChordResult::PartialChord(description) => { - // Update status to show chord mode - self.buffer_mut().set_status_message(description.clone()); - // Update chord mode in renderer with available completions - // Extract the completions from the description - if description.contains("y=row") { - self.key_sequence_renderer - .set_chord_mode(Some("y(a,c,q,r,v)".to_string())); - } else { - self.key_sequence_renderer - .set_chord_mode(Some(description.clone())); - } - false // Don't exit, waiting for more keys - } - ChordResult::Cancelled => { - self.buffer_mut() - .set_status_message("Chord cancelled".to_string()); - // Clear chord mode in renderer - self.key_sequence_renderer.clear_chord_mode(); - false - } - ChordResult::SingleKey(single_key) => { - // Not a chord, process normally - self.handle_results_input(single_key)? - } - } - } else { - // For other modes, process keys normally - match self.buffer().get_mode() { - AppMode::Command => self.handle_command_input(key)?, - AppMode::Results => unreachable!(), // Handled above - AppMode::Search - | AppMode::Filter - | AppMode::FuzzyFilter - | AppMode::ColumnSearch => self.handle_search_modes_input(key)?, - AppMode::Help => self.handle_help_input(key)?, - AppMode::History => self.handle_history_input(key)?, - AppMode::Debug => self.handle_debug_input(key)?, - AppMode::PrettyQuery => self.handle_pretty_query_input(key)?, - AppMode::JumpToRow => self.handle_jump_to_row_input(key)?, - AppMode::ColumnStats => self.handle_column_stats_input(key)?, - } - }; + /// Dispatch key to appropriate mode handler, returns true if exit is requested + fn try_handle_mode_dispatch(&mut self, key: crossterm::event::KeyEvent) -> Result { + match self.buffer().get_mode() { + AppMode::Command => self.handle_command_input(key), + AppMode::Results => { + // Results mode uses chord processing + self.try_handle_chord_processing(key) + } + AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { + self.handle_search_modes_input(key) + } + AppMode::Help => self.handle_help_input(key), + AppMode::History => self.handle_history_input(key), + AppMode::Debug => self.handle_debug_input(key), + AppMode::PrettyQuery => self.handle_pretty_query_input(key), + AppMode::JumpToRow => self.handle_jump_to_row_input(key), + AppMode::ColumnStats => self.handle_column_stats_input(key), + } + } - if should_exit { - break; - } + /// Handle key event processing, returns true if exit is requested + fn try_handle_key_event( + &mut self, + terminal: &mut Terminal, + key: crossterm::event::KeyEvent, + ) -> Result { + // On Windows, filter out key release events - only handle key press + // This prevents double-triggering of toggles + if key.kind != crossterm::event::KeyEventKind::Press { + return Ok(false); + } - // Only redraw after handling a key event OR if TableWidgetManager needs render - if self.table_widget_manager.borrow().needs_render() { - info!("TableWidgetManager needs render after key event"); - } - terminal.draw(|f| self.ui(f))?; - self.table_widget_manager.borrow_mut().rendered(); - } - _ => { - // Ignore other events (mouse, resize, etc.) to reduce CPU + // SAFETY: Always allow Ctrl-C to exit, regardless of app state + // This prevents getting stuck in unresponsive states + if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { + info!(target: "app", "Ctrl-C detected, forcing exit"); + return Ok(true); + } + + // Record key press for visual indicator + let key_display = format_key_for_display(&key); + self.key_indicator.record_key(key_display.clone()); + self.key_sequence_renderer.record_key(key_display); + + // Dispatch to appropriate mode handler + let should_exit = self.try_handle_mode_dispatch(key)?; + + if should_exit { + return Ok(true); + } + + // Only redraw after handling a key event OR if TableWidgetManager needs render + if self.table_widget_manager.borrow().needs_render() { + info!("TableWidgetManager needs render after key event"); + } + terminal.draw(|f| self.ui(f))?; + self.table_widget_manager.borrow_mut().rendered(); + + Ok(false) + } + + /// Handle all events, returns true if exit is requested + fn try_handle_events(&mut self, terminal: &mut Terminal) -> Result { + // Use poll with timeout to allow checking for debounced actions + if event::poll(std::time::Duration::from_millis(50))? { + match event::read()? { + Event::Key(key) => { + if self.try_handle_key_event(terminal, key)? { + return Ok(true); } } - } else { - // No event available, but still redraw if we have pending debounced actions or table needs render - if self.search_modes_widget.is_active() - || self.table_widget_manager.borrow().needs_render() - { - if self.table_widget_manager.borrow().needs_render() { - info!("TableWidgetManager needs periodic render"); - } - terminal.draw(|f| self.ui(f))?; - self.table_widget_manager.borrow_mut().rendered(); + _ => { + // Ignore other events (mouse, resize, etc.) to reduce CPU + } + } + } else { + // No event available, but still redraw if we have pending debounced actions or table needs render + if self.search_modes_widget.is_active() + || self.table_widget_manager.borrow().needs_render() + { + if self.table_widget_manager.borrow().needs_render() { + info!("TableWidgetManager needs periodic render"); } + terminal.draw(|f| self.ui(f))?; + self.table_widget_manager.borrow_mut().rendered(); + } + } + Ok(false) + } + + fn run_app(&mut self, terminal: &mut Terminal) -> Result<()> { + self.initialize_viewport(terminal)?; + + loop { + // Handle debounced search actions + if self.try_handle_debounced_actions(terminal)? { + break; + } + + // Handle all events (key presses, etc.) + if self.try_handle_events(terminal)? { + break; } } Ok(()) @@ -1714,336 +1750,70 @@ impl EnhancedTuiApp { return Ok(result); } - // Try dispatcher for other text editing operations - if let Some(action) = self.key_dispatcher.get_command_action(&key) { - match action { - "expand_asterisk" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.expand_asterisk(&self.hybrid_parser) { - // Sync for rendering if needed - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - } - } - } - return Ok(false); - } - "expand_asterisk_visible" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.expand_asterisk_visible() { - // Sync for rendering if needed - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - } - } - } - return Ok(false); - } - // "move_to_line_start" and "move_to_line_end" now handled by editor_widget - "delete_word_backward" => { - self.delete_word_backward(); - return Ok(false); - } - "delete_word_forward" => { - self.delete_word_forward(); - return Ok(false); - } - "kill_line" => { - self.kill_line(); - return Ok(false); - } - "kill_line_backward" => { - self.kill_line_backward(); - return Ok(false); - } - "move_word_backward" => { - self.move_cursor_word_backward(); - return Ok(false); - } - "move_word_forward" => { - self.move_cursor_word_forward(); - return Ok(false); - } - "jump_to_prev_token" => { - self.jump_to_prev_token(); - return Ok(false); - } - "jump_to_next_token" => { - self.jump_to_next_token(); - return Ok(false); - } - "paste_from_clipboard" => { - self.paste_from_clipboard(); - return Ok(false); - } - _ => {} // Fall through to hardcoded handling - } + // Try text editing operations + if let Some(result) = self.try_handle_text_editing(&key)? { + return Ok(result); } - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Enter => { - // Always use single-line mode handling - let query = self.get_input_text().trim().to_string(); - debug!(target: "action", "Executing query: {}", query); + // Try mode transitions and core input handling + if let Some(result) = self.try_handle_mode_transitions(&key, old_cursor)? { + return Ok(result); + } - if !query.is_empty() { - // Check for special commands - if query == ":help" { - self.state_container.set_help_visible(true); - self.buffer_mut().set_mode(AppMode::Help); - self.buffer_mut() - .set_status_message("Help Mode - Press ESC to return".to_string()); - } else if query == ":exit" || query == ":quit" || query == ":q" { - return Ok(true); - } else if query == ":tui" { - // Already in TUI mode - self.buffer_mut() - .set_status_message("Already in TUI mode".to_string()); + // All input should be handled by the try_handle_* methods above + // If we reach here, it means we missed handling a key combination + + Ok(false) + } + + // ========== COMMAND INPUT HELPER METHODS ========== + // These helpers break down the massive handle_command_input method into logical groups + + /// Handle function key inputs (F1-F12) + fn try_handle_function_keys( + &mut self, + key: &crossterm::event::KeyEvent, + ) -> Result> { + match key.code { + KeyCode::F(1) | KeyCode::Char('?') => { + // Toggle between Help mode and previous mode + if self.buffer().get_mode() == AppMode::Help { + // Exit help mode + let mode = if self.buffer().has_dataview() { + AppMode::Results } else { - self.buffer_mut() - .set_status_message(format!("Processing query: '{}'", query)); - self.execute_query(&query)?; - } + AppMode::Command + }; + self.buffer_mut().set_mode(mode); + self.state_container.set_help_visible(false); + self.help_widget.on_exit(); } else { - self.buffer_mut() - .set_status_message("Empty query - please enter a SQL command".to_string()); + // Enter help mode + self.state_container.set_help_visible(true); + self.buffer_mut().set_mode(AppMode::Help); + self.help_widget.on_enter(); } + Ok(Some(false)) } - KeyCode::Tab => { - // Tab completion works in both modes - // Always use single-line completion - self.apply_completion() + KeyCode::F(3) => { + // Show pretty printed query + self.show_pretty_query(); + Ok(Some(false)) } - // Ctrl+R is now handled by the editor widget above - // History navigation - Ctrl+P or Alt+Up - KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Navigate to previous command in history - // Get history entries first, before mutable borrow - let history_entries = self - .state_container - .command_history() - .get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_up(&history_commands) { - // Sync the input field with buffer (for now, until we complete migration) - let text = buffer.get_input_text(); - - // Debug: show what we got from history - let debug_msg = if text.is_empty() { - "History navigation returned empty text!".to_string() - } else { - format!( - "History: {}", - // ========== MAIN RUN LOOP ========== - if text.len() > 50 { - format!("{}...", &text[..50]) - } else { - text.clone() - } - ) - }; - - // Sync all input states - self.sync_all_input_states(); - self.buffer_mut().set_status_message(debug_msg); - } - } + KeyCode::F(5) => { + // Toggle debug mode + self.toggle_debug_mode(); + Ok(Some(false)) } - // History navigation - Ctrl+N or Alt+Down - KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Navigate to next command in history - // Get history entries first, before mutable borrow - let history_entries = self - .state_container - .command_history() - .get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_down(&history_commands) { - // Sync all input states - self.sync_all_input_states(); - self.buffer_mut() - .set_status_message("Next command from history".to_string()); - } - } - } - // Alternative: Alt+Up for history previous (in case Ctrl+P is intercepted) - KeyCode::Up if key.modifiers.contains(KeyModifiers::ALT) => { - let history_entries = self - .state_container - .command_history() - .get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_up(&history_commands) { - // Sync all input states - self.sync_all_input_states(); - self.buffer_mut() - .set_status_message("Previous command (Alt+Up)".to_string()); - } - } - } - // Alternative: Alt+Down for history next - KeyCode::Down if key.modifiers.contains(KeyModifiers::ALT) => { - let history_entries = self - .state_container - .command_history() - .get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_down(&history_commands) { - // Sync all input states - self.sync_all_input_states(); - self.buffer_mut() - .set_status_message("Next command (Alt+Down)".to_string()); - } - } - } - KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Kill line - delete from cursor to end of line - self.buffer_mut() - .set_status_message("Ctrl+K pressed - killing to end of line".to_string()); - self.kill_line(); - } - KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::ALT) => { - // Alternative: Alt+K for kill line (for terminals that intercept Ctrl+K) - self.buffer_mut() - .set_status_message("Alt+K - killing to end of line".to_string()); - self.kill_line(); - } - KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Kill line backward - delete from cursor to beginning of line - self.kill_line_backward(); - } - // Ctrl+Z (undo) now handled by editor_widget - KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Yank - paste from kill ring - self.yank(); - } - KeyCode::Char('v') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Paste from system clipboard - self.paste_from_clipboard(); - } - KeyCode::Char('[') if key.modifiers.contains(KeyModifiers::ALT) => { - // Jump to previous SQL token - self.jump_to_prev_token(); - } - KeyCode::Char(']') if key.modifiers.contains(KeyModifiers::ALT) => { - // Jump to next SQL token - self.jump_to_next_token(); - } - KeyCode::Left if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Move backward one word - self.move_cursor_word_backward(); - } - KeyCode::Right if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Move forward one word - self.move_cursor_word_forward(); - } - KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::ALT) => { - // Move backward one word (alt+b like in bash) - self.move_cursor_word_backward(); - } - KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::ALT) => { - // Move forward one word (alt+f like in bash) - self.move_cursor_word_forward(); - } - KeyCode::Down - if self.buffer().has_dataview() - && self.buffer().get_edit_mode() == EditMode::SingleLine => - { - self.buffer_mut().set_mode(AppMode::Results); - // Restore previous position or default to 0 - let row = self.buffer().get_last_results_row().unwrap_or(0); - self.state_container.set_table_selected_row(Some(row)); - - // Restore the exact scroll offset from when we left - let last_offset = self.buffer().get_last_scroll_offset(); - self.buffer_mut().set_scroll_offset(last_offset); - } - _ => { - // Use the new helper to handle input keys through buffer - self.handle_input_key(key); - - // Clear completion state when typing other characters - self.state_container.clear_completion(); - - // Always use single-line completion - self.handle_completion() - } - } - - // Update horizontal scroll if cursor moved - if self.get_input_cursor() != old_cursor { - self.update_horizontal_scroll(120); // Assume reasonable terminal width, will be adjusted in render - } - - Ok(false) - } - - // ========== COMMAND INPUT HELPER METHODS ========== - // These helpers break down the massive handle_command_input method into logical groups - - /// Handle function key inputs (F1-F12) - fn try_handle_function_keys( - &mut self, - key: &crossterm::event::KeyEvent, - ) -> Result> { - match key.code { - KeyCode::F(1) | KeyCode::Char('?') => { - // Toggle between Help mode and previous mode - if self.buffer().get_mode() == AppMode::Help { - // Exit help mode - let mode = if self.buffer().has_dataview() { - AppMode::Results - } else { - AppMode::Command - }; - self.buffer_mut().set_mode(mode); - self.state_container.set_help_visible(false); - self.help_widget.on_exit(); - } else { - // Enter help mode - self.state_container.set_help_visible(true); - self.buffer_mut().set_mode(AppMode::Help); - self.help_widget.on_enter(); - } - Ok(Some(false)) - } - KeyCode::F(3) => { - // Show pretty printed query - self.show_pretty_query(); - Ok(Some(false)) - } - KeyCode::F(5) => { - // Toggle debug mode - self.toggle_debug_mode(); - Ok(Some(false)) - } - KeyCode::F(6) => { - // Toggle row numbers - let current = self.buffer().is_show_row_numbers(); - self.buffer_mut().set_show_row_numbers(!current); - self.buffer_mut().set_status_message(format!( - "Row numbers: {}", - if !current { "ON" } else { "OFF" } - )); - Ok(Some(false)) + KeyCode::F(6) => { + // Toggle row numbers + let current = self.buffer().is_show_row_numbers(); + self.buffer_mut().set_show_row_numbers(!current); + self.buffer_mut().set_status_message(format!( + "Row numbers: {}", + if !current { "ON" } else { "OFF" } + )); + Ok(Some(false)) } KeyCode::F(7) => { // Toggle compact mode @@ -2283,6 +2053,326 @@ impl EnhancedTuiApp { } } + /// Handle text editing operations (word movement, kill line, clipboard, etc.) + fn try_handle_text_editing( + &mut self, + key: &crossterm::event::KeyEvent, + ) -> Result> { + // Try dispatcher actions first + if let Some(action) = self.key_dispatcher.get_command_action(key) { + match action { + "expand_asterisk" => { + if let Some(buffer) = self.buffer_manager.current_mut() { + if buffer.expand_asterisk(&self.hybrid_parser) { + // Sync for rendering if needed + if buffer.get_edit_mode() == EditMode::SingleLine { + let text = buffer.get_input_text(); + let cursor = buffer.get_input_cursor_position(); + self.set_input_text_with_cursor(text, cursor); + } + } + } + return Ok(Some(false)); + } + "expand_asterisk_visible" => { + if let Some(buffer) = self.buffer_manager.current_mut() { + if buffer.expand_asterisk_visible() { + // Sync for rendering if needed + if buffer.get_edit_mode() == EditMode::SingleLine { + let text = buffer.get_input_text(); + let cursor = buffer.get_input_cursor_position(); + self.set_input_text_with_cursor(text, cursor); + } + } + } + return Ok(Some(false)); + } + "delete_word_backward" => { + self.delete_word_backward(); + return Ok(Some(false)); + } + "delete_word_forward" => { + self.delete_word_forward(); + return Ok(Some(false)); + } + "kill_line" => { + self.kill_line(); + return Ok(Some(false)); + } + "kill_line_backward" => { + self.kill_line_backward(); + return Ok(Some(false)); + } + "move_word_backward" => { + self.move_cursor_word_backward(); + return Ok(Some(false)); + } + "move_word_forward" => { + self.move_cursor_word_forward(); + return Ok(Some(false)); + } + "jump_to_prev_token" => { + self.jump_to_prev_token(); + return Ok(Some(false)); + } + "jump_to_next_token" => { + self.jump_to_next_token(); + return Ok(Some(false)); + } + "paste_from_clipboard" => { + self.paste_from_clipboard(); + return Ok(Some(false)); + } + _ => {} // Not a text editing action, fall through + } + } + + // Handle hardcoded text editing keys + match key.code { + KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Kill line - delete from cursor to end of line + self.buffer_mut() + .set_status_message("Ctrl+K pressed - killing to end of line".to_string()); + self.kill_line(); + Ok(Some(false)) + } + KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::ALT) => { + // Alternative: Alt+K for kill line (for terminals that intercept Ctrl+K) + self.buffer_mut() + .set_status_message("Alt+K - killing to end of line".to_string()); + self.kill_line(); + Ok(Some(false)) + } + KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Kill line backward - delete from cursor to beginning of line + self.kill_line_backward(); + Ok(Some(false)) + } + KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Yank - paste from kill ring + self.yank(); + Ok(Some(false)) + } + KeyCode::Char('v') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Paste from system clipboard + self.paste_from_clipboard(); + Ok(Some(false)) + } + KeyCode::Char('[') if key.modifiers.contains(KeyModifiers::ALT) => { + // Jump to previous SQL token + self.jump_to_prev_token(); + Ok(Some(false)) + } + KeyCode::Char(']') if key.modifiers.contains(KeyModifiers::ALT) => { + // Jump to next SQL token + self.jump_to_next_token(); + Ok(Some(false)) + } + KeyCode::Left if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Move backward one word + self.move_cursor_word_backward(); + Ok(Some(false)) + } + KeyCode::Right if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Move forward one word + self.move_cursor_word_forward(); + Ok(Some(false)) + } + KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::ALT) => { + // Move backward one word (alt+b like in bash) + self.move_cursor_word_backward(); + Ok(Some(false)) + } + KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::ALT) => { + // Move forward one word (alt+f like in bash) + self.move_cursor_word_forward(); + Ok(Some(false)) + } + _ => Ok(None), // Not a text editing key we handle + } + } + + /// Handle mode transitions and core input processing (Enter, Tab, Down arrow, input) + fn try_handle_mode_transitions( + &mut self, + key: &crossterm::event::KeyEvent, + old_cursor: usize, + ) -> Result> { + match key.code { + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Ctrl+C - exit application + Ok(Some(true)) + } + KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Ctrl+D - exit application + Ok(Some(true)) + } + KeyCode::Enter => { + // Query execution and special command handling + let query = self.get_input_text().trim().to_string(); + debug!(target: "action", "Executing query: {}", query); + + if !query.is_empty() { + // Check for special commands + if query == ":help" { + self.state_container.set_help_visible(true); + self.buffer_mut().set_mode(AppMode::Help); + self.buffer_mut() + .set_status_message("Help Mode - Press ESC to return".to_string()); + } else if query == ":exit" || query == ":quit" || query == ":q" { + return Ok(Some(true)); + } else if query == ":tui" { + // Already in TUI mode + self.buffer_mut() + .set_status_message("Already in TUI mode".to_string()); + } else { + self.buffer_mut() + .set_status_message(format!("Processing query: '{}'", query)); + self.execute_query(&query)?; + } + } else { + self.buffer_mut() + .set_status_message("Empty query - please enter a SQL command".to_string()); + } + Ok(Some(false)) + } + KeyCode::Tab => { + // Tab completion works in both modes + self.apply_completion(); + Ok(Some(false)) + } + KeyCode::Down + if self.buffer().has_dataview() + && self.buffer().get_edit_mode() == EditMode::SingleLine => + { + // Switch to Results mode and restore state + self.buffer_mut().set_mode(AppMode::Results); + // Restore previous position or default to 0 + let row = self.buffer().get_last_results_row().unwrap_or(0); + self.state_container.set_table_selected_row(Some(row)); + + // Restore the exact scroll offset from when we left + let last_offset = self.buffer().get_last_scroll_offset(); + self.buffer_mut().set_scroll_offset(last_offset); + Ok(Some(false)) + } + _ => { + // Fallback input handling and completion + self.handle_input_key(*key); + + // Clear completion state when typing other characters + self.state_container.clear_completion(); + + // Always use single-line completion + self.handle_completion(); + + // Update horizontal scroll if cursor moved + if self.get_input_cursor() != old_cursor { + self.update_horizontal_scroll(120); // Assume reasonable terminal width, will be adjusted in render + } + + Ok(Some(false)) + } + } + } + + /// Handle navigation keys specific to Results mode + fn try_handle_results_navigation( + &mut self, + key: &crossterm::event::KeyEvent, + ) -> Result> { + match key.code { + KeyCode::PageDown | KeyCode::Char('f') + if key.modifiers.contains(KeyModifiers::CONTROL) => + { + NavigationBehavior::page_down(self); + Ok(Some(false)) + } + KeyCode::PageUp | KeyCode::Char('b') + if key.modifiers.contains(KeyModifiers::CONTROL) => + { + NavigationBehavior::page_up(self); + Ok(Some(false)) + } + _ => Ok(None), // Not a navigation key we handle + } + } + + /// Handle clipboard/yank operations in Results mode + fn try_handle_results_clipboard( + &mut self, + key: &crossterm::event::KeyEvent, + ) -> Result> { + match key.code { + KeyCode::Char('y') => { + let selection_mode = self.get_selection_mode(); + debug!("'y' key pressed - selection_mode={:?}", selection_mode); + match selection_mode { + SelectionMode::Cell => { + // In cell mode, single 'y' yanks the cell directly + debug!("Yanking cell in cell selection mode"); + self.buffer_mut() + .set_status_message("Yanking cell...".to_string()); + YankBehavior::yank_cell(self); + // Status message will be set by yank_cell + } + SelectionMode::Row => { + // In row mode, 'y' is handled by chord handler (yy, yc, ya) + // The chord handler will process the key sequence + debug!("'y' pressed in row mode - waiting for chord completion"); + self.buffer_mut().set_status_message( + "Press second key for chord: yy=row, yc=column, ya=all, yv=cell" + .to_string(), + ); + } + SelectionMode::Column => { + // In column mode, 'y' yanks the current column + debug!("Yanking column in column selection mode"); + self.buffer_mut() + .set_status_message("Yanking column...".to_string()); + YankBehavior::yank_column(self); + } + } + Ok(Some(false)) + } + _ => Ok(None), + } + } + + /// Handle export operations in Results mode + fn try_handle_results_export( + &mut self, + key: &crossterm::event::KeyEvent, + ) -> Result> { + match key.code { + KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { + self.export_to_csv(); + Ok(Some(false)) + } + KeyCode::Char('j') if key.modifiers.contains(KeyModifiers::CONTROL) => { + self.export_to_json(); + Ok(Some(false)) + } + _ => Ok(None), + } + } + + /// Handle help and mode transitions in Results mode + fn try_handle_results_help( + &mut self, + key: &crossterm::event::KeyEvent, + ) -> Result> { + match key.code { + KeyCode::F(1) | KeyCode::Char('?') => { + self.state_container.set_help_visible(true); + self.buffer_mut().set_mode(AppMode::Help); + self.help_widget.on_enter(); + Ok(Some(false)) + } + _ => Ok(None), + } + } + fn handle_results_input(&mut self, key: crossterm::event::KeyEvent) -> Result { let selection_mode = self.state_container.get_selection_mode(); @@ -2379,80 +2469,30 @@ impl EnhancedTuiApp { ); } - // Fall back to direct key handling for special cases not in dispatcher - match normalized_key.code { - // Space, 'x', and Ctrl+Space are now handled by the action system - // Column operations are now handled by the action system - // - 'H' to hide column - // - Ctrl+Shift+H to unhide all columns - // - Shift+Left/Right to move columns - KeyCode::PageDown | KeyCode::Char('f') - if key.modifiers.contains(KeyModifiers::CONTROL) => - { - NavigationBehavior::page_down(self); - } - KeyCode::PageUp | KeyCode::Char('b') - if key.modifiers.contains(KeyModifiers::CONTROL) => - { - NavigationBehavior::page_up(self); - } - // 'n' and 'N' search navigation are now handled by the action system - // Filter functionality is handled by dispatcher above - // Removed duplicate handlers for filter keys (F, f) - // Sort functionality (lowercase s) - handled by dispatcher above - // Removed to prevent double handling - // 'S' and Alt-S are now handled by the action system - // 'v' key is now handled by the action system - // Clipboard operations (vim-like yank) - KeyCode::Char('y') => { - let selection_mode = self.get_selection_mode(); - debug!("'y' key pressed - selection_mode={:?}", selection_mode); - match selection_mode { - SelectionMode::Cell => { - // In cell mode, single 'y' yanks the cell directly - debug!("Yanking cell in cell selection mode"); - self.buffer_mut() - .set_status_message("Yanking cell...".to_string()); - YankBehavior::yank_cell(self); - // Status message will be set by yank_cell - } - SelectionMode::Row => { - // In row mode, 'y' is handled by chord handler (yy, yc, ya) - // The chord handler will process the key sequence - debug!("'y' pressed in row mode - waiting for chord completion"); - self.buffer_mut().set_status_message( - "Press second key for chord: yy=row, yc=column, ya=all, yv=cell" - .to_string(), - ); - } - SelectionMode::Column => { - // In column mode, 'y' yanks the current column - debug!("Yanking column in column selection mode"); - self.buffer_mut() - .set_status_message("Yanking column...".to_string()); - YankBehavior::yank_column(self); - } - } - } - // Export to CSV - KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.export_to_csv(); - } - // Export to JSON - KeyCode::Char('j') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.export_to_json(); - } - // Number keys now handled by action system for vim counts (5j, 3k, etc.) - // Direct column sorting moved to 's' key + column navigation - KeyCode::F(1) | KeyCode::Char('?') => { - self.state_container.set_help_visible(true); - self.buffer_mut().set_mode(AppMode::Help); - self.help_widget.on_enter(); - } - _ => { - // Other keys handled normally - } + // Try Results-specific navigation keys + if let Some(result) = self.try_handle_results_navigation(&normalized_key)? { + return Ok(result); + } + + // Try clipboard/yank operations + if let Some(result) = self.try_handle_results_clipboard(&normalized_key)? { + return Ok(result); + } + + // Try export operations + if let Some(result) = self.try_handle_results_export(&normalized_key)? { + return Ok(result); + } + + // Try help and mode transitions + if let Some(result) = self.try_handle_results_help(&normalized_key)? { + return Ok(result); } + + // All key handling has been migrated to: + // - Action system (handles most keys) + // - try_handle_results_* methods (handles specific Results mode keys) + // This completes the orchestration pattern for Results mode input Ok(false) } // ========== SEARCH OPERATIONS ========== @@ -2936,6 +2976,10 @@ impl EnhancedTuiApp { debug!(target: "search", "Cancel: No saved SQL from widget"); } + // Clear all search navigation state (so n/N keys work properly after escape) + self.state_container.clear_search(); + self.vim_search_manager.borrow_mut().cancel_search(); + // Switch back to Results mode self.buffer_mut().set_mode(AppMode::Results); } @@ -3094,12 +3138,19 @@ impl EnhancedTuiApp { fn execute_query(&mut self, query: &str) -> Result<()> { info!(target: "query", "Executing query: {}", query); - // 1. Save query to buffer and state container + // 1. Clear previous search state so n/N keys work properly + // This fixes the bug where N key was stuck in search mode after query execution + self.state_container.clear_search(); + self.state_container.clear_column_search(); + // Also clear vim search state + self.vim_search_manager.borrow_mut().cancel_search(); + + // 2. Save query to buffer and state container self.buffer_mut().set_last_query(query.to_string()); self.state_container .set_last_executed_query(query.to_string()); - // 2. Update status + // 3. Update status self.buffer_mut() .set_status_message(format!("Executing query: '{}'...", query)); let start_time = std::time::Instant::now(); @@ -3722,66 +3773,6 @@ impl EnhancedTuiApp { self.enter_search_mode(SearchMode::Search); } - /// Update vim search pattern and navigate to first match - fn update_vim_search_pattern(&mut self, pattern: String) { - let result = { - let mut viewport_borrow = self.viewport_manager.borrow_mut(); - if let Some(ref mut viewport) = *viewport_borrow { - if let Some(dataview) = self.buffer().get_dataview() { - self.vim_search_manager.borrow_mut().update_pattern( - pattern.clone(), - dataview, - viewport, - ) - } else { - None - } - } else { - None - } - }; // Drop viewport_borrow here - - // Update the buffer's selected row AND column AFTER dropping the viewport borrow - if let Some(ref m) = result { - self.state_container.set_table_selected_row(Some(m.row)); - self.buffer_mut().set_selected_row(Some(m.row)); - - // IMPORTANT: Also update the selected column to match the search match - self.buffer_mut().set_current_column(m.col); - self.state_container.navigation_mut().selected_column = m.col; - - // CRITICAL: Also update navigation's selected_row to trigger proper rendering - self.state_container.navigation_mut().selected_row = m.row; - - // Update scroll offset if row changed significantly - let viewport_height = 79; // Typical viewport height - let (current_scroll_row, current_scroll_col) = self.buffer().get_scroll_offset(); - - // If the match is outside current viewport, update scroll - if m.row < current_scroll_row || m.row >= current_scroll_row + viewport_height { - let new_scroll = m.row.saturating_sub(viewport_height / 2); - self.buffer_mut() - .set_scroll_offset((new_scroll, current_scroll_col)); - self.state_container.navigation_mut().scroll_offset = - (new_scroll, current_scroll_col); - } - } - - // Now we can update the status without borrow conflicts - if let Some(first_match) = result { - // Update status to show we found a match - self.buffer_mut().set_status_message(format!( - "/{} - found at ({}, {})", - pattern, - first_match.row + 1, - first_match.col + 1 - )); - } else if !pattern.is_empty() { - self.buffer_mut() - .set_status_message(format!("/{} - no matches", pattern)); - } - } - /// Navigate to next vim search match (n key) fn vim_search_next(&mut self) { if !self.vim_search_manager.borrow().is_navigating() { @@ -4953,7 +4944,8 @@ impl EnhancedTuiApp { // ========== RENDERING ========== } - fn render_status_line(&self, f: &mut Frame, area: Rect) { + /// Add mode styling and indicator to status spans + fn add_mode_styling(&self, spans: &mut Vec) -> (Style, Color) { // Determine the mode color let (status_style, mode_color) = match self.buffer().get_mode() { AppMode::Command => (Style::default().fg(Color::Green), Color::Green), @@ -4985,15 +4977,17 @@ impl EnhancedTuiApp { AppMode::ColumnStats => "STATS", }; - let mut spans = Vec::new(); - // Mode indicator with color spans.push(Span::styled( format!("[{}]", mode_indicator), Style::default().fg(mode_color).add_modifier(Modifier::BOLD), )); - // Show data source + (status_style, mode_color) + } + + /// Add data source display to status spans + fn add_data_source_display(&self, spans: &mut Vec) { if let Some(ref source) = self.data_source { spans.push(Span::raw(" ")); let source_display = if source.starts_with("http://") || source.starts_with("https://") @@ -5013,33 +5007,33 @@ impl EnhancedTuiApp { Style::default().fg(Color::Cyan), )); } + } - // Show buffer information - { - let index = self.buffer_manager.current_index(); - let total = self.buffer_manager.all_buffers().len(); + /// Add buffer information to status spans + fn add_buffer_information(&self, spans: &mut Vec) { + let index = self.buffer_manager.current_index(); + let total = self.buffer_manager.all_buffers().len(); - // Show buffer indicator if multiple buffers - if total > 1 { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("[{}/{}]", index + 1, total), - Style::default().fg(Color::Yellow), - )); - } + // Show buffer indicator if multiple buffers + if total > 1 { + spans.push(Span::raw(" ")); + spans.push(Span::styled( + format!("[{}/{}]", index + 1, total), + Style::default().fg(Color::Yellow), + )); + } - // Show current buffer name - if let Some(buffer) = self.buffer_manager.current() { - spans.push(Span::raw(" ")); - let name = buffer.get_name(); - let modified = if buffer.is_modified() { "*" } else { "" }; - spans.push(Span::styled( - format!("{}{}", name, modified), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - } + // Show current buffer name + if let Some(buffer) = self.buffer_manager.current() { + spans.push(Span::raw(" ")); + let name = buffer.get_name(); + let modified = if buffer.is_modified() { "*" } else { "" }; + spans.push(Span::styled( + format!("{}{}", name, modified), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )); } // Get buffer name from the current buffer @@ -5053,8 +5047,10 @@ impl EnhancedTuiApp { .add_modifier(Modifier::BOLD), )); } + } - // Mode-specific information + /// Add mode-specific information to status spans + fn add_mode_specific_info(&self, spans: &mut Vec, mode_color: Color, area: Rect) { match self.buffer().get_mode() { AppMode::Command => { // In command mode, show editing-related info @@ -5086,257 +5082,143 @@ impl EnhancedTuiApp { } } AppMode::Results => { - // In results mode, show navigation and data info - let total_rows = self.get_row_count(); - if total_rows > 0 { - // Get selected row directly from navigation state (0-indexed) and add 1 for display - let selected = self.state_container.navigation().selected_row + 1; - spans.push(Span::raw(" | ")); + // Extract this separately due to its size + self.add_results_mode_info(spans, area); + } + AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { + // Show the pattern being typed - always use input for consistency + let pattern = self.get_input_text(); + if !pattern.is_empty() { + spans.push(Span::raw(" | Pattern: ")); + spans.push(Span::styled(pattern, Style::default().fg(mode_color))); + } + } + _ => {} + } + } - // Show selection mode - let selection_mode = self.get_selection_mode(); - let mode_text = match selection_mode { - SelectionMode::Cell => "CELL", - SelectionMode::Row => "ROW", - SelectionMode::Column => "COL", - }; + /// Add Results mode specific information (restored critical navigation info) + fn add_results_mode_info(&self, spans: &mut Vec, area: Rect) { + let total_rows = self.get_row_count(); + if total_rows > 0 { + // Get selected row directly from navigation state (0-indexed) and add 1 for display + let selected = self.state_container.navigation().selected_row + 1; + spans.push(Span::raw(" | ")); + + // Show selection mode + let selection_mode = self.get_selection_mode(); + let mode_text = match selection_mode { + SelectionMode::Cell => "CELL", + SelectionMode::Row => "ROW", + SelectionMode::Column => "COL", + }; + spans.push(Span::styled( + format!("[{}]", mode_text), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )); + + spans.push(Span::raw(" ")); + spans.push(Span::styled( + format!("Row {}/{}", selected, total_rows), + Style::default().fg(Color::White), + )); + + // Add cursor coordinates (x,y) - column and row position + // Use ViewportManager's visual column position (1-based for display) + let visual_col_display = + if let Some(ref viewport_manager) = *self.viewport_manager.borrow() { + viewport_manager.get_crosshair_col() + 1 + } else { + 1 + }; + spans.push(Span::raw(" ")); + spans.push(Span::styled( + format!("({},{})", visual_col_display, selected), + Style::default().fg(Color::DarkGray), + )); + + // Add actual terminal cursor position if we can calculate it + if let Some(ref mut viewport_manager) = *self.viewport_manager.borrow_mut() { + let available_width = area.width.saturating_sub(TABLE_BORDER_WIDTH) as u16; + // Use ViewportManager's crosshair column position + let visual_col = viewport_manager.get_crosshair_col(); + if let Some(x_pos) = + viewport_manager.get_column_x_position(visual_col, available_width) + { + // Add 2 for left border and padding, add 3 for header rows + let terminal_x = x_pos + 2; + let terminal_y = (selected as u16) + .saturating_sub(self.buffer().get_scroll_offset().0 as u16) + + 3; + spans.push(Span::raw(" ")); spans.push(Span::styled( - format!("[{}]", mode_text), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), + format!("[{}x{}]", terminal_x, terminal_y), + Style::default().fg(Color::DarkGray), )); + } + } - spans.push(Span::raw(" ")); + // Column information + if let Some(dataview) = self.buffer().get_dataview() { + let headers = dataview.column_names(); + + // Get ViewportManager's crosshair position (visual coordinates) + // and use it to get the correct column name + let (visual_row, visual_col) = + if let Some(ref viewport_manager) = *self.viewport_manager.borrow() { + ( + viewport_manager.get_crosshair_row(), + viewport_manager.get_crosshair_col(), + ) + } else { + (0, 0) + }; + + // Use ViewportManager's visual column index to get the correct column name + if visual_col < headers.len() { + spans.push(Span::raw(" | Col: ")); spans.push(Span::styled( - format!("Row {}/{}", selected, total_rows), - Style::default().fg(Color::White), + headers[visual_col].clone(), + Style::default().fg(Color::Cyan), )); - // Add cursor coordinates (x,y) - column and row position - // Use ViewportManager's visual column position (1-based for display) - let visual_col_display = + // Show ViewportManager's crosshair position and viewport size + let viewport_info = if let Some(ref viewport_manager) = *self.viewport_manager.borrow() { - viewport_manager.get_crosshair_col() + 1 + let viewport_rows = viewport_manager.get_viewport_rows(); + let viewport_height = viewport_rows.end - viewport_rows.start; + format!("[V:{},{} @ {}r]", visual_row, visual_col, viewport_height) } else { - 1 + format!("[V:{},{}]", visual_row, visual_col) }; spans.push(Span::raw(" ")); spans.push(Span::styled( - format!("({},{})", visual_col_display, selected), - Style::default().fg(Color::DarkGray), + viewport_info, + Style::default().fg(Color::Magenta), )); + } + } + } + } - // Add actual terminal cursor position if we can calculate it - if let Some(ref mut viewport_manager) = *self.viewport_manager.borrow_mut() { - let available_width = area.width.saturating_sub(TABLE_BORDER_WIDTH) as u16; - // Use ViewportManager's crosshair column position - let visual_col = viewport_manager.get_crosshair_col(); - if let Some(x_pos) = - viewport_manager.get_column_x_position(visual_col, available_width) - { - // Add 2 for left border and padding, add 3 for header rows - let terminal_x = x_pos + 2; - let terminal_y = (selected as u16) - .saturating_sub(self.buffer().get_scroll_offset().0 as u16) - + 3; - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("[{}x{}]", terminal_x, terminal_y), - Style::default().fg(Color::DarkGray), - )); - } - } - - // Column information - if let Some(dataview) = self.buffer().get_dataview() { - let headers = dataview.column_names(); - - // Get ViewportManager's crosshair position (visual coordinates) - // and use it to get the correct column name - let (visual_row, visual_col) = - if let Some(ref viewport_manager) = *self.viewport_manager.borrow() { - ( - viewport_manager.get_crosshair_row(), - viewport_manager.get_crosshair_col(), - ) - } else { - (0, 0) - }; - - debug!(target: "render", - "Status line: headers.len()={}, headers={:?}, viewport_crosshair=(row:{}, col:{})", - headers.len(), headers, visual_row, visual_col); - - // Use ViewportManager's visual column index to get the correct column name - if visual_col < headers.len() { - spans.push(Span::raw(" | Col: ")); - spans.push(Span::styled( - headers[visual_col].clone(), - Style::default().fg(Color::Cyan), - )); - - // Show ViewportManager's crosshair position and viewport size - let viewport_info = if let Some(ref viewport_manager) = - *self.viewport_manager.borrow() - { - let viewport_rows = viewport_manager.get_viewport_rows(); - let viewport_height = viewport_rows.end - viewport_rows.start; - format!("[V:{},{} @ {}r]", visual_row, visual_col, viewport_height) - } else { - format!("[V:{},{}]", visual_row, visual_col) - }; - spans.push(Span::raw(" ")); - spans.push(Span::styled( - viewport_info, - Style::default().fg(Color::Magenta), - )); - - // Show pinned columns count if any - if let Some(dataview) = self.buffer().get_dataview() { - let pinned_count = dataview.get_pinned_columns().len(); - if pinned_count > 0 { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("📌{}", pinned_count), - Style::default().fg(Color::Magenta), - )); - } + fn render_status_line(&self, f: &mut Frame, area: Rect) { + let mut spans = Vec::new(); - // Show hidden columns count if any - let hidden_count = dataview.get_hidden_column_names().len(); - if hidden_count > 0 { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("👁️‍🗨️{} hidden", hidden_count), - Style::default().fg(Color::DarkGray), - )); - spans.push(Span::raw(" ")); - spans.push(Span::styled( - "[- hide/+ unhide]", - Style::default() - .fg(Color::DarkGray) - .add_modifier(Modifier::DIM), - )); - } else { - // Show hint about column hiding when no columns are hidden - spans.push(Span::raw(" ")); - spans.push(Span::styled( - "[- to hide col]", - Style::default() - .fg(Color::DarkGray) - .add_modifier(Modifier::DIM), - )); - } - } // Close the dataview if let - - // In cell mode, show the current cell value - if self.get_selection_mode() == SelectionMode::Cell { - if let Some(selected_row) = - self.state_container.get_table_selected_row() - { - if let Some(row_data) = - dataview.source().get_row_as_strings(selected_row) - { - // Get visual column from ViewportManager and convert to DataTable index - let datatable_idx = if let Some(ref viewport_manager) = - *self.viewport_manager.borrow() - { - let visual_col = viewport_manager.get_crosshair_col(); - let display_columns = dataview.get_display_columns(); - if visual_col < display_columns.len() { - display_columns[visual_col] - } else { - 0 - } - } else { - self.buffer().get_current_column() - }; - if let Some(cell_value) = row_data.get(datatable_idx) { - // Truncate if too long - let display_value = if cell_value.len() > 30 { - format!("{}...", &cell_value[..27]) - } else { - cell_value.clone() - }; - - spans.push(Span::raw(" = ")); - spans.push(Span::styled( - display_value, - Style::default().fg(Color::Yellow), - )); - } - } - } - } - } - } + // Add mode styling and indicator + let (status_style, mode_color) = self.add_mode_styling(&mut spans); - // Viewport efficiency indicator (only show if viewport manager is active) - if let Some(ref efficiency) = *self.viewport_efficiency.borrow() { - spans.push(Span::raw(" | ")); - let efficiency_color = if efficiency.efficiency_percent >= 90 { - Color::Green - } else if efficiency.efficiency_percent >= 75 { - Color::Yellow - } else { - Color::Red - }; - spans.push(Span::styled( - format!("{}% eff", efficiency.efficiency_percent), - Style::default().fg(efficiency_color), - )); - if efficiency.wasted_space > 10 { - spans.push(Span::styled( - format!(" ({}w lost)", efficiency.wasted_space), - Style::default().fg(Color::DarkGray), - )); - } - } + // Add data source display + self.add_data_source_display(&mut spans); - // Filter indicators - if self.buffer().is_fuzzy_filter_active() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Fuzzy: {}", self.buffer().get_fuzzy_filter_pattern()), - Style::default().fg(Color::Magenta), - )); - } else if self.state_container.filter().is_active { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Filter: {}", self.state_container.filter().pattern), - Style::default().fg(Color::Cyan), - )); - } + // Add buffer information + self.add_buffer_information(&mut spans); - // Show last yanked value from AppStateContainer - { - if let Some(ref yanked) = self.state_container.clipboard().last_yanked { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - "Yanked: ", - Style::default().fg(Color::DarkGray), - )); - spans.push(Span::styled( - format!("{}={}", yanked.description, yanked.preview), - Style::default().fg(Color::Green), - )); - } - } - } - } - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // Show the pattern being typed - always use input for consistency - let pattern = self.get_input_text(); - if !pattern.is_empty() { - spans.push(Span::raw(" | Pattern: ")); - spans.push(Span::styled(pattern, Style::default().fg(mode_color))); - } - } - _ => {} - } + // Add mode-specific information + self.add_mode_specific_info(&mut spans, mode_color, area); - // Data source indicator (shown in all modes) + // Data source indicator (shown in all modes) - TO BE EXTRACTED if let Some(source) = self.buffer().get_last_query_source() { spans.push(Span::raw(" | ")); let (icon, label, color) = match source.as_str() { @@ -5370,11 +5252,10 @@ impl EnhancedTuiApp { spans.push(Span::styled(label, Style::default().fg(color))); } - // Global indicators (shown when active) + // Global indicators (shown when active) - TO BE EXTRACTED let case_insensitive = self.buffer().is_case_insensitive(); if case_insensitive { spans.push(Span::raw(" | ")); - // Use to_string() to ensure we get the actual string value let icon = self.config.display.icons.case_insensitive.clone(); spans.push(Span::styled( format!("{} CASE", icon), @@ -5382,7 +5263,7 @@ impl EnhancedTuiApp { )); } - // Show column packing mode instead of compact mode + // Show column packing mode - TO BE EXTRACTED if let Some(ref viewport_manager) = *self.viewport_manager.borrow() { let packing_mode = viewport_manager.get_packing_mode(); spans.push(Span::raw(" | ")); @@ -5394,34 +5275,7 @@ impl EnhancedTuiApp { spans.push(Span::styled(text, Style::default().fg(color))); } - // Show lock status indicators - { - let navigation = self.state_container.navigation(); - - // Viewport lock indicator with boundary status - if navigation.viewport_lock { - spans.push(Span::raw(" | ")); - let lock_text = if navigation.is_at_viewport_top() { - format!("{}V↑", &self.config.display.icons.lock) - } else if navigation.is_at_viewport_bottom() { - format!("{}V↓", &self.config.display.icons.lock) - } else { - format!("{}V", &self.config.display.icons.lock) - }; - spans.push(Span::styled(lock_text, Style::default().fg(Color::Magenta))); - } - - // Cursor lock indicator - if navigation.cursor_lock { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("{}C", &self.config.display.icons.lock), - Style::default().fg(Color::Yellow), - )); - } - } - - // Show status message if present + // Show status message if present - TO BE EXTRACTED let status_msg = self.buffer().get_status_message(); if !status_msg.is_empty() { spans.push(Span::raw(" | ")); @@ -5433,7 +5287,7 @@ impl EnhancedTuiApp { )); } - // Help shortcuts (right side) + // Help shortcuts (right side) - TO BE EXTRACTED let help_text = match self.buffer().get_mode() { AppMode::Command => "Enter:Run | Tab:Complete | ↓:Results | F1:Help", AppMode::Results => match self.get_selection_mode() { @@ -5451,35 +5305,9 @@ impl EnhancedTuiApp { AppMode::JumpToRow => "Enter:Jump | Esc:Cancel", }; - // Add key press indicator using smart sequence renderer - if self.key_sequence_renderer.has_content() { - let key_display = self.key_sequence_renderer.get_display(); - if !key_display.is_empty() { - spans.push(Span::raw(" | Keys: ")); - spans.push(Span::styled( - key_display, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::ITALIC), - )); - } - } - - // Calculate available space for help text - let current_length: usize = spans.iter().map(|s| s.content.len()).sum(); - let available_width = area.width.saturating_sub(TABLE_BORDER_WIDTH) as usize; // Account for borders - let help_length = help_text.len(); + self.add_global_indicators(&mut spans); - if current_length + help_length + 3 < available_width { - // Add spacing to right-align help text - let padding = available_width - current_length - help_length - 3; - spans.push(Span::raw(" ".repeat(padding))); - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - help_text, - Style::default().fg(Color::DarkGray), - )); - } + self.add_help_text_display(&mut spans, help_text, area); let status_line = Line::from(spans); let status = Paragraph::new(status_line) @@ -6852,6 +6680,39 @@ impl EnhancedTuiApp { self.debug_widget.generate_pretty_sql(&query); } } + + /// Add global indicators like key sequence display + fn add_global_indicators(&self, spans: &mut Vec) { + if self.key_sequence_renderer.has_content() { + let key_display = self.key_sequence_renderer.get_display(); + if !key_display.is_empty() { + spans.push(Span::raw(" | Keys: ")); + spans.push(Span::styled( + key_display, + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::ITALIC), + )); + } + } + } + + /// Add right-aligned help text if space allows + fn add_help_text_display<'a>(&self, spans: &mut Vec>, help_text: &'a str, area: Rect) { + let current_length: usize = spans.iter().map(|s| s.content.len()).sum(); + let available_width = area.width.saturating_sub(TABLE_BORDER_WIDTH) as usize; + let help_length = help_text.len(); + + if current_length + help_length + 3 < available_width { + let padding = available_width - current_length - help_length - 3; + spans.push(Span::raw(" ".repeat(padding))); + spans.push(Span::raw(" | ")); + spans.push(Span::styled( + help_text, + Style::default().fg(Color::DarkGray), + )); + } + } } // Implement ActionHandlerContext trait for EnhancedTuiApp diff --git a/sql-cli/src/ui/enhanced_tui_debug.rs b/sql-cli/src/ui/enhanced_tui_debug.rs index 44f4829d..a17037b6 100644 --- a/sql-cli/src/ui/enhanced_tui_debug.rs +++ b/sql-cli/src/ui/enhanced_tui_debug.rs @@ -1,6 +1,5 @@ use crate::debug::{ BufferDebugProvider, BufferManagerDebugProvider, DataViewDebugProvider, MemoryDebugProvider, - ViewportDebugProvider, }; use crate::ui::enhanced_tui::EnhancedTuiApp; use std::sync::Arc; diff --git a/sql-cli/src/ui/enhanced_tui_debug_integration.rs b/sql-cli/src/ui/enhanced_tui_debug_integration.rs index 57ec8dcd..780d8687 100644 --- a/sql-cli/src/ui/enhanced_tui_debug_integration.rs +++ b/sql-cli/src/ui/enhanced_tui_debug_integration.rs @@ -1,4 +1,3 @@ -use crate::debug::Priority; /// Integration of new debug registry system with existing toggle_debug_mode /// This file provides a gradual migration path from the old debug system to the new trait-based system use crate::ui::enhanced_tui::EnhancedTuiApp; diff --git a/sql-cli/src/ui/enhanced_tui_helpers.rs b/sql-cli/src/ui/enhanced_tui_helpers.rs index cfbd54f1..15c9233a 100644 --- a/sql-cli/src/ui/enhanced_tui_helpers.rs +++ b/sql-cli/src/ui/enhanced_tui_helpers.rs @@ -1,8 +1,6 @@ // Helper functions extracted from enhanced_tui.rs // These are pure functions with no dependencies on self -use anyhow::Result; - /// Sanitize table name by removing special characters and limiting length pub fn sanitize_table_name(name: &str) -> String { // Replace spaces and other problematic characters with underscores diff --git a/sql-cli/src/ui/viewport_manager.rs b/sql-cli/src/ui/viewport_manager.rs index 0aec9c5c..dde7b136 100644 --- a/sql-cli/src/ui/viewport_manager.rs +++ b/sql-cli/src/ui/viewport_manager.rs @@ -908,7 +908,7 @@ impl ViewportManager { // Work in visual coordinate space! // Visual indices are 0, 1, 2, 3... (contiguous, no gaps) - let mut visual_start = self.viewport_cols.start.min(total_visual_columns); + let visual_start = self.viewport_cols.start.min(total_visual_columns); let mut visual_end = visual_start; debug!(target: "viewport_manager", diff --git a/sql-cli/src/widgets/debounced_input.rs b/sql-cli/src/widgets/debounced_input.rs index c7743af0..62e30234 100644 --- a/sql-cli/src/widgets/debounced_input.rs +++ b/sql-cli/src/widgets/debounced_input.rs @@ -8,7 +8,6 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::{ layout::Rect, style::{Color, Style}, - text::{Line, Span}, widgets::{Block, Borders, Paragraph}, Frame, }; diff --git a/sql-cli/src/widgets/editor_widget.rs b/sql-cli/src/widgets/editor_widget.rs index 6d8863e8..63cb764a 100644 --- a/sql-cli/src/widgets/editor_widget.rs +++ b/sql-cli/src/widgets/editor_widget.rs @@ -1,7 +1,7 @@ use crate::buffer::{AppMode, BufferAPI}; use crate::key_dispatcher::KeyDispatcher; use anyhow::Result; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::KeyEvent; use ratatui::{ layout::Rect, style::{Color, Style}, diff --git a/sql-cli/test_search_classic.sh b/sql-cli/test_search_classic.sh new file mode 100755 index 00000000..15ebc7ea --- /dev/null +++ b/sql-cli/test_search_classic.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +echo "Testing search state reset in CLASSIC mode" +echo "==========================================" + +# Create test data +cat > test_search_fix.csv << 'CSV' +id,name,status,amount +1,Alice,active,100 +2,Bob,pending,200 +3,Charlie,active,150 +4,David,pending,300 +5,Eve,active,250 +CSV + +echo "Test: Running with --classic flag" +echo "Note: In classic mode, we can't test interactive key behavior" +echo "But we can verify the application loads and runs queries correctly" +echo "" + +./target/release/sql-cli test_search_fix.csv -e "select * from test_search_fix where status = 'active'" --classic + +rm -f test_search_fix.csv +echo "Classic mode test complete!" \ No newline at end of file diff --git a/sql-cli/test_search_state_fix.sh b/sql-cli/test_search_state_fix.sh new file mode 100755 index 00000000..c9c902d7 --- /dev/null +++ b/sql-cli/test_search_state_fix.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +echo "Testing search state reset bug fix" +echo "==================================" +echo "" + +# Create test data +cat > test_search_fix.csv << 'CSV' +id,name,status,amount +1,Alice,active,100 +2,Bob,pending,200 +3,Charlie,active,150 +4,David,pending,300 +5,Eve,active,250 +CSV + +echo "Test procedure:" +echo "1. Load file" +echo "2. Press 'N' - should toggle line numbers (first time)" +echo "3. Press '/' and search for 'active'" +echo "4. Press 'n', 'N' to navigate search results" +echo "5. Press Escape to exit search mode" +echo "6. Press 'N' - should toggle line numbers again (not search)" +echo "7. Type new query: select * from test_search_fix where status = 'active'" +echo "8. Press Enter to run query" +echo "9. Press 'N' - should toggle line numbers (not search navigation)" +echo "" +echo "EXPECTED: After escape or running new query, 'N' should toggle line numbers" +echo "BUG WAS: 'N' stayed stuck trying to navigate old search results" +echo "" + +# Start the application +echo "Starting application..." +./target/release/sql-cli test_search_fix.csv + +# Clean up +rm -f test_search_fix.csv +echo "Test complete!" \ No newline at end of file diff --git a/sql-cli/wsl_resource_monitor.sh b/sql-cli/wsl_resource_monitor.sh new file mode 100755 index 00000000..7e7c2c0b --- /dev/null +++ b/sql-cli/wsl_resource_monitor.sh @@ -0,0 +1,172 @@ +#!/bin/bash + +# WSL Resource Monitor Script +# Usage: ./wsl_resource_monitor.sh [interval_seconds] +# Default interval: 5 seconds + +INTERVAL=${1:-5} +LOGFILE="/tmp/wsl_resources.log" + +print_header() { + clear + echo "================================================================" + echo " WSL RESOURCE MONITOR" + echo "================================================================" + echo "Poll interval: ${INTERVAL}s | Log: $LOGFILE | Press Ctrl+C to exit" + echo "================================================================" + echo +} + +get_memory_info() { + echo "🧠 MEMORY STATUS" + echo "----------------" + free -h | grep -E "(Mem|Swap):" + + # Memory percentage + local mem_used=$(free | grep Mem | awk '{print int($3/$2 * 100)}') + local swap_used=$(free | grep Swap | awk '{print int($3/$2 * 100)}') + echo "Memory Usage: ${mem_used}% | Swap Usage: ${swap_used}%" + echo +} + +get_cpu_info() { + echo "⚡ CPU STATUS" + echo "-------------" + local load=$(cat /proc/loadavg | awk '{print $1, $2, $3}') + local cpu_count=$(nproc) + echo "Load Average: $load (${cpu_count} cores)" + + # CPU usage from top + local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1) + echo "CPU Usage: ${cpu_usage}%" + echo +} + +get_disk_info() { + echo "💾 DISK STATUS" + echo "--------------" + echo "Main partitions:" + df -h | grep -E "(/dev/sd[d-i]|/mnt/[cd])" | while read line; do + local usage=$(echo "$line" | awk '{print $5}' | tr -d '%') + local mount=$(echo "$line" | awk '{print $6}') + local size=$(echo "$line" | awk '{print $2}') + local used=$(echo "$line" | awk '{print $3}') + local avail=$(echo "$line" | awk '{print $4}') + + if [[ $usage -gt 80 ]]; then + echo "⚠️ $mount: $used/$size ($usage%) - $avail free" + else + echo "✅ $mount: $used/$size ($usage%) - $avail free" + fi + done + echo + + # Check for disk thrashing indicators + local pswpin=$(cat /proc/vmstat | grep pswpin | awk '{print $2}') + local pswpout=$(cat /proc/vmstat | grep pswpout | awk '{print $2}') + echo "Swap I/O: ${pswpin} pages in, ${pswpout} pages out (lifetime)" + echo +} + +get_process_info() { + echo "🔥 TOP PROCESSES" + echo "----------------" + echo "Memory hogs:" + ps aux --sort=-%mem | head -4 | tail -3 | while read line; do + local pid=$(echo "$line" | awk '{print $2}') + local mem=$(echo "$line" | awk '{print $4}') + local cmd=$(echo "$line" | awk '{for(i=11;i<=NF;i++) printf "%s ", $i; print ""}' | cut -c1-50) + echo " PID $pid: ${mem}% - $cmd" + done + echo + + echo "CPU hogs:" + ps aux --sort=-%cpu | head -4 | tail -3 | while read line; do + local pid=$(echo "$line" | awk '{print $2}') + local cpu=$(echo "$line" | awk '{print $3}') + local cmd=$(echo "$line" | awk '{for(i=11;i<=NF;i++) printf "%s ", $i; print ""}' | cut -c1-50) + echo " PID $pid: ${cpu}% - $cmd" + done + echo +} + +get_wsl_info() { + echo "🐧 WSL STATUS" + echo "-------------" + echo "Workload directories:" + du -sh /home/me/dev /home/me/.cache /home/me/.local 2>/dev/null | while read size dir; do + echo " $dir: $size" + done + echo +} + +check_alerts() { + echo "🚨 ALERTS" + echo "---------" + + # Memory alert + local mem_used=$(free | grep Mem | awk '{print int($3/$2 * 100)}') + if [[ $mem_used -gt 85 ]]; then + echo "⚠️ HIGH MEMORY USAGE: ${mem_used}%" + fi + + # Swap alert + local swap_used=$(free | grep Swap | awk '{print int($3/$2 * 100)}') + if [[ $swap_used -gt 10 ]]; then + echo "⚠️ SWAP USAGE: ${swap_used}%" + fi + + # Load alert + local load1=$(cat /proc/loadavg | awk '{print $1}') + local cpu_count=$(nproc) + local load_pct=$(echo "$load1 $cpu_count" | awk '{print int($1/$2 * 100)}') + if [[ $load_pct -gt 80 ]]; then + echo "⚠️ HIGH CPU LOAD: ${load1} (${load_pct}% of ${cpu_count} cores)" + fi + + # Disk space alert + df -h | grep -E "/dev/sd[d-i]" | while read line; do + local usage=$(echo "$line" | awk '{print $5}' | tr -d '%') + local mount=$(echo "$line" | awk '{print $6}') + if [[ $usage -gt 90 ]]; then + echo "⚠️ DISK SPACE: $mount at ${usage}%" + fi + done + + # Check if we have any alerts + if [[ $mem_used -le 85 && $swap_used -le 10 && $load_pct -le 80 ]]; then + echo "✅ All systems normal" + fi + echo +} + +log_metrics() { + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + local mem_used=$(free | grep Mem | awk '{print int($3/$2 * 100)}') + local swap_used=$(free | grep Swap | awk '{print int($3/$2 * 100)}') + local load=$(cat /proc/loadavg | awk '{print $1}') + local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1) + + echo "$timestamp,MEM:${mem_used}%,SWAP:${swap_used}%,LOAD:$load,CPU:${cpu_usage}%" >> "$LOGFILE" +} + +# Trap Ctrl+C to exit gracefully +trap 'echo -e "\n\n📊 Session log saved to: $LOGFILE"; exit 0' SIGINT + +# Main monitoring loop +while true; do + print_header + get_memory_info + get_cpu_info + get_disk_info + get_process_info + get_wsl_info + check_alerts + + log_metrics + + echo "================================================================" + echo "Next update in ${INTERVAL}s... (Ctrl+C to exit)" + + sleep "$INTERVAL" +done \ No newline at end of file