sql-cli 1.73.1

SQL query tool for CSV/JSON with both interactive TUI and non-interactive CLI modes - perfect for exploration and automation
Documentation
# Event-Driven Architecture Design

## Overview

This document outlines the vision for refactoring sql-cli from its current tightly-coupled architecture to a clean event-driven system inspired by Redux/Flux patterns. The goal is to completely separate concerns: input handling, state management, business logic, and rendering.

## Current Problems

1. **Tight Coupling** - TUI directly handles keys, modifies state, and renders
2. **Untestable** - Can't test business logic without UI
3. **Hard-coded Keys** - Users can't customize keybindings
4. **Complex Key Handling** - 1000+ lines of nested match statements
5. **State Scattered** - State lives in TUI, buffers, widgets, etc.
6. **Hard to Extend** - Adding features requires touching many files

## Proposed Architecture

```
┌─────────────┐     ┌──────────────┐     ┌────────────────┐
│   Input     │────▶│  KeyMapper   │────▶│     Event      │
│  (Terminal) │     │ (Configurable)│     │                │
└─────────────┘     └──────────────┘     └────────────────┘
┌─────────────┐     ┌──────────────┐     ┌────────────────┐
│   Render    │◀────│    State     │◀────│   Dispatcher   │
│    (TUI)    │     │  Container   │     │   (Reducer)    │
└─────────────┘     └──────────────┘     └────────────────┘
```

## Core Components

### 1. Event System

```rust
// All possible application events
#[derive(Debug, Clone)]
pub enum AppEvent {
    // Navigation Events
    Navigate(NavigationTarget),
    ViewportTop,
    ViewportMiddle,
    ViewportBottom,
    ScrollUp(usize),
    ScrollDown(usize),
    
    // Editing Events
    InsertChar(char),
    DeleteChar,
    DeleteWord,
    KillLine,
    Yank,
    ExpandAsterisk,
    
    // Mode Events
    EnterMode(AppMode),
    ExitMode,
    
    // Search Events
    StartSearch(SearchType),
    UpdateSearchQuery(String),
    NextMatch,
    PreviousMatch,
    
    // Query Events
    ExecuteQuery,
    LoadFromCache(String),
    SaveToCache(String),
    
    // Buffer Events
    NewBuffer,
    CloseBuffer,
    SwitchBuffer(usize),
    
    // Data Events
    SortColumn(String, SortOrder),
    FilterData(FilterType),
    ExportData(ExportFormat),
    
    // System Events
    Quit,
    ShowHelp,
    ShowDebug,
    Refresh,
}
```

### 2. KeyMapper (User Configurable)

```rust
pub struct KeyMapper {
    mappings: HashMap<(AppMode, KeyBinding), AppEvent>,
    chord_handler: ChordHandler,
    config_path: PathBuf,
}

impl KeyMapper {
    pub fn from_config(path: &Path) -> Result<Self> {
        // Load from TOML/JSON config
    }
    
    pub fn map_key(&self, key: KeyEvent, mode: AppMode) -> Option<AppEvent> {
        let binding = KeyBinding::from(key);
        self.mappings.get(&(mode, binding)).cloned()
    }
    
    pub fn register_override(&mut self, mode: AppMode, key: KeyBinding, event: AppEvent) {
        self.mappings.insert((mode, key), event);
    }
}
```

**Config Format (keybindings.toml):**
```toml
[command_mode]
"Ctrl+X" = "ExpandAsterisk"
"Ctrl+K" = "KillLine"
"Ctrl+U" = "KillLineBackward"
"Ctrl+R" = "SearchHistory"
"Enter" = "ExecuteQuery"
"Tab" = "AutoComplete"
"/" = { StartSearch = "Forward" }

[results_mode]
"H" = "ViewportTop"
"M" = "ViewportMiddle"
"L" = "ViewportBottom"
"g" = "FirstRow"
"G" = "LastRow"
"/" = { StartSearch = "InResults" }
"y" = "YankCell"
"yy" = "YankRow"

[global]
"F1" = "ShowHelp"
"F5" = "ShowDebug"
"Ctrl+C" = "Quit"
```

### 3. State Container (Redux-style)

```rust
pub struct AppStateContainer {
    state: Arc<RwLock<AppState>>,
    history: Vec<AppEvent>,  // For undo/redo
    subscribers: Vec<Box<dyn Fn(&StateChange)>>,
}

impl AppStateContainer {
    pub fn dispatch(&mut self, event: AppEvent) -> StateChange {
        // Log event for replay/debug
        self.history.push(event.clone());
        
        // Apply event to state (reducer pattern)
        let change = match event {
            AppEvent::Navigate(target) => {
                self.state.write().navigation.go_to(target);
                StateChange::Navigation
            }
            AppEvent::ExpandAsterisk => {
                let expanded = self.expand_select_star();
                self.state.write().query = expanded;
                StateChange::Query
            }
            AppEvent::ExecuteQuery => {
                let result = self.execute_current_query().await?;
                self.state.write().results = Some(result);
                StateChange::Results
            }
            // ... handle all events
        };
        
        // Notify subscribers
        self.notify_subscribers(&change);
        
        change
    }
    
    pub fn snapshot(&self) -> AppStateSnapshot {
        // Immutable snapshot for rendering
        self.state.read().clone().into()
    }
}
```

### 4. Pure Rendering TUI

