rattery 0.4.2

rattery: a sandboxed terminal host that runs ratatui apps delivered over HTTP, as a CLI and as a library
Documentation
/// rattery: a sandboxed terminal host for ratatui apps.
///
/// The `terminal` interface is the whole contract between a rattery app (the
/// guest, compiled to a WASI 0.2 component) and the host that owns the real
/// terminal. It is deliberately shaped like ratatui's `Backend` trait plus a
/// crossterm-style event stream, so the guest never emits escape sequences and
/// the host never needs to know what a widget is.
package rattery:tui@0.4.0;

interface terminal {
    // ---- rendering types -------------------------------------------------

    variant color {
        reset,
        black,
        red,
        green,
        yellow,
        blue,
        magenta,
        cyan,
        gray,
        dark-gray,
        light-red,
        light-green,
        light-yellow,
        light-blue,
        light-magenta,
        light-cyan,
        white,
        rgb(tuple<u8, u8, u8>),
        indexed(u8),
    }

    flags modifier {
        bold,
        dim,
        italic,
        underlined,
        slow-blink,
        rapid-blink,
        reversed,
        hidden,
        crossed-out,
    }

    /// One terminal cell. `symbol` is a grapheme cluster, not necessarily a
    /// single char.
    record cell {
        symbol: string,
        fg: color,
        bg: color,
        underline-color: color,
        modifier: modifier,
    }

    /// A cell that changed since the last frame, in absolute coordinates.
    record cell-update {
        x: u16,
        y: u16,
        cell: cell,
    }

    record size {
        width: u16,
        height: u16,
    }

    record position {
        x: u16,
        y: u16,
    }

    record window-size {
        columns-rows: size,
        pixels: size,
    }

    enum clear-type {
        all,
        after-cursor,
        before-cursor,
        current-line,
        until-new-line,
    }

    // ---- input types -----------------------------------------------------

    flags key-modifiers {
        shift,
        control,
        alt,
        super,
        hyper,
        meta,
    }

    enum key-event-kind {
        press,
        repeat,
        release,
    }

    flags key-event-state {
        keypad,
        caps-lock,
        num-lock,
    }

    enum media-key {
        play,
        pause,
        play-pause,
        reverse,
        stop,
        fast-forward,
        rewind,
        track-next,
        track-previous,
        %record,
        lower-volume,
        raise-volume,
        mute-volume,
    }

    enum modifier-key {
        left-shift,
        left-control,
        left-alt,
        left-super,
        left-hyper,
        left-meta,
        right-shift,
        right-control,
        right-alt,
        right-super,
        right-hyper,
        right-meta,
        iso-level3-shift,
        iso-level5-shift,
    }

    variant key-code {
        backspace,
        enter,
        left,
        right,
        up,
        down,
        home,
        end,
        page-up,
        page-down,
        tab,
        back-tab,
        delete,
        insert,
        f(u8),
        character(char),
        null,
        esc,
        caps-lock,
        scroll-lock,
        num-lock,
        print-screen,
        pause,
        menu,
        keypad-begin,
        media(media-key),
        modifier(modifier-key),
    }

    record key-event {
        code: key-code,
        modifiers: key-modifiers,
        kind: key-event-kind,
        state: key-event-state,
    }

    enum mouse-button {
        left,
        right,
        middle,
    }

    variant mouse-event-kind {
        down(mouse-button),
        up(mouse-button),
        drag(mouse-button),
        moved,
        scroll-down,
        scroll-up,
        scroll-left,
        scroll-right,
    }

    record mouse-event {
        kind: mouse-event-kind,
        column: u16,
        row: u16,
        modifiers: key-modifiers,
    }

    /// A newer version of the app the host holds, validated and ready.
    record update {
        /// The server's validator for the new version (an ETag or date),
        /// for display; it is opaque.
        version: option<string>,
        /// Milliseconds, as of this call, before the host may reload on its
        /// own at the next idle moment. Absent: the host leaves it to the
        /// app.
        reload-after-ms: option<u64>,
        /// Milliseconds, as of this call, before the host reloads regardless.
        reload-by-ms: option<u64>,
    }

    /// Whether this app can be updated at all, and by whom.
    enum availability {
        /// The host checks on its own and tells the app.
        watched,
        /// The host checks when the app calls `check-update`.
        on-request,
        /// The host has nowhere to look (the component is embedded in it):
        /// `check-update` finds nothing. The host's embedder may still hand
        /// it a version, which arrives as `update-changed` like any other.
        unavailable,
    }

