rotulus 0.2.2

A GTK4 scrollback view for text-stream chat
Documentation

Rotulus

A GTK4 scrollback view for text-stream chat that stays fast and stable however long the scrollback gets.

A rotulus is a medieval scroll, read top to bottom and grown by stitching on new sheets, which is how chat scrollback grows too. Rotulus is a successor to HexChat's xtext, rebuilt around structured messages instead of text with escape codes in it. It started life as the chat view in GtkHx, which is still its first user.

Rotulus is not 1.0 yet, and its API may still change.

What it promises

  • Cost follows what is on screen, not how long the scrollback is. Appends, resizes and zooms lay out only the rows you can see. A row that never scrolls into view is never shaped.

  • Your reading position never jumps. The scroll position is a row plus an offset, not a pixel value. Loading older history, trimming old lines, an image finishing its download, a resize and a font change all leave the text you are reading where it was. Sticking to the bottom is built in.

  • Scrollback grows at both ends. Prepending a batch of history (IRCv3 chathistory, Matrix backfill) is cheap, and so is trimming to a line limit.

  • Messages are data, not text with codes in it. The application passes a speaker, styled runs, and blocks of text, code, quotes and images. The widget parses no escape codes, so a remote user can't restyle your chat by sending control bytes.

  • Chat conventions work out of the box. Rotulus supports:

    • grouping by speaker
    • avatar and timestamp columns
    • copy that includes or leaves out timestamps
    • search and zoom
    • inline images
    • markdown code and quote blocks
    • a last-read marker line
    • a single-column mode for logs

    With no colors set, it follows the Adwaita theme. Whatever the colors, text is kept legible against what it is drawn on, and animations hold still when the desktop turns them off. An application that recolors the view's background in CSS passes the same color as the palette's background, since the view can't read it back.

  • The engine is tested without a display. The layout crate has no dependencies, and its whole suite runs under plain cargo test.

Against GtkTextView and GtkListView, at 20,000 messages (medians on one machine, at 60 Hz):

Rotulus GtkTextView GtkListView
ingest 38 ms 190 ms 4.8 s
settled after the burst 12 ms 540 ms 77 ms
resize, until settled 7 ms 495 ms 12 ms
memory 15.6 MB 41 MB 40 MB

The full tables, including 200,000 messages, are in docs/design.md.

What it isn't

  • Not a widget per message. It has no reaction buttons or thread expanders inside rows. An application that needs them wants GtkListView.
  • Not an editor. The view is read-only.
  • Not a terminal. It has no cursor addressing and no VT escapes; that's VTE's job.
  • No protocol knowledge. A protocol converts its formatting into styled runs before appending them. rotulus-mirc does that for IRC formatting codes, as an opt-in.
  • GTK4 only.

Crates

Crate What it is
rotulus-layout The layout engine: the message model, wrapping, the height index, scroll anchoring, selection and search. It has no dependencies.
rotulus The GTK4 widget over the engine, and the C ABI in include/rotulus.h. Meson builds it into librotulus-1.
rotulus-mirc Converts IRC (mIRC) formatting codes into styled runs. The rotulus crate's default mirc feature exposes it to C as rotulus_mirc_parse.

Using it

From C, link librotulus-1 through pkg-config:

cc app.c $(pkg-config --cflags --libs rotulus-1)
#include <rotulus.h>

RotulusView *view = ROTULUS_VIEW (rotulus_view_new ());

RotulusRun nick = ROTULUS_RUN_PLAIN ("alice", -1);
RotulusRun body = ROTULUS_RUN ("hello", -1, ROTULUS_COLOR_DEFAULT,
                               ROTULUS_ATTR_BOLD);
RotulusRow row = {
    .kind = ROTULUS_ROW_MESSAGE,
    .speaker = { .key = 42, .nick = "alice", .nick_len = -1 },
    .gutter = &nick, .n_gutter = 1,
    .body = &body, .n_body = 1,
};
rotulus_view_append (view, &row);

