Skip to content
Core/Host Interface

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:

  • Session
  • AppKeyEvent
  • Action
  • ClipboardIntent
  • Area
  • HintProfile
  • ChromeOverrides
  • ScreenFrame
  • RenderLine / 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 {
textagram/src/session.rs:33-33

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) {
textagram/src/session.rs:42-42

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 {
textagram/src/session.rs:89-89

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,
}
textagram/src/app/mod.rs:458-468

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,
}
textagram/src/session.rs:15-23
    pub fn clipboard_intent(event: AppKeyEvent) -> Option<ClipboardIntent> {
textagram/src/session.rs:158-158

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> {
textagram/src/session.rs:162-162
    pub fn clipboard_cut(&mut self, intent: ClipboardIntent) -> Option<String> {
textagram/src/session.rs:186-186
    pub fn clipboard_paste(&mut self, intent: ClipboardIntent, text: String) {
textagram/src/session.rs:211-211

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 {
textagram/src/session.rs:144-144

or, for hosts that need small chrome customizations:

    pub fn composed_frame_with_overrides(
        &self,
        area: Area,
        hints: HintProfile,
        chrome: &ChromeOverrides,
    ) -> ScreenFrame {
textagram/src/session.rs:148-153
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HintProfile {
    Terminal,
    Web,
    WebMac,
}
textagram/src/session.rs:8-13
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ChromeOverrides {
    pub left_title: Option<RenderLine>,
    pub top_center: Option<RenderLine>,
}
textagram/src/app/render.rs:130-134
#[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,
}
textagram/src/app/render.rs:136-142
#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderLine {
    pub segments: Vec<RenderSegment>,
}
textagram/src/app/render.rs:77-81
#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderSegment {
    pub text: String,
    pub style: SemanticStyle,
}
textagram/src/app/render.rs:70-75
#[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,
}
textagram/src/app/render.rs:13-26

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 reset
  • session.export_for_host_save() — finalize pending edit state first, then export
  • session.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 q should prompt to save
  • writing the saved text back atomically