sql-cli 1.69.1

SQL query tool for CSV/JSON with both interactive TUI and non-interactive CLI modes - perfect for exploration and automation
Documentation
# 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<AppState>,
    
    // 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<SearchType> {
        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.