From Python, JavaScript or Vala, through GObject introspection:

import gi
gi.require_version("Rotulus", "1")
from gi.repository import Rotulus

view = Rotulus.View()
msg = Rotulus.Message.new(Rotulus.RowKind.MESSAGE)
msg.set_speaker(42, "alice")
msg.add_text("hello", Rotulus.COLOR_DEFAULT, Rotulus.ATTR_BOLD)
view.append_message(msg)

From Rust:

use rotulus::RotulusView;
use rotulus_layout::{Message, ParsedText, Speaker};

let view = RotulusView::new();
view.append(Message::live(Speaker::new(42, "alice"), ParsedText::plain("hello")));

The view is a GtkScrollable, so put it in a GtkScrolledWindow. Leave it focusable: Tab steps through its links, speakers, images and load-more rows, and Enter activates them as a click does. The application reacts through signals:

  • link-activated, link-menu
  • speaker-activated, speaker-menu
  • load-more
  • media-activated
  • selection-changed

The API reference for C and the language bindings is at https://mishan.github.io/rotulus/; the Rust API is on docs.rs.

Its behavior is set through GObject properties such as markdown, link-schemes, activate-links, show-timestamps and max-lines. docs/design.md covers what an application hooks, and why the widget is shaped the way it is.

Building

You need GTK 4.10 or newer and Rust 1.92 or newer (see rust-toolchain.toml).

The shared library, its header, the pkg-config file, the introspection data, the Vala bindings and the translations are built with Meson, which drives cargo:

meson setup _build
meson compile -C _build
xvfb-run -a meson test -C _build     # the smoke tests need a display
meson install -C _build

Meson options:

  • introspection (auto): the GIR and typelib. Needs gobject-introspection.
  • vapi (auto): the Vala bindings. Needs vapigen.
  • documentation (false): the API reference, with gi-docgen, from the introspection data. Installed under share/doc/rotulus-1.
  • tests (true): the smoke tests. They drive the library from C, Python and GJS, and check that it exports nothing but its C API.

It builds against GTK 4.14's accessibility interface when it finds GTK 4.14 or newer, and adds 4.16's on-screen extents when it finds 4.16.

The Rust crates build and test with cargo alone:

cargo build
cargo test -p rotulus-layout                 # headless
xvfb-run -a cargo test --workspace            # the widget's tests need a display

Cargo features on rotulus:

  • mirc (default): the C entry points to rotulus-mirc.
  • v4_14: implements GtkAccessibleText, so a screen reader can read the transcript. Needs GTK 4.14. Without it, the view builds against GTK 4.10 and exposes only its accessible role, and an application has to announce new messages itself for a screen reader to hear them.
  • v4_16: also reports where the text is on screen, for magnifiers and screen readers that follow it. Needs GTK 4.16.
  • capi: for the shared library alone. It switches off gtk4-rs's check that Rust initialized GTK, which a C caller never does.

The golden render tests draw with the bundled DejaVu Sans Mono and compare the result at a tolerance. To accept a deliberate change to the rendering:

ROTULUS_UPDATE_GOLDEN=1 xvfb-run -a cargo test -p rotulus --test render

Benchmarks:

  • The engine: cargo bench -p rotulus-layout runs the criterion benchmarks.
  • Against GtkTextView and GtkListView: run crates/rotulus/examples/compare.rs.

Translations

The widget's own strings, in its context menus, are in the rotulus gettext domain. The catalogs are in crates/rotulus/po, and meson install installs them. An application whose catalogs are elsewhere calls bindtextdomain ("rotulus", localedir). Without a catalog, the menus are in English.

A change that adds or changes a string needs it translated in every language before it merges; tools/check-translations.sh, which CI runs, says what is missing. meson compile -C _build rotulus-update-po brings the catalogs up to date with the source.

License

LGPL-2.1-or-later. See COPYING. The bundled test fonts carry their own license, in crates/rotulus/tests/fonts/LICENSE.