docs/architecture.md
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.
Fresh runs a synchronous main thread and communicates with background workers:
PluginCommands back to
the editor.Key entrypoint: src/main.rs
The main loop is a fixed-timestep-ish render loop (~60fps target) that interleaves:
Editor::process_async_messages)Editor::render)Key file: src/main.rs
The terminal produces crossterm::event::Event values:
The main loop routes these into Editor:
Editor::handle_key (src/app/input.rs)Editor::handle_mouse (src/app/mouse_input.rs)Keyboard input has a strict priority order for “modal” UI:
Modal components implement a small hierarchical InputHandler trait and return deferred work
(DeferredAction) that the Editor executes after dispatch.
Key files:
src/app/input_dispatch.rssrc/input/handler.rssrc/view/prompt_input.rssrc/view/ui/menu_input.rssrc/view/popup_input.rsWhen no modal consumes input, keys resolve to an editor Action:
KeyContext determines which keymap applies (global vs normal vs prompt vs popup, etc.)Key file: src/input/keybindings.rs
Fresh has two distinct layers that are easy to conflate:
Action (Intent)crate::input::keybindings::Action is the “what the user wants” layer:
Save, CommandPalette, MoveLeft, InsertChar('a'), LspHover, PluginAction(...)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:
Insert, Delete, MoveCursor, AddCursor, Batch, plus some view eventsEventLog for undo/redo and “modified since saved” trackingKey files:
src/model/event.rssrc/app/undo_actions.rsMany 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 wrapperMulti-cursor edits typically become Event::Batch so undo is atomic.
All undoable buffer mutations should go through:
Editor::apply_event_to_active_buffer (src/app/mod.rs)This method centralizes cross-cutting concerns:
EditorStateKey file: src/app/mod.rs
Fresh separates shared buffer state from per-split view state:
EditorState owns “the document” and content-anchored decorations:
Key file: src/state.rs
SplitViewState owns “how it’s displayed in this split”:
view_transform payload (plugin-provided token stream)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.
Every main-loop iteration drains async results via:
Editor::process_async_messages (src/app/mod.rs)This processes:
PluginCommand) from the plugin threadKey files:
src/app/async_messages.rssrc/app/async_messages.rs and src/app/lsp_actions.rsRendering is designed to preserve source-byte → screen-cell mappings for cursors and hit testing:
view_transform tokens if presentKey files:
src/app/render.rssrc/view/ui/split_rendering.rssrc/view/ui/view_pipeline.rsPlugins run on a separate thread. The editor interacts with plugins through:
plugin_manager.run_hook(...) queues work to the plugin thread (non-blocking).PluginCommands back to the editor, which are applied when the main
thread drains them during process_async_messages().Implications:
SubmitViewTransform, overlays, virtual text, etc. may become visible on a later
frame (typically the next frame).Key files:
src/services/plugins/manager.rssrc/services/plugins/thread.rssrc/services/plugins/hooks.rssrc/app/mod.rs, src/app/plugin_commands.rs, src/app/async_messages.rs