Skip to content

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:

  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)

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 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_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 PluginCommands 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

Released under the Apache 2.0 License