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
PluginCommands 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:
- Drain async work/results (
Editor::process_async_messages) - Time-based checks (hover timers, warning log, auto-save, polling file changes)
- Render when needed (
Editor::render) - 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:
- Settings
- Menu
- Prompt (including file browser prompts)
- Popup
- 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:
KeyContextdetermines 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
EventLogfor 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 Events 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_transformpayload (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.rsandsrc/app/lsp_actions.rs
Rendering Pipeline (Overview)
Rendering is designed to preserve source-byte → screen-cell mappings for cursors and hit testing:
- Determine viewport per split (scroll + size)
- Build base tokens for visible bytes
- (Optional) apply per-split
view_transformtokens if present - Generate view lines + mappings
- Apply styling layers (syntax/semantic, selection, overlays, etc.)
- 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
PluginCommands back to the editor, which are applied when the main thread drains them duringprocess_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