rabbita_xterm

Rabbita-friendly xterm.js bindings for MoonBit

rabbita
xterm
terminal
web
moon add tonyfettes/rabbita_xterm@0.1.2
Download zip
Version
0.1.2
License
Apache-2.0
Last updated
3 months ago
Downloads
22

Dependencies

README

#rabbita_xterm

rabbita_xterm is a MoonBit binding that makes xterm.js usable from Rabbita applications.

The package has two layers:

  • tonyfettes/rabbita_xterm provides the Rabbita-style managed API: model state, actions, subscriptions, and a host view.
  • tonyfettes/rabbita_xterm/js exposes the lower-level xterm.js wrapper for advanced use cases and parser/event hooks.

#Demo

The repository includes an asciicast player demo in examples/web. It replays asciicast v2/v3 NDJSON through xterm.js, including terminal resize events and OSC title changes.

Build it from the repository root:

moon build --target js --release examples/web

Serve the repository root:

python3 -m http.server 8765 --bind 127.0.0.1

Open:

http://127.0.0.1:8765/examples/web/

The demo loads xterm.js, xterm.css, and Lucide icons from pinned CDN URLs, so the browser needs network access.

The repository also includes a GitHub Pages workflow at .github/workflows/pages.yml. Once Pages is configured to use GitHub Actions as its source, pushes to main publish the demo at:

https://moonbit-community.github.io/rabbita_xterm/

#Managed API

Store @xterm.State in your app model, route @xterm.Action through @xterm.update, render state.view(...), and return @xterm.subscriptions(...) from your app subscriptions.

priv struct Model {
xterm : @xterm.State
}

priv enum Msg {
Xterm(@xterm.Action)
XtermEvent(@xterm.Event)
}

fn initial_model() -> Model {
{
xterm: @xterm.new(
cols=80,
rows=24,
background="#101418",
foreground="#dce4ea",
),
}
}

fn update(
dispatch : @cmd.Dispatch[Msg],
msg : Msg,
model : Model,
) -> (@cmd.Cmd, Model) {
match msg {
Xterm(action) => {
let (cmd, xterm) = @xterm.update(action, model.xterm, event => {
dispatch(XtermEvent(event))
})
(cmd, { ..model, xterm, })
}
XtermEvent(Ready) =>
(dispatch(Xterm(@xterm.write(@utf8.encode("ready\\r\\n")))), model)
XtermEvent(_) => (@cmd.none, model)
}
}

fn view(_dispatch : @cmd.Dispatch[Msg], model : Model) -> @html.Html {
model.xterm.view(class="terminal-host")
}

fn subscriptions(dispatch : @cmd.Dispatch[Msg], model : Model) -> @sub.Sub {
@xterm.subscriptions(model.xterm, action => dispatch(Xterm(action)))
}

Useful actions:

  • @xterm.write(bytes) / @xterm.writeln(bytes)
  • @xterm.clear() / @xterm.reset()
  • @xterm.resize(cols=..., rows=...)
  • @xterm.set_theme(background=..., foreground=...)
  • @xterm.fit() when use_fit=true

Useful events:

  • Ready
  • Data(String)
  • Resized(Size)
  • TitleChanged(String)
  • LoadFailed(String)
  • FitFailed(String)

@xterm.new owns the host id internally. For cast/replay-style demos where the terminal size should be determined by recorded rows and columns, use use_fit=false and set scrollback=0.

#Packages

The root module is tonyfettes/rabbita_xterm.

xterm.mbt managed Rabbita API js/xterm.mbt xterm.js FFI wrapper js/listen.mbt lower-level subscriptions and parser hooks addon/fit/fit.mbt @xterm/addon-fit wrapper examples/web/ asciicast player demo

#Development

Run the normal MoonBit checks before committing:

moon check moon fmt moon info

Build the web demo:

moon build --target js --release examples/web

#
Action

type Action

Opaque action handled by update.

Construct values with helpers such as write, resize, and fit, or pass actions received from subscriptions back to update.

#
Event

pub enum Event {
Ready
Data(String)
Resized(Size)
TitleChanged(String)
LoadFailed(String)
FitFailed(String)
}

Events emitted by the managed terminal.

Feed these back into your application message type from the on_event dispatch passed to update.

#
Size

