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-mircdoes 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:
RotulusView *view = ;
RotulusRun nick = ;
RotulusRun body = ;
RotulusRow row = ;
;
From Python, JavaScript or Vala, through GObject introspection:
=
=
From Rust:
use RotulusView;
use ;
let view = new;
view.append;
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-menuspeaker-activated,speaker-menuload-moremedia-activatedselection-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 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 undershare/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 features on rotulus:
mirc(default): the C entry points torotulus-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
Benchmarks:
- The engine:
cargo bench -p rotulus-layoutruns 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.