Core/Host Interface
This page describes the actual interface between the reusable Textagram core and a host.
There is no host trait to implement. Instead, a host drives the core through
textagram::Session and a small set of shared types.
The main surface
The main host-facing types are:
SessionAppKeyEventActionClipboardIntentAreaHintProfileChromeOverridesScreenFrameRenderLine/RenderSegment/SemanticStyle
Host code should import these types from textagram.
Lifecycle
The integration overview shows the main host loop. This page expands the individual calls in that loop: construction, input dispatch, clipboard routing, rendering, sizing, timers, and export.
The important lifecycle rule is that the host owns orchestration. The core
does not poll events, read files, touch the OS clipboard, schedule timers, or
paint directly. It only updates a Session and returns structured results the
host can act on.
Construction and document loading
pub fn new(width: u16, height: u16) -> Self {This creates a new editing session with an initial viewport-sized canvas.
If the host already has document text to open, it loads it explicitly:
pub fn load(&mut self, text: &str) {load replaces the current document state with the provided plain-text
diagram. The host decides where that text came from:
- terminal file I/O
- browser bootstrap content
- Monaco fenced block extraction
- tests and demos
The core does not read files directly.
Input path
Normal key input goes through:
pub fn handle_key(&mut self, event: AppKeyEvent) -> Action {The host owns raw platform event capture and translation. The core owns the
meaning of the translated event once it becomes AppKeyEvent.
That means:
- the host decides how to map crossterm, DOM, or editor events into
AppKeyEvent - the core decides what that translated event means in the current mode
Action
handle_key returns an Action.
#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Action {
pub exit: ExitDisposition,
pub rerender: bool,
pub document_changed: bool,
pub timer: TimerDirective,
pub embedded_canvas_size: Option<(u16, u16)>,
pub navigation_exit: Option<NavigationExit>,
pub recording: RecordingDirective,
}Hosts mainly care about:
- whether a rerender is needed
- whether the document changed
- whether the host should exit
- whether a timer was scheduled or cleared
- whether embedded hosts should react to navigation exit or canvas-size changes
The terminal host uses Action::record_and_exit() and
Action::exit_requested() to decide whether to leave the main loop. The
browser host serializes the same information through the wasm wrapper for
JavaScript.
Clipboard path
Clipboard does not go through handle_key.
The host first translates the platform key event to AppKeyEvent, then asks
the core whether that key means a clipboard operation:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClipboardIntent {
CopySelectionOrDocument,
CutSelection,
CopyDocument,
CutDocument,
PasteSelection,
PasteDocument,
} pub fn clipboard_intent(event: AppKeyEvent) -> Option<ClipboardIntent> {If the core reports a clipboard intent, the host branches into the clipboard methods instead of normal key handling:
pub fn clipboard_copy(&mut self, intent: ClipboardIntent) -> Option<String> { pub fn clipboard_cut(&mut self, intent: ClipboardIntent) -> Option<String> { pub fn clipboard_paste(&mut self, intent: ClipboardIntent, text: String) {This split is important because the host still owns the real clipboard transport:
- reading OS/browser clipboard text before paste
- writing clipboard text after copy or cut
- deciding how native clipboard events map onto the shared intent model
The core owns clipboard semantics:
- what “copy selection or document” means
- what “paste document” means
- which clipboard actions are suppressed in each mode
The helper methods session.clipboard_text(),
session.set_clipboard_text(text), and
session.clipboard_shortcut_suppressed(intent) exist so the host can bridge
real clipboard APIs while keeping the core’s working clipboard state coherent.
Rendering path
Rendering is surface-agnostic.
The core does not return ratatui widgets, DOM nodes, or Monaco decorations. Instead it returns a structured frame:
pub fn composed_frame(&self, area: Area, hints: HintProfile) -> ScreenFrame {or, for hosts that need small chrome customizations:
pub fn composed_frame_with_overrides(
&self,
area: Area,
hints: HintProfile,
chrome: &ChromeOverrides,
) -> ScreenFrame {#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HintProfile {
Terminal,
Web,
WebMac,
}#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ChromeOverrides {
pub left_title: Option<RenderLine>,
pub top_center: Option<RenderLine>,
}#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScreenFrame {
pub rows: Vec<RenderLine>,
pub width: u16,
pub height: u16,
}#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderLine {
pub segments: Vec<RenderSegment>,
}#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderSegment {
pub text: String,
pub style: SemanticStyle,
}#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticStyle {
Plain,
Grid,
Text,
Connector,
Corruption,
Cursor,
Selection,
HelpHeading,
HelpKey,
HintKey,
}So the split is:
- core: generate structured presentation
- host: turn that structure into terminal cells, HTML rows, or editor UI
Hint profile
HintProfile lets the core tailor host-facing hint content for terminal or
web-style hosts. This affects things like footer hints and help text
filtering, but not the core editing rules.
Chrome overrides
ChromeOverrides lets a host inject small host-owned pieces of top chrome
without forking the whole compositor.
Today tg uses this for:
- file identity in the top-left
- dirty markers
- save/quit prompts in the centered title slot
That is a good example of the intended boundary: the host owns file state and save UI, while the core still owns the shared frame structure.
Viewport and surface sizing
Hosts are responsible for measuring their own surface and pushing those sizes into the session.
The main entry points are:
session.set_viewport_size(width, height)session.set_frame_size(width, height)session.set_canvas_viewport_size(width, height)for embedded/canvas-only cases
The terminal host uses frame-sized updates. Embedded hosts such as Monaco use a canvas viewport size that is independent from full outer chrome.
The core owns clamping, canvas growth, and viewport behavior after the host supplies those dimensions.
Timers and transient UI
The core does not run its own timer loop. It only tells the host when another tick is needed:
session.tick_ui(elapsed_ms)session.next_ui_tick_delay_ms()
Hosts schedule and deliver those ticks using their own runtime:
- terminal poll timeout
- browser
setTimeout - editor-host timer callback
This is how transient UI animation stays host-independent.
Export and save-related helpers
Hosts choose what “save” means, but the session exposes helpers for different export use cases:
session.export()— plain document export and clean-baseline resetsession.export_for_host_save()— finalize pending edit state first, then exportsession.export_for_embedded_sync()— trimmed export for embedded text-model sync
This is an important distinction:
- the core can export document text
- the host decides whether that text is written to a file, a browser download, a Monaco text model, or something else
Dirty state and host-owned file logic
The session tracks document revisions:
session.is_dirty()session.revision()
That is enough for hosts to build save prompts and file-backed flows, but the core does not know anything about file paths, atomic writes, or save dialogs.
For example, tg owns:
- loading text from a file path
- deciding when
qshould prompt to save - writing the saved text back atomically