pub(all) struct Size {
cols : Int
rows : Int
} derive(Eq)

Logical terminal size measured in character cells.

cols is the number of columns and rows is the number of rows passed to xterm.js. The managed API updates this value after explicit resize actions and terminal resize events.

#
State

type State

Managed Rabbita state for one xterm.js instance.

Store this in your application model, render it with State::view, handle Action values with update, and install subscriptions from your app's subscription function. The lifecycle subscription owns loading and disposal of the underlying terminal.

#
State::cols

fn State::cols(self : State) -> Int

Last known column count.

#
State::is_ready

fn State::is_ready(self : State) -> Bool

Whether the terminal has loaded and been opened in the DOM.

#
State::rows

fn State::rows(self : State) -> Int

Last known row count.

#
State::size

fn State::size(self : State) -> Size

Last known terminal size.

#
State::status

fn State::status(self : State) -> Status

Current loading status.

#
State::status_text

fn State::status_text(self : State) -> String

Human-readable status text suitable for a small loading/error label.

#
State::title

fn State::title(self : State) -> String

Last known terminal title.

This starts from the title argument passed to new and is updated when the terminal emits TitleChanged.

#
State::view

fn State::view(self : State, class? : String) ->
Html

Render the DOM host element for this terminal.

Include this exactly once in your view while the state is alive. The managed lifecycle opens xterm.js into this element after render, so the id is generated internally and does not need to be supplied by the application.

#
Status

pub enum Status {
Loading
Ready
Failed(String)
} derive(Eq)

Loading state for a managed terminal.

#
clear

fn clear() -> Action

Clear the visible terminal buffer.

This corresponds to xterm.js term.clear. If the action is handled before the terminal reaches Ready, it is a no-op.

#
fit

fn fit() -> Action

Ask the fit addon to resize the terminal to its DOM container.

This only has an effect when new(use_fit=true) was used and the fit addon has loaded. The managed subscription also triggers this on window resize.

#
new

fn new(use_fit? : Bool, background? : String, foreground? : String, cursor_blink? : Bool, font_size? : Int, cols? : Int, rows? : Int, mac_option_is_meta? : Bool, scrollback? : Int, title? : String) -> State

Create a managed terminal state.

The terminal is not loaded immediately. It starts in Loading, and the first call to subscriptions schedules loading xterm.js and, when use_fit is true, the fit addon. cols and rows are the initial logical cell size; if fit is enabled, the addon may resize the terminal after it is attached to the DOM.

#
reset

fn reset() -> Action

Reset terminal state, modes, cursor, and buffers.

This corresponds to xterm.js term.reset. It is stronger than clear. If the action is handled before the terminal reaches Ready, it is a no-op.

#
resize

fn resize(cols~ : Int, rows~ : Int) -> Action

Resize the terminal to an explicit logical cell size.

The managed state is updated immediately. xterm.js receives the resize when the terminal is ready; before Ready, only the stored initial size changes.

#
set_theme

fn set_theme(background~ : String, foreground~ : String) -> Action

Update the terminal foreground and background colors.

The managed state is updated immediately. xterm.js receives the theme change when the terminal is ready; before Ready, these colors become the initial theme used at construction time.

#
subscriptions

Install subscriptions for the managed terminal.

This loads xterm.js while the state is Loading, forwards user input, resize, and title-change events, and disposes the underlying JS resources when the subscription is removed. Applications using the managed API should not call low-level dispose directly.

#
update

Update managed terminal state.

Call this from your application update branch for Action values. The returned command performs the xterm.js side effect, and on_event maps terminal events back into your application messages.

A typical Rabbita app stores State, wraps Action in an app message, and passes terminal events through another app message:

let (cmd, xterm) = @xterm.update(action, model.xterm, event => dispatch(XtermEvent(event)))

#
write

fn write(data : Bytes) -> Action

Write output bytes to the terminal.

This corresponds to xterm.js term.write. It is intended for program output such as an asciicast stream. User keyboard input arrives separately as Event::Data.

If the action is handled before the terminal reaches Ready, it is a no-op.

#
writeln

fn writeln(data : Bytes) -> Action

Write output bytes followed by a newline.

This corresponds to xterm.js term.writeln. If the action is handled before the terminal reaches Ready, it is a no-op.

Source Files

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io