    variant event {
        focus-gained,
        focus-lost,
        key(key-event),
        mouse(mouse-event),
        paste(string),
        resize(size),
        /// The pending update changed: one appeared, was superseded, or was
        /// withdrawn. Read `pending-update` for the state; handle it by
        /// saving state (see `storage`) and calling `reload` when convenient.
        update-changed,
    }

    // ---- rendering (mirrors ratatui's Backend trait) ---------------------

    draw: func(updates: list<cell-update>);
    append-lines: func(n: u16);
    hide-cursor: func();
    show-cursor: func();
    get-cursor-position: func() -> position;
    set-cursor-position: func(pos: position);
    clear: func(kind: clear-type);
    get-size: func() -> size;
    get-window-size: func() -> window-size;
    flush: func();

    // ---- input -----------------------------------------------------------

    /// Wait for the next input event. Async: other tasks in the app, such as
    /// server calls, keep running while it waits.
    next-event: async func() -> event;

    /// Drain every event received since the last call. Never blocks.
    read-events: func() -> list<event>;

    // ---- environment -----------------------------------------------------

    /// The origin (`scheme://host[:port]`) the app was loaded from, if any.
    /// The host restricts outbound HTTP to this origin unless told otherwise.
    origin: func() -> option<string>;

    /// The full URL the app was loaded from, query string included, if it
    /// was loaded from one. The terminal's `window.location`.
    location: func() -> option<string>;

    /// Set the terminal window title.
    set-title: func(title: string);

    /// Tell the host the app is ready for the user: its data is loaded and
    /// it is showing a real screen rather than a placeholder. Optional;
    /// embedders that want a readiness signal beyond "a frame was drawn"
    /// wait for this.
    ready: func();

    /// Ask the host to look for a newer version now; returns the pending
    /// update afterwards. Fails when the source cannot be reached or
    /// refuses this host.
    check-update: async func() -> result<option<update>, string>;
    /// The pending update, with its deadlines as of now.
    pending-update: func() -> option<update>;
    /// Whether this app can be updated at all, and by whom.
    update-availability: func() -> availability;
    /// Restart the app in place on the pending update, or on the current
    /// version if there is none. Never returns: the instance is torn down
    /// and a new one is started.
    reload: func();

    enum log-level {
        trace,
        debug,
        info,
        warn,
        error,
    }

    /// Emit a log record. The host hands it to the embedder live, sanitised
    /// and rate limited; nothing is written to the terminal.
    log: func(level: log-level, target: string, message: string);
}

/// Key-value storage scoped to the app's origin, like a browser's
/// `localStorage`: private to the origin, bounded by a quota, persisted by
/// the host between runs when it has an origin to key it by.
interface storage {
    variant storage-error {
        /// The origin's quota (bytes or entries) would be exceeded.
        quota-exceeded,
        /// The key or value is larger than allowed.
        too-large,
        /// The host has storage disabled.
        disabled,
    }

    get: func(key: string) -> option<list<u8>>;
    set: func(key: string, value: list<u8>) -> result<_, storage-error>;
    remove: func(key: string);
    keys: func() -> list<string>;
    clear: func();
    /// Bytes in use and the quota, for the app to show or budget against.
    usage: func() -> tuple<u64, u64>;
}

/// WebSockets, provided by the host because WASI 0.2 has none. Connections
/// are subject to the same origin policy and cookie jar as HTTP requests.
interface websocket {
    variant message {
        text(string),
        binary(list<u8>),
    }

    variant error {
        /// The origin policy refused the connection.
        denied,
        /// Connecting or the handshake failed.
        connect(string),
        /// The connection is closed; the payload is the close reason, if any.
        closed(option<string>),
        /// A protocol or transport error after the connection opened.
        protocol(string),
    }

    resource socket {
        /// Connect to a `ws://`, `wss://`, `http://`, or `https://` URL and
        /// complete the handshake.
        connect: static async func(url: string) -> result<socket, error>;

        /// Wait for the next message. Once the connection has closed or
        /// failed, the error, every time.
        receive: async func() -> result<message, error>;

        /// Send a message. Waits while the outgoing queue is full, so a
        /// slow peer applies backpressure instead of growing host memory.
        send: async func(message: message) -> result<_, error>;

        /// Close the connection.
        close: func();
    }
}

/// A rattery app imports the terminal and websockets, plus (through `std` and
/// the `wasip3` crate) the WASI interfaces it links: 0.2 for stdio and clocks,
/// 0.3 for HTTP. It exports one async `run`; the host drives it with the
/// component model's async ABI, so the app is a real async program.
world app {
    import terminal;
    import websocket;
    import storage;

    /// Run the app to completion. `err` is a message the host shows the user.
    export run: async func() -> result<_, string>;
}