# Fresh Architecture
Fresh is a high-performance terminal-based text editor written in Rust. This document describes the
runtime structure and the core “flow” concepts: event loop, input handling, actions vs events,
state ownership, rendering, and plugins.
## Runtime Model
Fresh runs a synchronous main thread and communicates with background workers:
- **Main thread:** terminal input, frame loop, state mutation, rendering.
- **Tokio runtime / background tasks:** LSP, file I/O, terminal PTY I/O, directory refresh.
- **Plugin thread (TypeScript runtime):** executes hooks/actions and sends `PluginCommand`s back to
the editor.
Key entrypoint: `src/main.rs`
## Main Event Loop
The main loop is a fixed-timestep-ish render loop (~60fps target) that interleaves:
1. Drain async work/results (`Editor::process_async_messages`)
2. Time-based checks (hover timers, warning log, auto-save, polling file changes)
3. Render when needed (`Editor::render`)
4. Poll terminal input (keyboard/mouse/resize)
Key file: `src/main.rs`
## Input Handling
### Terminal Input Events
The terminal produces `crossterm::event::Event` values:
- key press events
- mouse events (click/drag/move/scroll)
- resize events
The main loop routes these into `Editor`:
- keys → `Editor::handle_key` (`src/app/input.rs`)
- mouse → `Editor::handle_mouse` (`src/app/mouse_input.rs`)
### Modal Dispatch (Settings/Menu/Prompt/Popup)
Keyboard input has a strict priority order for “modal” UI:
1. Settings
2. Menu
3. Prompt (including file browser prompts)
4. Popup
5. Normal/FileExplorer/Terminal contexts
Modal components implement a small hierarchical `InputHandler` trait and return deferred work
(`DeferredAction`) that the `Editor` executes after dispatch.
Key files:
- Dispatch glue: `src/app/input_dispatch.rs`
- Input handler primitives: `src/input/handler.rs`
- Prompt: `src/view/prompt_input.rs`
- Menu: `src/view/ui/menu_input.rs`
- Popup: `src/view/popup_input.rs`
### Key Contexts and Keybindings
When no modal consumes input, keys resolve to an editor `Action`:
- `KeyContext` determines which keymap applies (global vs normal vs prompt vs popup, etc.)
- chord sequences are supported (multi-key bindings)
- context fallthrough is limited to “application-wide” actions to prevent leakage from modals
Key file: `src/input/keybindings.rs`
## Actions vs Events (Core Concepts)
Fresh has two distinct layers that are easy to conflate:
### `Action` (Intent)
`crate::input::keybindings::Action` is the “what the user wants” layer:
- examples: `Save`, `CommandPalette`, `MoveLeft`, `InsertChar('a')`, `LspHover`, `PluginAction(...)`
- produced by keybindings, menus, command palette, and some UI handlers
Execution entrypoint: `Editor::handle_action` (`src/app/input.rs`)
### `Event` (State Change + Undo/Redo)
`crate::model::event::Event` is the event-sourced “what changed” layer for undoable mutations:
- examples: `Insert`, `Delete`, `MoveCursor`, `AddCursor`, `Batch`, plus some view events
- stored in a per-buffer `EventLog` for undo/redo and “modified since saved” tracking
Key files:
- Event definitions + event log: `src/model/event.rs`
- Undo/redo application: `src/app/undo_actions.rs`
### Action → Event Conversion
Many editing/navigation actions convert into one or more `Event`s via:
- `src/input/actions.rs` (pure conversion logic)
- `Editor::action_to_events` (`src/app/render.rs`) as a convenience wrapper
Multi-cursor edits typically become `Event::Batch` so undo is atomic.
### Centralized Event Application
All undoable buffer mutations should go through:
- `Editor::apply_event_to_active_buffer` (`src/app/mod.rs`)
This method centralizes cross-cutting concerns:
- apply to `EditorState`
- sync cursor state into active split view state
- invalidate layouts for splits viewing the buffer
- adjust cursors in other splits viewing the same buffer
- clear/update search highlights appropriately
- fire plugin hooks for edits
- send LSP change notifications using pre-computed positions
Key file: `src/app/mod.rs`
## State Ownership: Buffer vs View
Fresh separates shared buffer state from per-split view state:
### Buffer State (shared per buffer)
`EditorState` owns “the document” and content-anchored decorations:
- text buffer
- cursors (authoritative positions)
- overlays, margins, virtual text
- syntax/semantic highlighting caches
Key file: `src/state.rs`
### View State (per split)
`SplitViewState` owns “how it’s displayed in this split”:
- viewport (scroll position, wrap mode, dimensions)
- a copy of cursors for hit testing / render bookkeeping
- optional `view_transform` payload (plugin-provided token stream)
- compose settings (width, column guides)
Key file: `src/view/split.rs`
View-only events (scrolling, recentering, set viewport) are applied at the `Editor`/split layer;
buffer events (insert/delete/etc.) are applied to `EditorState`.
## Async Messages (LSP, Plugins, File Watching, Terminals)
Every main-loop iteration drains async results via:
- `Editor::process_async_messages` (`src/app/mod.rs`)
This processes:
- LSP results/diagnostics (via the async bridge)
- plugin commands (`PluginCommand`) from the plugin thread
- terminal output/exits, file-open directory loads, file tree refresh, etc.
Key files:
- Message handling: `src/app/async_messages.rs`
- LSP handlers: `src/app/async_messages.rs` and `src/app/lsp_actions.rs`
## Rendering Pipeline (Overview)
Rendering is designed to preserve source-byte → screen-cell mappings for cursors and hit testing:
1. Determine viewport per split (scroll + size)
2. Build base tokens for visible bytes
3. (Optional) apply per-split `view_transform` tokens if present
4. Generate view lines + mappings
5. Apply styling layers (syntax/semantic, selection, overlays, etc.)
6. Emit ratatui widgets
Key files:
- High-level render + hook emission: `src/app/render.rs`
- Split rendering and tokenization: `src/view/ui/split_rendering.rs`
- View line generation: `src/view/ui/view_pipeline.rs`
## Plugins (Updated Timing Model)
Plugins run on a separate thread. The editor interacts with plugins through:
- **Hooks:** `plugin_manager.run_hook(...)` queues work to the plugin thread (non-blocking).
- **Commands:** plugins send `PluginCommand`s back to the editor, which are applied when the main
thread drains them during `process_async_messages()`.
Implications:
- Hooks do not block rendering.
- Effects like `SubmitViewTransform`, overlays, virtual text, etc. may become visible on a later
frame (typically the next frame).
Key files:
- Plugin manager facade: `src/services/plugins/manager.rs`
- Plugin thread interface: `src/services/plugins/thread.rs`
- Hook definitions: `src/services/plugins/hooks.rs`
- Plugin command handling: `src/app/mod.rs`, `src/app/plugin_commands.rs`, `src/app/async_messages.rs`