Scribe is a native GNOME Markdown editor that renders your document as you type — no preview switch, one surface. Built with Rust, GTK 4 and libadwaita. It renders Markdown live on the editing buffer so headings look like headings and bold text looks bold, without switching to a separate preview or a browser engine.
I wanted a Markdown editor that felt like part of GNOME, not a port of a web app. The ones I tried either wrapped a browser engine (ProseMirror, Milkdown inside WebKit) and consumed hundreds of megabytes, or showed a plain source buffer next to a rendered preview. I kept both panes open, going back and forth between them instead of just writing.
The idea is an editor where the source text IS the preview: the buffer stays
plain Markdown, but the rendering happens on top with GtkTextTag. There is
nothing to sync, no HTML under the hood, no browser engine to bundle. It
started as mockups for a GTK4 design exploration and evolved into something I
actually use to draft notes and docs.
- Rust + GTK4 + libadwaita — native toolkit, no Electron. The binary is around 8 MB and idles at a few dozen megabytes of RAM. A WebKit-based editor would be ten times that before opening a file.
- GtkSourceView 5 for the text buffer and pulldown-cmark for Markdown
parsing, then custom
GtkTextTagspans for rendering. No HTML or CSS in the preview. The editor is a singleGtkTextViewwith decorations applied to the buffer. - No database, no server, no JavaScript. It is a desktop application that opens, edits and saves files. Preferences go through GSettings.
- Live WYSIWYG rendering on the editing buffer: headings at real scale,
bold/italic/strikethrough, inline and fenced code, quotes, links, images,
footnotes, tables and HTML blocks via
GtkTextTagspans. - Rich rendered view on top of the buffer (
snapshot_layer): rounded code blocks, tables as a visual grid (bold header, painted column separators and a rule where the delimiter row was), a rule under H1/H2, vertical quote bars, drawn bullets per nesting level, checkable task boxes and horizontal rules. The mark a decoration replaces is shrunk instead of hidden. - Spell checking: misspelt words are underlined as you type, with suggestions and "ignore/add to dictionary" in the context menu; the language follows the system locale and it can be switched off in Preferences.
- Tables of prose as real tables: when a table's prose cells cannot fit its delimiter geometry (the common long-form table), the source rows collapse and the table is painted as a canvas: every cell wraps within its column, the header is bold with a rule, and alignments are honoured. The row under the cursor shows its raw source for editing.
- Local images in block: when an
is alone on its line and the file exists (relative to the document), it is painted scaled below the mark (max 144 px, 20 MB limit, cache of 64). Missing files show a discreet placeholder. The buffer is never modified: no widgets, no characters inserted. - Remote images: when the destination is a URL, Scribe asks for its size
first (20 MB limit), downloads it and caches it on disk under
~/.cache/scribe/images, so later runs paint it without going out again. A URL that fails keeps the placeholder and is not retried on every redraw. - YouTube cards: a video that is alone on its line, written as a Markdown
link, a bare URL or an
<iframe>, is drawn below the line as a 16:9 card with its thumbnail and a play badge. The line stays visible and editable, and there is no embedded player: clicking the card opens your browser. - Ctrl+click a link opens the destination with the default application;
holding Ctrl over one shows where it goes. Only
http,httpsandmailtoare offered, so the click stays a text click (cursor, selection) when there is nothing to open. - Configurable markup visibility: dim the syntax markers (
**,#, backticks) everywhere, hide them, or reveal them on the cursor line. Because of a GTK bug (gtk#8346), "hide" currently shrinks the marks (scale ~0) instead of removing them; when GTK ships the fix, real hiding returns on its own. - Focus mode (Ctrl+Shift+F) dims everything except the current paragraph.
- Typewriter mode (Ctrl+Shift+T) keeps the cursor vertically centered.
- Split preview on the right (Ctrl+Shift+P, or the header icon) renders the
full document in a side panel using the same
GtkTextTagengine, so you can keep an eye on the outline while you write. - Find in the document (Ctrl+F): a search bar inside the editor pane, with a match counter, next/previous buttons and Enter/Shift+Enter.
- Ctrl+B/I/K wraps the selection in
**,*or backticks. - Align tables (Ctrl+Alt+T) reformats all tables in the document so their columns line up in the source.
- List continuation: pressing Enter starts the next item and re-numbers ordered lists. Leaving an item empty closes the list.
- Zoom (Ctrl +/−/0).
- Header bar follows GNOME Text Editor: open button with recent files dropdown, new tab button, centered title, split preview toggle and main menu.
- Preferences window with three pages: Appearance, Editor and Export.
- Export to PDF (Ctrl+P): print or export the document through the GTK
print pipeline, reusing the same span/ornament analysis as the editor.
Headings, code blocks, quotes and tables are themed, with separate export
settings (color scheme, fonts, page size and orientation) and careful
pagination (no orphan headings,
\newpagesupport). - Files: open, save and save-as with
GtkFileDialog, atomic writes, unsaved-changes warning, configurable autosave. - Sidebar (F9) with filterable recent files and a navigable outline.
- Light and dark theme follows the GtkSourceView style scheme; the Preferences pick the colour scheme explicitly (follow system, light, dark).
- Integration:
scribe file.mdand "Open with" from the file manager. - Translated UI (gettext): English and Spanish. New translations go in
po/(seepo/update-pot.sh).
Two modules, neither depending on application types:
src/markdown_render.rsparses Markdown with pulldown-cmark and returns two lists: spans (byte ranges with aGtkTextTagname) and ornaments (elements keyed by line number to be painted). GTK-independent, unit-tested without a display.src/markdown_view.rsis aGtkSourceViewsubclass that implementssnapshot_layer, the vfunc GTK exposes for drawing below or above text. It works in buffer coordinates (GTK handles scrolling) and draws withgsk::PathBuilder.
Markers replaced by an ornament (bullets, [x], ---, >, fences, pipes) are
shrunk rather than hidden: the tiny glyph stays in GTK's layout, so the
invisible-text abort path of GNOME/gtk#8346 is unreachable by construction.
- UTF-8 only. Files are read as UTF-8 and written back byte for byte. A file in another encoding (Latin-1, UTF-16, and so on) does not open.
- The source is the preview. Rendering is a set of
GtkTextTagspans and painted ornaments over the buffer, not a layout engine. HTML in the source is not interpreted: it stays as dimmed text in the editor and is dropped in the split preview. The result is an approximation of the Markdown, not a copy of a browser rendering. - No replace, no export to HTML, and no spell checking. The find bar (Ctrl+F) searches the active document; replacing is not implemented.
- PDF export has no outline/bookmarks.
GtkPrintOperationdoes not expose PDF bookmarks, so the exported PDF carries no table of contents or outline. - Images: local files up to 20 MB are painted, at most 144 px tall, and remote URLs are downloaded with the same limits. Inline images (not alone on their line), missing files and downloads that fail show the placeholder.
- The image cache has no expiry. A picture edited on disk is not refreshed
until the document is reopened, and a downloaded one is reused from
~/.cache/scribe/imagesuntil you delete that directory. - Videos are not played inside the editor. The card is drawn from the video
id and a thumbnail fetched from
i.ytimg.com, and clicking it opens the browser. A link whose id is not a valid 11-character id is left as text. - In "hide/reveal on cursor line" mode the marks are shrunk, not removed (see above).
Scribe collects nothing and has no accounts, telemetry or cloud. The only
requests it makes are the ones a document asks for: remote images and video
thumbnails referenced by the text are downloaded to be painted and cached under
~/.cache/scribe/images. Nothing is ever uploaded. See
PRIVACY.md.
Main window with live Markdown rendering
Split preview (toggled from the header icon) and find in the document
Preferences window
Focus mode (everything dimmed except the current paragraph)
- Rust 1.85 or later
- GTK4 (≥ 4.14), libadwaita (≥ 1.5), GtkSourceView 5 and GLib development files
Arch / CachyOS:
sudo pacman -S rust gtk4 libadwaita gtksourceview5 glib2Debian / Ubuntu:
sudo apt install libgtk-4-dev libadwaita-1-dev libgtksourceview-5-dev libglib2.0-devFedora:
sudo dnf install gtk4-devel libadwaita-devel gtksourceview5-devel glib2-develInstall to ~/.local (no root) with:
make installThis builds the release binary and installs the binary to ~/.local/bin, the
GSettings schema to ~/.local/share/glib-2.0/schemas (recompiled with
glib-compile-schemas), the desktop entry and the icons. make build compiles
in release mode and make check runs fmt, clippy and the tests.
The GSettings schema is mandatory, not optional: when a release adds a new
preference, the app aborts if the installed schema is stale (it no longer
contains the new key). make install recompiles it so that does not happen.
cargo build --release
cargo runFor development you can point at the local schema instead of installing it:
glib-compile-schemas data/
GSETTINGS_SCHEMA_DIR=$PWD/data cargo runIf the schema is not found at all, the app starts with defaults (preferences are not saved) and logs a warning to stderr.
A Flatpak manifest is at build-aux/flatpak targeting the
GNOME 50 runtime. cargo-sources.json is generated with
flatpak-cargo-generator
and Cargo.lock is committed:
cargo generate-lockfile
python3 flatpak-cargo-generator.py Cargo.lock -o build-aux/flatpak/cargo-sources.json
cd build-aux/flatpak
flatpak-builder --user --install build-dir app.scribe.Scribe.json --force-cleangit clone https://github.com/gnacho/scribe.git
cd scribe
cargo build
cargo test
cargo runAGPL-3.0-or-later. See LICENSE.