```rust
impl EnhancedTuiApp {
    pub async fn run(mut self) -> Result<()> {
        let mut key_mapper = KeyMapper::from_config("~/.config/sql-cli/keys.toml")?;
        let mut state_container = AppStateContainer::new();
        
        loop {
            // 1. Handle Input
            if let Ok(key) = event::read()? {
                // 2. Map to Event
                let mode = state_container.current_mode();
                if let Some(event) = key_mapper.map_key(key, mode) {
                    // 3. Dispatch Event
                    let change = state_container.dispatch(event).await?;
                    
                    // 4. Render if needed
                    if change.needs_render() {
                        let snapshot = state_container.snapshot();
                        self.render_from_state(&snapshot)?;
                    }
                }
            }
        }
    }
    
    fn render_from_state(&mut self, state: &AppStateSnapshot) -> Result<()> {
        // Pure rendering - no business logic
        self.terminal.draw(|f| {
            match state.mode {
                AppMode::Command => self.render_command_mode(f, state),
                AppMode::Results => self.render_results_mode(f, state),
                AppMode::Help => self.render_help(f, state),
                // ...
            }
        })?;
        Ok(())
    }
}
```

### 5. Event Bus for Widgets

```rust
pub trait Widget {
    fn handle_event(&mut self, event: &AppEvent) -> Option<AppEvent>;
    fn render(&self, f: &mut Frame, area: Rect, state: &WidgetState);
}

pub struct EventBus {
    widgets: Vec<Box<dyn Widget>>,
}

impl EventBus {
    pub fn dispatch(&mut self, event: AppEvent) -> Vec<AppEvent> {
        // Allow widgets to handle/transform events
        let mut new_events = vec![];
        for widget in &mut self.widgets {
            if let Some(new_event) = widget.handle_event(&event) {
                new_events.push(new_event);
            }
        }
        new_events
    }
}
```

## Migration Roadmap

### Phase 0: Complete State Migration (Current - 1-2 days)
- [x] FilterState, SearchState, SortState, SelectionState
- [x] ViewportLockState, TabCompleteState, HistorySearchState, HelpState  
- [ ] NavigationState, UndoRedoState, ClipboardState, SearchStates

### Phase 1: DataView Layer (1-2 weeks)
- Abstract all data operations into DataView trait
- Implement for CSV, JSON, API, Cache sources
- Unified data pipeline with transformations
- Virtual scrolling and lazy loading

### Phase 2: Event System Core (1 week)
- Define all AppEvents
- Create EventDispatcher
- Implement StateChange notifications
- Add event logging/replay infrastructure

### Phase 3: KeyMapper Implementation (1 week)
- Build configurable mapping system
- Create default keybinding configs
- Support user overrides
- Implement chord handling via events

### Phase 4: State Container Redux (2 weeks)
- Move all business logic from TUI to reducers
- Implement state snapshots
- Add undo/redo via event replay
- Create middleware system for async operations

### Phase 5: Widget Abstraction (1 week)
- Define Widget trait
- Convert existing rendering to widgets
- Implement event bubbling
- Create widget composition system

### Phase 6: TUI Simplification (1 week)
- Remove all key handling from TUI
- Convert to pure rendering
- Implement render diffing for performance
- Add render debugging tools

## Benefits

### For Users
- **Customizable Keys** - Full control over keybindings
- **Consistent Behavior** - All state changes go through one path
- **Better Performance** - Only re-render what changed
- **Undo/Redo** - Event sourcing enables time travel
- **Scriptable** - Can replay event sequences

### For Developers
- **Testable** - Test state changes without UI
- **Debuggable** - See every event and state change
- **Maintainable** - Clear separation of concerns
- **Extensible** - Easy to add new features
- **Reusable** - Widgets and state can be used elsewhere

### For Future
- **Multiple UIs** - Web UI, GUI, CLI all share state
- **Plugins** - Events make plugin system possible
- **Macros** - Record and replay event sequences
- **Remote Control** - Send events over network
- **AI Integration** - AI can generate events

## Implementation Priority

1. **High Priority**
   - Event definition
   - Basic dispatcher
   - KeyMapper with config

2. **Medium Priority** 
   - State snapshots
   - Widget abstraction
   - Event replay

3. **Low Priority**
   - Render diffing
   - Plugin system
   - Remote control

## Testing Strategy

```rust
#[test]
fn test_expand_asterisk() {
    let mut container = AppStateContainer::new();
    container.set_query("SELECT * FROM users");
    container.set_schema(vec!["id", "name", "email"]);
    
    let change = container.dispatch(AppEvent::ExpandAsterisk);
    
    assert_eq!(change, StateChange::Query);
    assert_eq!(container.get_query(), "SELECT id, name, email FROM users");
}
```

## Configuration Examples

### Power User Config
```toml
[command_mode]
";" = "ExecuteQuery"  # Vim-style command
"Ctrl+Space" = "AutoComplete"
":w" = { SaveToCache = "default" }
":q" = "Quit"

[chord_sequences]
"gg" = "FirstRow"
"dd" = "DeleteLine"
"yy" = "YankLine"
```

### Emacs User Config
```toml
[command_mode]
"Ctrl+X Ctrl+S" = "SaveQuery"
"Ctrl+X Ctrl+C" = "Quit"
"Meta+X" = "ShowCommandPalette"
```

## Conclusion

This architecture provides a clean, testable, and extensible foundation for sql-cli. The event-driven approach separates concerns, enables user customization, and sets up the codebase for future features like plugins, macros, and alternative UIs.

The migration will take approximately 6-8 weeks but will result in a much more maintainable and feature-rich application.

## Next Steps

1. Complete current state migration (v30+)
2. Design DataView interface
3. Create proof-of-concept for one widget
4. Build event system prototype
5. Implement basic KeyMapper

---

*Last Updated: 2025-01-11*
*Status: Planning Phase*
*Owner: sql-cli team*