A GNOME (GTK4 + libadwaita) editor for writing blog articles in Markdown
and exporting them as native WordPress Gutenberg blocks — not a single
classic/freeform HTML block, but real, individually editable blocks
(core/paragraph, core/heading, core/list, ...) — to a self-hosted
WordPress site via its REST API.
Split-screen editing: Markdown on the left (GtkSourceView, syntax highlighting), a live HTML preview on the right (WebKit), with Gutenberg export running in the background against your own WordPress install using an Application Password.
Functionally complete for its core purpose - write Markdown, review a live preview, and publish/update a real WordPress post as native Gutenberg blocks. Implemented so far:
- Workflow from first draft to published post (see
docs/gui-redesign.md) — write locally, upload as a draft, correct, upload again, publish:- The library sidebar on the left (
AdwSidebar) lists "In Arbeit" - every article in the library, most recently changed first, with its WordPress status as subtitle and a paper-plane icon for changes not uploaded yet - and "Im Blog" with the site's drafts, scheduled, published posts, pages and trash and their counts. Picking a group opens the blog archive: server-side search, 50 posts at a time while scrolling, trash/restore; opening a post creates its working copy in the library (or reopens the existing one). - One main action at the end of the header bar says what it does for the open article: "Als Entwurf hochladen" (the first upload is always a draft), "Entwurf aktualisieren", "Veröffentlichen …", "Änderungen veröffentlichen …" - with submit for review, update and preview, compare, schedule, revert to draft and discard in its menu. The window title shows the article and its state ("Entwurf · nicht hochgeladen").
- Publishing goes through a release check: title, excerpt, category, tags, featured image, alt texts, links, focus keyword, each with a way to fix it, plus "Sofort / Geplant".
- A sync check (at start, when the window becomes active again, and on demand) notices posts changed or deleted on the blog; a banner then offers to load the blog's version, resolve a conflict or unlink. "Mit Blog-Fassung vergleichen" shows a line diff of the working copy against the blog's current version before deciding.
- A first upload interrupted by a dropped connection doesn't create a second post on the next try: Blocksatz first looks for the post it may already have created.
- "Vorschau im Blog" shows changes to a published post in the blog's theme without touching the live post (as a WordPress autosave).
- Several blogs: Einstellungen → WordPress lists them (add, edit, remove, pick the active one); the sidebar's footer switches between them. "In Arbeit" shows the active blog's working copies and the local-only ones; uploads, previews and the sync check of a working copy always go to the blog it belongs to.
- Translations for a second blog (see
docs/translations.md): the English version of a post lives next to its original asartikel.en.mdin the same library folder - one sidebar row per article, showing the state of each language. The DE · EN switch in the header bar (Alt+1 / Alt+2) flips between the two at the same paragraph, the right-hand pane shows the other language following along, and the language decides the blog an upload goes to. Translate by hand from the original as a template, or copy the original into DeepL or a chat with code and links protected and paste the result back - the placeholders come back and the checks say what doesn't match. When the original changes, its view marks the changed sections with a word diff and an "Erledigt" button each. The AI translation is there as an option: section by section, code and markup untouched, and only published once you've reviewed it. - The right-hand pane (F9) has four views: Vorschau (rendered / Gutenberg code / "Im Blog" - the open article as the blog itself shows it, draft preview or live post), Beitrag (state, all post properties, media, statistics), Assistent (chat / evaluation) and a Browser of its own (switchable in Einstellungen → Browser).
- The library sidebar on the left (
- Split-pane editor — the window remembers its size (and whether it was
maximized) across restarts, together with the layout of its panes: the
editor/pane split (a ratio, 50/50 to start), whether the sidebar and the
pane are shown, and the pane's last view; a header-bar toggle button
collapses the whole right-hand pane for a full-width editor and restores
it again. Below roughly 700sp of window width (a tiled quarter of a
typical monitor, or a Linux tablet in portrait) - via
libadwaitabreakpoints - the side-by-side split gives way to a single pane at a time, switched by anAdw.InlineViewSwitcherstyled like the sidebar's own tab switcher, and reverts automatically once the window is wide enough again; the formatting toolbar scrolls horizontally rather than clipping if it doesn't fit at that width. A second toggle button next to it (Ctrl+Shift+F) is a Fokus-Schreibmodus, additionally hiding the header bar and the editor's own formatting toolbar down to just the editor text -Adw.ToolbarView's own animated reveal handles the header/status bar, so entering and leaving is a smooth slide rather than an abrupt layout jump. A plain launch with no file argument reopens the most recent one automatically instead of always starting at a blank "Unbenannt" document -Ctrl+Nstill gets to a blank one in one step. Markdown editing pane (GtkSourceView, syntax highlighting, spell-checking vialibspelling) with a compact formatting toolbar in three groups - inline (bold Ctrl+B, italic Ctrl+I, strikethrough Ctrl+Shift+X, code Ctrl+E, link Ctrl+K), line/block (heading menu with Ctrl+2/3/4/0, list, numbered list, quote - applied to every selected line and toggled back off - code block, table) and insert ("Bild einfügen" opening a native image file picker instead of typing a filename by hand, plus an "Einfügen" menu with video/audio, media library, separator, "Weiterlesen" marker, containers and dynamic blocks); "Bestehenden Artikel verlinken" opening a searchable picker over the site's existing posts and inserting a real Markdown link to the one picked; pasting an image straight from the clipboard with Ctrl+V - a screenshot, or "Copy Image" from a browser - saves it into the article's own folder and inserts it; pasting rich text copied from a browser, word processor, or anywhere else that puts atext/htmlentry on the clipboard alongside its plain-text one converts that formatting to Markdown on the way in (headings, bold/italic, links, lists, tables - via a real HTML5 parser rather than a hand-rolled one, to hold up against how varied real-world HTML actually is) instead of dropping it, falling through to a normal plain-text paste only when there's genuinely no image or HTML on the clipboard; dragging one or more local files from a file manager onto the editor inserts them the same way, even in an unsaved article; a "Weiterlesen" button inserting WordPress's<!--more-->marker, exported as a realwp:moreblock rather than generic HTML), a Ctrl+F search-and-replace bar sliding up from the bottom of the editor (live match highlighting and count, next/previous navigation, replace one or all), a debounced live HTML preview kept in scroll-sync with the editor (matched by source line, not scroll percentage, so a tall image doesn't throw off the sync), and a footer status bar with word count and reading time for the whole article - plus the same two numbers for the current selection, whenever one is active. The right pane's views are "Vorschau" - rendered (follows the app's light/dark mode, with a choice of Modern/Klassisch/Sepia typographic styles picked in Einstellungen; a caption renders as a small line under its image, matching the published post; every image gets small badges in its bottom-right corner - an upload arrow once it's on WordPress, "Alt" once its alt text is defined (hovering it shows the actual alt text as a tooltip, not a generic sentence), and its file format - in that fixed order whenever more than one applies, updated live from Medienverwaltung/ the alt-text dialogs, not just on the next edit; right-clicking an image offers "Alternativtext bearbeiten…"/"KI-Alternativtext generieren…"/"Bild bearbeiten…" - WebKit's own default image actions (open/save/copy the rendered file, copy its address) are trimmed from that menu, alongside the navigation items, since none of them apply to an embedded article image; hovering a link shows where it goes), Gutenberg code (the exact block HTML that would be published) and "Im Blog" -, "Beitrag", whose statistics section has word/character/paragraph counts, estimated reading time, and a German-adapted Flesch reading-ease score with a qualitative label - expandable into the formula itself, the article's actual average words-per-sentence and syllables-per-word, and concrete tips for improving the score, derived from whichever of those two numbers is actually holding it down), and "Assistent" with "Chat" - a writing assistant with message bubbles (replies rendered as Markdown), backed by Gemini, ChatGPT, Claude, Groq, or Ollama (self-hosted, no API key), with a provider/model picker both in the tab itself and in Einstellungen. A message typed here gets the editor's current selection - or, if nothing's selected, the whole article - appended before it's sent, the same rule the context menu's AI actions below already follow, so the model always has the article as context without pasting it in by hand. The "Browser" view is a plainWebKitview with an address bar and back/forward/reload controls, for consulting documentation or the live target site without alt-tabbing away - typing a bare domain addshttps://automatically, anything else is sent to Google as a search query. Its start page (Startpage by default) is configurable in a "Browser" page in Einstellungen, which also holds a "Werbung blockieren" toggle (on by default) - basic ad/tracker blocking built on the same WebKit content-blocker mechanism GNOME Web itself uses, driven by a real EasyList-syntax rule file rather than a hand-coded domain list, so it can be extended without a code change. - AI actions in the editor's context menu — right-click the editor for "Inhalt prüfen", "Stil & Formatierung prüfen", "Rechtschreibung prüfen", "Zeichensetzung prüfen", and "Länge anpassen…"; each sends the selection (or the whole article, if nothing's selected) to the Chat tab with a matching prompt. All five built-in prompts are editable/resettable in a "KI-Prompts" settings page, which also holds your own custom prompts (kept separate from the built-ins), reflected in the context menu immediately as you edit them.
- Gutenberg block engine (
crates/gutenberg) — a standalone, unit-tested library that parses Markdown into a block tree and renders it as block-comment-annotated HTML (<!-- wp:paragraph -->...), independent of the GUI. A local image/video/audio file referenced with![]()becomes the matchingwp:image/wp:video/wp:audioblock by its file extension, and a bare URL alone on its own line becomes a realwp:embedblock (YouTube, X/Twitter, Vimeo, Instagram, SoundCloud, Spotify recognized by name for a nicer immediate block-editor preview; any other URL still embeds generically, the same way WordPress's own editor falls back to oEmbed discovery for it). Since Markdown has no native syntax for side-by-side columns, buttons, a photo gallery, a highlighted pullquote, or a collapsible disclosure widget, those are written as fenced code blocks with a special "language" tag -```columns(split into columns on a+++line, each side re-parsed as ordinary Markdown),```buttons(one Markdown link per line),```gallery(one Markdown image per line),```pullquote(quote text and an optional citation, split on a+++line), and```details(a summary and its body, also+++-split, the body re-parsed as ordinary Markdown,wp:detailsin modern WordPress),```preformattedand```verse(lines and spacing kept) - all with full round-trip support back to the same Markdown when re-opening an existing post. Design Markdown has no syntax for goes into an attribute line in curly braces below the block (Pandoc/kramdown style), using the blog theme's preset slugs:{bg=accent color=base},{gradient=accent-fade},{size=large align=center},{style=stripes},{width=100%},{dropcap},{reversed}, boxes with{padding=1.5rem border="1px solid #ddd" radius=10px shadow=natural}, custom colors ({color=#1d4ed8}), typography ({line-height=2 weight=300 transform=uppercase}),{link-color=warning},{marker=upper-roman}for list numbering and{aspect=1 scale=cover}for images; buttons take{justify=center}below the fence and[Text](url){style=outline radius=0px width=50% newtab}per button; a heading carries it at its end (## Titel {#anker color=accent}). Tables, galleries and embeds take{caption="..."}, tables also{footer}(last row is the footer); an ordered list starting at5.keeps its start number. Images follow this app's convention- the same brackets caption audio and video, and may hold links and emphasis (](bild.png)) - and a linked image is plain Markdown,[](ziel). A quote's last paragraph starting with an em dash becomes its citation:> — Cicero, *De finibus*. Footnotes are written as on GitHub,Satz.[^1]with[^1]: Quelleanywhere (Einfügen → Fußnote numbers and places them), and go out as WordPress footnotes - the references, the footnote list and thefootnotesmeta; a post opened from the blog gets its footnotes back as[^1]. Blocks that hold other blocks are fenced containers (Pandoc/MyST style), their content ordinary Markdown, nestable:::: group {bg=base-2 style=lui-card layout=grid columns=3},:::: columnswith::: column {width=25%},:::: accordionwith::: item "Frage" {open},:::: tabswith::: tab "Reiter 1",::: cover {image=titel.png overlay=contrast dim=60 height=420px},::: media-text {image=bild.png position=right valign=center fill},::: details "Zusammenfassung" {open}. A cover's or media-text's local image is uploaded with the other images. A line of colons closes the innermost container. The older```columns/```detailsfences are still read. - Lossless import — opening a post from the blog turns a block into
Markdown only if that Markdown renders back to the same block structure
(attributes, classes, styles, captions, table footers); anything
Markdown can't carry (a border on one side only, a link with
attributes beyond its address, dynamic blocks) stays as its original block markup and
goes back unchanged. Inline markup without Markdown syntax (
<mark>,<sub>, a link withtarget) is kept as inline HTML. - Block inspector — the "Beitrag" view has a "Block" section for the block the cursor is in: text color and background (colors and gradients as swatches from the blog theme's palette), font size, alignment and the block styles the theme registers - only what the block supports. Every change rewrites the attribute line (or a heading's braces, or a container's opening line) as one undoable edit.
- Markdown closeness check — opening a post from the blog rates how
much of it is beyond plain Markdown; a heavily designed post suggests
editing it in wp-admin instead, a moderately designed one gets a banner
with details (
docs/markdown-naehe.md). - Theme presets in the preview — the active blog's color palette,
gradients, font sizes and block styles are fetched over the REST API
(
src/themestyle.rs, cached per blog) and turned into the same preset classes WordPress generates, so attribute lines, striped tables, accordions and tabs look in the preview as they will on the blog. - Document model — per-article frontmatter (title, slug, status -
Entwurf/Ausstehend/Veröffentlicht/Geplant/Privat, matching every native
WordPress post status -, scheduled publish date/time, categories, tags,
excerpt/meta description, RankMath SEO title/description/focus keyword,
featured image and its own alt text, WordPress post id) stored in the
.mdfile itself, editable in the right-hand pane's "Beitrag" view with autocomplete for every existing WordPress category/tag (backed by an on-disk cache,src/termcache.rs, fully paginated so it never silently caps out on a site with hundreds of tags, refreshed at startup and on demand) and a native file picker for the featured image, not just a path field - its alt text is sent as the resulting WordPress attachment'salt_texton upload, the same as any body image's. A "Slug aus Titel generieren" button fills the slug from the title using the same transliteration WordPress's ownsanitize_title()uses, and a "URL-Länge (SEO)" row shows the full URL the post would actually publish at - domain, real category slug, and post slug together, not just the slug in isolation - with a green checkmark once it's within Google's search-result truncation length, or a warning past it. A "Kategorien & Tags verwalten" dialog next to the autocomplete's refresh button lists every existing category/tag and lets you rename or permanently delete one straight from the app, instead of only ever being able to read or auto-create a term. - Library and continuous saving — every article lives in its own
folder under
~/Dokumente/Blocksatz/(<slug>/artikel.mdplus its images). A new article gets its folder as soon as something is typed (named by date and time, renamed after the title once the first#heading is finished); a post opened from WordPress gets one right away, and opening the same post again reopens that working copy instead of overwriting it. The open article is written to disk every two seconds and when the window closes, so there is no unsaved state to lose. Markdown files from elsewhere still open in place; they are only written once actually edited (or with Ctrl+S). - Editing existing posts — the blog archive (Ctrl+Shift+O opens the
drafts) opens any post or page as Markdown:
crates/gutenberg's reverse converter turns its Gutenberg block HTML back into Markdown, categories/tags are resolved from ids back to names, and the post's id carries over so uploading afterward updates that same post instead of creating a duplicate. - WordPress pages — besides blog posts, Blocksatz edits static pages
("Impressum", "Über mich"): a "Typ" row in the "Beitrag" view (locked
once the document is linked to WordPress), "Neue Seite" (Ctrl+Alt+N, in
the menu of the sidebar's new-article button), and upload/preview/
conflict checks all targeting
/wp/v2/pages. The "Kategorien & Tags" group is hidden for pages, which have neither taxonomy. - WordPress-Mediathek (Ctrl+Shift+L, primary menu) — browse and manage the whole media library without opening wp-admin: a thumbnail grid (WordPress's own generated thumbnails, type icons for documents/audio/ video), a type filter (Alle Medien/Bilder/Dokumente/Audio/Video), server-side search, 48 items per page behind "Mehr laden", and a details pane with file name, MIME type, dimensions, size, upload date, alt text and URL - plus "URL kopieren", "Im Browser öffnen", "In Artikel einfügen" (images, videos and audio) and "Endgültig löschen". The insert menu's "Aus WordPress-Mediathek …" opens the same browser.
- KI-Artikel schreiben (Ctrl+Shift+G, menu of the sidebar's new-article button) — drafts a whole
article from a topic/brief with the active KI-Chat provider, at a chosen
length, optionally imitating your own writing style: your 1-5 most
recently published posts are sent along as style samples (style only,
not content). The result lands in an editable preview first and is only
then used "Als neues Dokument" (title taken from its
#heading) or inserted "An Cursor". - Medienverwaltung (Ctrl+Shift+M, also reachable from the "Beitrag"
view and as the "Bilder" page of the release check right before
publishing, and per-image via "Alternativtext
festlegen…" in the editor's right-click context menu - which, like its
"KI-Alternativtext generieren…" neighbor, only appears when the click
actually landed on a line with a media reference, rebuilt live from the
cursor position on every right-click rather than always shown) — every
unique image referenced in the article gets its own alt text, caption,
and WordPress upload state, independent of the Markdown source
(persisted alongside the rest of the document in the frontmatter) - the
same image referenced more than once (a logo, a divider) shares that one
entry across every occurrence rather than getting a separate one each
time. Alt text is a three-state value rather
than a plain on/off: not yet defined (flagged by a non-blocking "N von M
Bildern haben noch keinen Alternativtext" hint), deliberately left empty
for decorative images (not treated as an error), or defined text - an
unusually long one (WCAG guidance: well under 150 characters, since a
screen reader reads the whole thing aloud) gets its own non-blocking
warning icon and tooltip, here and everywhere else alt text can be
entered (the featured image's field, the quick-edit dialog); the
caption is a separate field, never derived from the alt text, though it
is seeded the first time an image is seen from the Markdown image's
optional
"title"() if present, else from its bracket text () - most images never get the quoted-title form, so the bracket text is usually the only description there is - and both alt text and caption actually reach the published post's HTML on export (a real<figcaption>, not just an invisible<img title="">), not only the WordPress media library's own attachment metadata. A "Zu WordPress hochladen" button per image uploads it via the real REST API and stores the resulting media id/URL so re-opening the article recognizes it as already uploaded rather than re-uploading it; the list's own "Aufmacherbild" row does the same for the featured image, showing whether one is set, pending upload, or already live. An "Alle hochladen" button above the list uploads every not-yet-uploaded image in one go - the featured image included, if one's pending - instead of one at a time, tracked with a progress bar and a final summary of how many succeeded, and a failure partway through doesn't stop the rest. A "Bild einfügen…" button in the editor toolbar opens a native image file picker and inserts a real Markdown image reference at the cursor (relative to the document's own folder when possible) - previously the only way to add an image reference was to type its filename by hand. A "Video/Audio einfügen…" button next to it does the same for local video/audio files - the same![]()reference, just picked from a video/audio file filter instead. A right-click on an image - its Markdown line in the editor, or the rendered image itself in the Vorschau pane - also offers "KI-Alternativtext generieren…": the active KI-Chat provider looks at the real image and proposes an accessible alt text at a choice of three detail levels (Standard/Ausführlich/Hohe Genauigkeit, remembered across uses), only starting once "Text generieren" is actually clicked, shown for review and correction before it's applied directly into the same alt-text field, ready for the next upload - applying it keeps the preview scrolled to wherever it already was rather than jumping back to the top. The rendered image in the Vorschau pane also offers a plain "Alternativtext bearbeiten…" (the same dialog as the editor's own line-based shortcut, without the AI step) and "Bild bearbeiten…": convert it to PNG/JPEG/WebP and/or resize it by width or height (with an optional "Seitenverhältnis beibehalten" toggle for a free, non-proportional resize) - writes a new sibling file and updates the article's own image reference to it, leaving the original untouched. - Einstellungen dialog (
Adw.PreferencesDialog, Ctrl+,) — an "Erscheinungsbild" page adopted directly from GNOME Builder's own implementation (light/dark/follow-system cards using Builder's bundled preview illustrations, and an editor color-scheme grid using GtkSourceView'sStyleSchemePreviewwidget filtered to schemes matching the current light/dark mode, the same widget and filtering Builder uses, plus the article preview's own typographic style picker). The grid shows GtkSourceView's own real, canonical bundled schemes (six per mode:Adwaita/-dark,classic/-dark,cobalt/-light,kate/-dark,oblivion,solarized-light/-dark,tango) rather than the app shipping its own copies - the same set GNOME Builder and GNOME Text Editor themselves offer. Whichever scheme is picked also colors the live Markdown preview's code blocks to match, not just the editor. Independent font pickers for the editor and the preview (family/size/weight/style, each with a live sample and a reset-to-default button), a WordPress-connection page (site URL/username in a small config file, the Application Password in the Secret Service viaoo7, never written to disk in plain text), a "KI-Chat" page (a provider picker for Gemini/ChatGPT/Claude/Groq/Ollama, each with its own API key - verified live against the provider's API as soon as it's entered, and saved automatically once that check succeeds, with no separate save button - and a model picker populated from that account's actual available models, Ollama additionally getting a configurable base URL; plus a fully editable, resettable system prompt shared across all providers), a "KI-Modelle" page (see "Per-task AI models" below), and a "KI-Prompts" page (the context menu's five built-in prompts and custom prompts - see above). - Per-task AI models with a capability check — Einstellungen →
"KI-Modelle" checks, per provider, which of the models the key lists
it can actually use: each model gets a tiny dry-run request, and the
outcome is classified (usable, quota temporarily exhausted, paid
plan/credit required - including Gemini's free tier
limit: 0models -, no access, blocked in the region, retired, invalid key) and cached per key fingerprint with a status-dependent lifetime (src/modelcheck.rs, the error matrix isllm::classify). Image descriptions (alt text and captions), editing (in-place rewrites, article evaluation, tag suggestions) and text generation (the AI article draft) can each get their own primary and fallback model; unassigned tasks keep following the KI-Chat model. A task skips a model known to be blocked, and falls back automatically when a real call fails for a model/account reason, with a toast saying so (src/aitasks.rs). The chat pane itself keeps using the KI-Chat model. - Publishing — uploads create/update the WordPress post via its REST
API on a background thread, always sending the status the chosen action
stands for, and always updating the same tracked post. A successful
upload writes the post id, each image's upload reference and the sync
baseline back into the working copy right away. Scheduling needs a
valid date; WordPress itself would otherwise publish immediately. Draft
previews open in the app's web view, which needs a wp-admin login once:
its cookies are kept on disk (
blocksatz/webkit/cookies.sqliteunder the user's data directory). Re-uploading a post first re-fetches its server content and compares it against the locally remembered baseline; if it changed on WordPress since, a confirmation asks before overwriting. Skipped (fails open) if the check itself can't complete, so a network hiccup never blocks publishing outright. - Broken-link checker — the "Links" page of the release check scans
the article for every unique
http(s)://URL (Markdown link/image destinations, plus a bare URL alone on its own line that exports as awp:embedblock) and, on "Links prüfen", HEADs each one on a background thread (falling back to GET if a server rejects HEAD), flagging anything outside the 2xx/3xx range or timing out. Category/tag names are resolved to WordPress term ids (creating them if they don't exist yet). Locally-referenced images are uploaded to the media library "bei Bedarf" (as needed), sharing the same tracked media list Medienverwaltung uses: an image whose content hash still matches what's already on the server is reused rather than re-uploaded, and a changed one is uploaded as a new attachment with the superseded one cleaned up automatically, since WordPress can't replace an existing attachment's file in place. Every PNG/JPEG is uploaded as WebP (transparency kept), downscaled to at most 2000px on its longer edge; a 3.9 MB PNG screenshot ends up around 110 KB. Media uploads get a timeout that grows with the file size, so a slow uplink doesn't cut them off. Only what's sent is ever affected, never the local file. Posts are only ever moved to WordPress's (recoverable) trash, never deleted permanently. - Primary menu (in the sidebar's header bar) — "WordPress-Mediathek",
"Galerie einfügen…", "Einstellungen", "Tastenkürzel" (an
AdwShortcutsDialog, also reachable via Ctrl+?), and "Über Blocksatz", the latter a nativeAdw.AboutDialogwith the version (always in sync withCargo.toml), GPL-3.0-or-later license text, issue tracker/repository links, and the fullCHANGELOG.mdhistory as its browsable "Neuigkeiten" release notes. - Internationalization — translatable via GNU gettext (
gettext-rs). Source strings are German (the app's original language); essentially the whole UI is wrapped for translation, andpo/en.pois a complete, real English translation (~270 strings) proving the pipeline works end to end (build.rscompiles everypo/*.pointo a.mocatalog on every build, picked up automatically by acargo runfrom this source tree). AI prompt content and proper nouns (WordPress, provider names) deliberately stay untranslated by design - seepo/README.mdfor the full translator/contributor workflow. - Flatpak packaging — manifest and build script under
build-aux/flatpak/, desktop entry, AppStream metainfo and icons underdata/. - GNOME desktop integration — the
.desktopfile declaresMimeType=text/markdown;and the app handles being launched with a file argument, so double-clicking a.mdfile (or "Open With" → Blocksatz) in GNOME Files opens it directly, loading into the already-running window rather than a second one if Blocksatz is already open. Opening or saving a file also registers it withGtk.RecentManager, GNOME's shared recent-files list. Publishing, an image upload, or a link check finishing while the window isn't focused raises a desktop notification.
Requires a Rust toolchain (stable) and the GTK4/libadwaita/GtkSourceView5/
WebKitGTK 6.0/libspelling development packages (available on any recent
GNOME-based Linux distribution). Spell-checking needs at least one hunspell
dictionary installed for it to have anything to check against. GNU
gettext's msgfmt (for compiling po/*.po translations - see
po/README.md) is optional: build.rs only prints a build warning and
skips it if not found, and the app runs fine without it, just always
showing its original German source strings.
cargo build
cargo runcargo test --workspaceA few tests exercise real system services (e.g. the Secret Service via
oo7) rather than mocks, and are marked #[ignore] so a normal test run
doesn't depend on your desktop's state. Run those explicitly with:
cargo test --workspace -- --ignoredBlocksatz is packaged and installed as a Flatpak. The manifest at
build-aux/flatpak/de.linuxundich.Blocksatz.json targets
org.gnome.Platform 51, which already bundles GTK4, libadwaita,
GtkSourceView5 and WebKitGTK 6.0 - only libspelling is built as an extra
module, plus the org.freedesktop.Sdk.Extension.rust-stable SDK extension
for the Rust toolchain itself.
One script builds the current checkout and installs it for the current user (it also installs the runtime/SDK/extension if they're missing):
build-aux/flatpak/build.sh # build + install
build-aux/flatpak/build.sh --run # ... and launch it afterwards
build-aux/flatpak/build.sh --bundle # ... and also write blocksatz.flatpakThe sandboxed build runs fully offline: the script first vendors every
crate from Cargo.lock into build-aux/flatpak/.cache/vendor with
cargo vendor, so a dependency change needs no separate manual step.
Compiled translations are installed to /app/share/locale (see
po/README.md). The app icon (data/icons/hicolor/) is generated by
build-aux/icons/generate_icons.py: a scalable SVG and a symbolic SVG, plus
PNGs at 48/64/128/256 px rendered from the SVG. The PNGs matter on systems
whose gdk-pixbuf has no SVG loader any more (librsvg 2.62 dropped it) -
GNOME Shell would otherwise show a blank tile. build-aux/icons/make_preview.sh
renders docs/icon-preview.png; docs/icon.md explains the design.
The sandbox sees the documents folder (--filesystem=xdg-documents,
where the library ~/Dokumente/Blocksatz lives) and the templates folder
(xdg-templates: on the first launch Blocksatz puts a
"Blocksatz-Artikel.md" there, so Nautilus offers it under "Neues
Dokument") and nothing else of the home directory: other files arrive through the file chooser and drag and
drop portals, and an image or video picked from elsewhere is copied into
the article's folder, so an article stays self-contained.
For Flathub, build-aux/flathub/prepare.sh <tag> writes the submission
manifest (building from the Git tag) and cargo-sources.json (every
crate from Cargo.lock, generated by build-aux/flathub/cargo-sources.py)
into build-aux/flathub/out/; --local builds from this checkout's HEAD
for a test. docs/flathub-verification.md covers the submission and the
domain verification.
Secrets (WordPress application password, AI API keys) go through oo7,
which inside the sandbox uses the Secret portal's own per-app keyring
rather than the host's GNOME Keyring - so they have to be entered once
again after switching from a non-Flatpak build.
Blocksatz follows Semantic Versioning. The version
in Cargo.toml is the source of truth; see CHANGELOG.md for
what changed in each release. Before 1.0.0, minor version bumps (0.x.0)
may still change the on-disk frontmatter format or other user-facing
behavior — check the changelog when upgrading.
GPL-3.0-or-later. See LICENSE.