Mind map tool write in Rust
  • Rust 98.8%
  • Shell 0.7%
  • NSIS 0.4%
  • Nix 0.1%
Find a file
Jonatas Oliveira 4c4b6972f8
All checks were successful
ci/woodpecker/push/test/1 Pipeline was successful
ci/woodpecker/push/test/2 Pipeline was successful
build: move mutation testing out of CI and into a pre-commit hook
The step swept the whole allow-list on every push: ~2 800 mutants, an hour
of pipeline, all to say something about the dozen lines the commit touched.
`.githooks/pre-commit` runs `cargo mutants --in-diff` over the staged diff
instead, which is four mutants and half a minute for an ordinary change, and
nothing at all — one second, no build — for a commit that touches no Rust.

Install it once per clone with `git config core.hooksPath .githooks`.
`git commit --no-verify` skips it once, `RUSTMIND_SKIP_MUTANTS=1` for as long
as it is set, and `bin/verify.sh --full` still runs the whole sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tSQgSyek9Lnkis3Znf8aZ
2026-09-04 14:28:43 +02:00
.agents build: move mutation testing out of CI and into a pre-commit hook 2026-09-04 14:28:43 +02:00
.cargo build: move mutation testing out of CI and into a pre-commit hook 2026-09-04 14:28:43 +02:00
.githooks build: move mutation testing out of CI and into a pre-commit hook 2026-09-04 14:28:43 +02:00
.woodpecker build: move mutation testing out of CI and into a pre-commit hook 2026-09-04 14:28:43 +02:00
apps/rustmind-desktop fix(app): a menu is opaque on a dark theme too 2026-09-01 11:57:41 +02:00
bin build: move mutation testing out of CI and into a pre-commit hook 2026-09-04 14:28:43 +02:00
crates fix(mutants): a pattern that named the wrong crate, and a field it cannot skip 2026-09-03 14:30:52 +02:00
docs feat: themes that live in a file, and levels you can style whole 2026-08-25 08:24:20 +02:00
packaging fix(docs): point at the repository that actually exists 2026-08-28 17:42:29 +02:00
.gitignore build(windows): cross-compile the app, and hand out a setup 2026-08-28 09:52:54 +02:00
Cargo.lock feat(browser): change workspace, rename what is in it, and show an import 2026-09-01 06:13:22 +02:00
Cargo.toml build: raise the MSRV to 1.98, and take the lints that came with it 2026-08-28 18:00:00 +02:00
CHANGELOG.md docs(release): regenerate the changelog for 0.0.1 2026-08-28 09:56:58 +02:00
cliff.toml docs(release): a changelog generated from the history 2026-08-28 09:15:47 +02:00
clippy.toml chore(workspace): initialize Cargo workspace with rustmind-core and tooling config 2026-08-10 18:33:42 +02:00
CONTRIBUTING.de.md build: move mutation testing out of CI and into a pre-commit hook 2026-09-04 14:28:43 +02:00
CONTRIBUTING.md build: move mutation testing out of CI and into a pre-commit hook 2026-09-04 14:28:43 +02:00
CONTRIBUTING.pt-BR.md build: move mutation testing out of CI and into a pre-commit hook 2026-09-04 14:28:43 +02:00
flake.nix build: package for deb, rpm, AppImage, Flatpak and Nix 2026-08-27 18:33:03 +02:00
LICENSE docs: create README, CONTRIBUTING, LICENSE, and 6 initial ADRs 2026-08-10 18:33:42 +02:00
NOTICE feat(icon): an RM monogram, as SVG and as .ico 2026-08-28 09:52:43 +02:00
README.md build: raise the MSRV to 1.98, and take the lints that came with it 2026-08-28 18:00:00 +02:00
rust-toolchain.toml ci: matrix on 1.98 and latest, and guardrails that a test enforces 2026-08-25 08:39:22 +02:00
rustfmt.toml chore(workspace): initialize Cargo workspace with rustmind-core and tooling config 2026-08-10 18:33:42 +02:00

RustMind

A Rust mind-map engine that uses Markdown as the single source of truth.

                    [RustMind]
                        |
        +---------------+---------------+
        |               |               |
   [Parser]        [Layout]       [Render]
   Markdown         engines          SVG
        |               |               |
        +---------------+---------------+
                        |
                  [Document Model]

RustMind lets you author, version-control, and render mind maps from plain Markdown files. The same file can be edited in your favorite text editor, validated and exported from the command line, or opened in a visual desktop application.

Features

  • Markdown-first: mind maps are stored as extended Markdown with YAML frontmatter, headings, nested lists, IDs, tags, relations, and position/style overrides.
  • Deterministic SVG rendering: the same document always produces the same SVG output.
  • Multiple layouts: freeform (manual positions), tree (top-down, bottom-up, left-right, right-left), and radial (clockwise and counter-clockwise).
  • Complete CLI: new, validate, render, layout, format, export.
  • Visual editor: Iced-based desktop app with canvas, zoom, pan, node selection, drag-to-move, and undo/redo.
  • Undo/redo: all document mutations in the desktop app go through a command history.
  • Clean architecture: domain crates have no GUI dependencies; screen pixels are only used at rendering and input boundaries.

Installation

You need a recent Rust toolchain (MSRV 1.98).

git clone https://forgejo.singularjourney.org/devjonatas/rustmind.git
cd rustmind

# Build everything
cargo build --workspace

# Build only the CLI
cargo build -p rustmind-cli

# Build only the desktop app
cargo build -p rustmind-desktop

The CLI binary is named rustmind and is produced by the rustmind-cli crate. The desktop binary is named rustmind-desktop and is produced by apps/rustmind-desktop.

Quick start

# Create a new mind map
cargo run -p rustmind-cli -- new my-map.md

# Edit the file with your favorite editor, then validate it
cargo run -p rustmind-cli -- validate my-map.md

# Apply a radial layout and write positions back to the file
cargo run -p rustmind-cli -- layout my-map.md --type radial

# Render to SVG (stdout)
cargo run -p rustmind-cli -- render my-map.md --format svg > my-map.svg

# Or export directly to a file with the same base name
cargo run -p rustmind-cli -- export my-map.md

# Canonicalize / reformat the Markdown
cargo run -p rustmind-cli -- format my-map.md

# Run the desktop app
cargo run -p rustmind-desktop

CLI reference

Command Description
rustmind new <file> Create a new mind map with default frontmatter and a root node.
rustmind validate <file> Parse the file and report errors.
rustmind render <file> [--format svg|png|pdf|opml|terminal] [--output <file>] Render the document. Defaults to SVG on stdout. Terminal output is a stub for future work.
rustmind layout <file> [--type radial|tree|freeform] [--direction <dir>] Apply a layout and write positions back to the file. Preserves manual positions.
rustmind format <file> Parse and serialize the file back to canonical Markdown.
rustmind import xmind <file> [--output <file>] [--arrange] Convert an XMind workbook (.xmind, new JSON or old XML) to a RustMind map. What could not be converted is reported on stderr.
rustmind export <file> [--format svg|png|pdf|opml] Export next to the source file, e.g. <file>.pdf.

Direction values depend on the layout:

  • radial: clockwise, counter-clockwise
  • tree: top-down, bottom-up, left-right, right-left
  • freeform: ignored

Markdown format

A RustMind document is a Markdown file with optional YAML frontmatter.

---
layout: mindmap
direction: clockwise
version: 1
---

# Root {#root}

## Child A {#child-a}

### Grandchild {#grandchild}

## Child B {#child-b}

@relation child-a -> child-b {type: dependency, label: "depends on"}
@position root {x: 0.0, y: 0.0, mode: manual}
@style root {fill: "#e3f2fd", shape: rounded_rect}

Frontmatter

Key Description
title Document title.
author Document author.
layout Default layout: mindmap, tree, or freeform.
direction Default direction for the layout.
root Optional explicit root node ID.
version Document format version.

Nodes

Headings and nested lists both create nodes. IDs are written with {#id}; tags with {tag1,tag2}. If no ID is provided, a slug is generated from the title.

Relations

Relations are declared on their own lines:

@relation source-id -> target-id
@relation source-id -> target-id {type: dependency, label: "label text"}

Supported relation types: relation, dependency, reference, causes, blocks, implements, extends.

Pictures and icons

A node can carry a Font Awesome icon and a picture:

@icon root star
@image root .rustmind/images/logo.png

Pictures are PNG, JPEG, GIF, WebP or SVG, stored relative to the workspace so they travel with the map. Import one from Inspector → Picture → Import…; the file is copied into .rustmind/images/.

Markers and labels

Markers state something about a node — priority, progress, task state or mood — and come from a fixed catalog, so a map means the same thing in every renderer. At most one marker per group sticks: setting a second priority replaces the first.

@marker root priority-high, task-doing
Group Ids
Priority priority-high, priority-medium, priority-low
Progress progress-start, progress-half, progress-done
Task task-todo, task-doing, task-done, task-blocked
Mood mood-idea, mood-question, mood-star, mood-heart

Labels are free text and ride in the heading itself:

## Backend {infra, q3} {#backend}

Both are edited under Inspector → Markers & labels.

A node can carry links: web addresses, and jumps to another topic written node:<id>. One per line, so a URL may contain commas:

@link backend https://example.com/spec
@link docs node:backend

They are edited under Inspector → Links. Clicking a topic link selects the target and centers the map on it; clicking a web address hands it to the desktop's browser — only http, https and mailto links are opened, since opening a link starts whatever program the desktop registered for its scheme. A node with a note or a link shows a small badge in its top-right corner.

Filtering

The toolbar filter (Ctrl + F) dims what does not match instead of hiding it, so the layout stays put while you type. A bare word looks in the title and the note, #word looks in the labels, and @word looks in the markers — a group name such as @priority catches every marker in the group. Terms combine:

roadmap #infra @priority

Position, size and style overrides

@position node-id {x: 120.0, y: 80.0, mode: manual}
@size node-id {width: 220.00, height: 90.00}
@style node-id {fill: "#ff0000", stroke: "#000000", shape: diamond}

@size is a floor, not a cap: a topic never shrinks below the text it holds, so a smaller number just lets the title decide again.

Branch style

@edge node-id {type: bezier, color: "#ff8800", width: 2, dash: dashed, taper: true}

@edge styles the line arriving at a topic, and everything below it wears the same line — set it on a top-level topic to restyle a whole branch. A topic deeper down can still say something different for its own subtree. dash is solid, dashed or dotted; branch_color: true makes the line wear the colour its branch's topics wear.

bend: 12 -30 pulls the middle of the line that far off the straight run between the two topics, which is what dragging the ring in the middle of a selected topic's line writes. It is an offset rather than a place, so the curve keeps its bulge when either topic is moved; Endireitar a linha in the inspector takes it back off.

taper: true draws the branch thick where it leaves the parent and thinner at every level below, until the far leaves hang from a hairline — the way a tree grows. A tapered branch is a filled shape rather than a stroke, so it carries no arrowhead and no dashes: the thickness says which way it flows. To taper the whole map, put taper: true in the frontmatter, or use Format → Taper every branch.

Drawing on the map

F8, or Caneta in the toolbar, turns the map into paper: dragging draws instead of moving topics. Pick an ink and a width beside the button, or reach for the eraser, which rubs out the stroke under the pointer. Each scribble is one entry on the undo stack.

@stroke pen-1 {color: "#e04a4a", width: 3, points: "10 20 14 26 21 30"}

A stroke belongs to the map rather than to a topic: it stays where it was put when a topic is dragged out from under it, which is what drawing on top of something means. Points are pairs, in document coordinates, kept to two decimals — finer than the screen shows.

Packaging

bin/package.sh target/release 0.0.1 writes into dist/: a tarball, a .deb, an .rpm, an AppImage and the checksums of all of them. Each package whose tool is missing on the machine is skipped with a line saying which — the tarball is always produced, and it is what somebody packaging RustMind themselves starts from.

Flatpak and Nix live beside the script, because their own tools drive the build:

flatpak-builder build/ packaging/com.rustmind.RustMind.yml
nix build          # or: nix run .#rustmind, nix develop

Reading other tools' files

rustmind import xmind workbook.xmind
rustmind import freemind plan.mm
rustmind import opml outline.opml
rustmind import docx report.docx
rustmind import mindmanager plan.mmap
rustmind import mindnode plan.mindnode
rustmind import textbundle map.textbundle

The same formats have buttons in the project browser. An import is never lossless — every tool has features the others do not — so anything that could not be carried over is reported rather than dropped in silence. A Word document is read by its heading levels: Heading 1 becomes a root, Heading 2 its child, and the paragraphs under a heading become that topic's note.

TextBundle

A TextBundle is a folder holding text.markdown, info.json and an assets/ directory — a map and the pictures it uses, in one thing to move. Arquivo → Exportar como TextBundle writes one, the browser's Importar TextBundle reads one back, and the CLI has both:

rustmind export map.md --format textbundle
rustmind export map.md --format docx
rustmind export map.md --format xlsx
rustmind import textbundle map.textbundle --output back.md

Pictures are copied in and the map's @image paths are pointed at the copies, so the folder stands on its own; on the way back they land in the workspace's .rustmind/images. A name already taken is numbered rather than overwritten.

Map shot

Recorte, beside the pen, puts a frame in your hand: drag a rectangle over the map and everything outside it dims. Exportar o recorte then writes what is inside as SVG, PNG or PDF.

The frame decides the picture, not the topics: a shot of a corner of the map is a shot of whatever happens to be there. A topic reaching over the edge is kept and clipped by the frame, the way a photograph clips; a line with one end outside is left out, since it would be drawn from nowhere.

Audio notes

The inspector's Recording row records a note for the selected topic: the same button starts and stops, counting up while the microphone is on. It lands in the workspace's .rustmind/audio/ as a 16-bit WAV and the topic gets a speaker badge, since a recording is otherwise invisible on a map.

@audio root .rustmind/audio/root.wav

WAV because the map has to outlive this app: a format every player already reads is worth more here than a smaller file. Taking a recording off a topic leaves the file alone — it is something somebody made.

Equations

The inspector's Equation button opens a box for LaTeX. What it draws is an SVG written into the workspace and set as the topic's picture, so a formula travels through every renderer the way any other picture does — and the source stays on the topic, because an SVG cannot be edited back into LaTeX.

\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}

It is not TeX. Variables, numbers, operators, fractions, roots, sub- and superscripts are what a formula on a topic needs; the rest of LaTeX is left to LaTeX. The parsing is pulldown-latex, the layout and the SVG are ours — the alternative was a TeX engine that has to be installed.

Illustrations

The inspector's Illustrations button opens the drawings that ship with the app — 151 hand-drawn doodles that pair with the sketch style. Choosing one copies it into the workspace's .rustmind/images/ and points the topic's @image at it, so the map keeps its picture with or without this app.

They are the Mega Doodles Pack by Maria Letta, under CC BY-SA 4.0. The credit is beside them in the picker and in NOTICE: a published map with one in it is handing the drawing on, and the licence asks for the name to travel with it.

The picker windows

The icon and illustration pickers are windows, not rectangles drawn over the map: a tiling compositor can float, move and tile a window, and can do none of those to a drawing. They carry the application id rustmind-picker, which is what a rule is written against:

# Hyprland
windowrulev2 = float, class:^(rustmind-picker)$

Imported textures

Five fill textures are drawn in code — dots, grid, diagonal, cross. A sixth kind comes from a file: the button beside the texture picker imports a GIMP .pat, which lands in the workspace's .rustmind/textures/ and joins the list. It is written the same way in the file:

@style root {texture: bricks}

A name that is not one of the five is an imported pattern. A map naming one this machine has not imported still draws — the topic loses its texture, the way a map naming a missing font still draws — because the map is right and the machine is the one missing a file.

Stickers

Adesivo, beside the pen, opens the icon picker and sticks the chosen icon on the map — loose from any topic, in the middle of what is on screen. Drag it to move it, +/ to size it, Del to peel it off.

@sticker sticker-1 {icon: star, x: 120, y: -40, size: 48, color: "#e0a52f"}

The x/y are the middle of it, because a sticker grows and shrinks about where it was put rather than walking off from its top-left corner.

Boundaries, areas and summaries

Three ways to group topics without changing the tree.

An @boundary draws an outline around a set of topics — either a whole branch or a hand-picked list:

@boundary discovery {subtree: research, title: "Discovery", fill: "#89b4fa22", stroke: "#89b4fa"}
@boundary delivery {nodes: build prototype tests, title: "Delivery", shape: rounded, dash: dotted}
Field Values Default
subtree a topic id: it and everything below it
nodes topic ids, space-separated
title label drawn at the top-left none
kind boundary, zone boundary
shape rounded, rect, ellipse rounded
fill hex, alpha allowed (#89b4fa22) hollow
stroke hex theme edge
width outline width 1.5
dash solid, dashed, dotted dashed
corner_radius rounding for rounded 14
padding gap between the topics and the outline 16
collapsed zones only: fold the area away false

Either subtree or nodes is required — an outline around nothing is a parse error rather than an empty box. A boundary that names a topic which is not there drops it and keeps the rest; one left with no members at all is dropped.

A zone (kind: zone) is the same outline with a fold: collapsed: true hides its topics and the lines that reached them, and leaves a title bar where the outline was.

A @summary braces a run of siblings and hangs one topic off the brace:

@summary s1 {parent: plan, from: 0, to: 2, topic: result, stroke: "#f9e2af", width: 2}

from and to are child indices, inclusive; a range written backwards reads the same, and one running past the last child stops there. The brace brackets the whole branches, not just the sibling boxes, and sits on the far side from the parent — so it never crosses the topics it is summarising. The summary topic is placed at the brace's tip and drops its parent line: the brace is its connection.

In the app: the Groups section of the inspector, and Format → Outline this branch / Make an area / Summarise from here.

Title alignment

@style node-id {text_align: right, text_valign: bottom}

text_align is left, center (the default) or right; text_valign is top, middle (the default) or bottom. Both are in the desktop app under Format in the menu bar and in the inspector's typography section. Vertical alignment shows on a topic whose box is taller than its title — one given a @size, or one carrying a picture, markers or labels.

Desktop app

The first time you launch the desktop app it asks for a workspace folder. That folder becomes the root for all your RustMind projects. Each project is a sub-folder and can contain any number of .md map files.

Configuration is stored in ~/.config/rustmind/config.toml:

workspace_path = "/home/you/RustMindWorkspace"
first_run = false

Fonts and icon fonts imported from Settings → Fonts & icons are copied into <workspace>/.rustmind/, so they travel with the maps that use them. See docs/FONTS.md.

UI themes

Settings → UI theme dresses the app in any of the built-in palettes — Dracula, the four Catppuccins, Gruvbox, Tokyo Night, Nord, One Dark, Solarized, Everforest, Kanagawa, Rosé Pine, Nightfox, Monokai — and gives the open map the matching map theme at the same time, so the chrome and the map agree.

Beside the picker, Import UI theme… reads a palette from a TOML file and Export UI theme… writes the active one back out:

name = "Midnight Harbor"
dark = true
background = "#101820"
surface = "#1b2733"
text = "#e6edf3"
muted = "#8b9aa8"
primary = "#4fa3d1"
success = "#63c187"
warning = "#e0b352"
danger = "#e06c75"
colors = ["#4fa3d1", "#63c187", "#e0b352", "#e06c75", "#a78bfa"]

Only name is required; anything left out takes a dark default. An imported theme is kept in ~/.config/rustmind/ui-themes/ and reloaded at start-up, and from then on it sits beside the built-ins in the picker, in the palette gallery, and as a map theme. See docs/THEMES.md.

Quick entry

Ctrl + E, or the Quick entry button in the browser, opens a box for the moment before a map exists: type what you are thinking and watch the map build itself next to the text.

Ctrl + Alt + E does the same from any window — it brings RustMind forward and opens the box, so a thought that arrives while you are somewhere else has somewhere to go. Whether the shortcut can be taken at all is the desktop's call: X11, Windows and macOS hand it over, Wayland does not, and when it refuses the settings say so rather than leaving a key that does nothing.

Projeto
  Backend
    API
  Frontend

Indenting makes a child of the line above; bullets (-, *, +, ) and list numbers are optional and stripped; blank lines only separate. When the text leaves several top-level lines, the name typed for the map holds them — or, with no name, the first line does. Pick the layout and theme next to the text, then Create map writes it into the selected project and opens it.

Importing XMind

rustmind import xmind plano.xmind          # writes plano.md, laid out
rustmind import xmind plano.xmind -o m.md  # somewhere else

Both workbook formats are read: content.json from XMind 8 and later, and content.xml from XMind 2012 and earlier. Topics, notes, labels, markers, hyperlinks (links to another topic included) and relationships come through; the structure class picks the layout.

An import is never lossless, so nothing is dropped in silence — boundaries, summaries, sheets past the first and markers with no equivalent here are each reported on stderr:

warning: the sheet "Rascunho" was not imported; a RustMind map holds one sheet
warning: the marker "people-red" has no equivalent here (1 topic)

Exporting

File → Export writes the map as SVG, PNG, PDF or OPML, chosen by the extension you give the file.

SVG, PNG and PDF are the map as drawn, pictures included — a focused branch exports focused, because that is what is on screen. The PDF goes through the same SVG renderer as the others and keeps text as text and shapes as vectors, so it is not a picture of a PNG.

OPML is the document instead of the drawing: every topic, whatever is focused or collapsed, since an outline has no geometry. Titles, notes, labels, markers and links travel; anything RustMind-specific rides in an attribute prefixed with _, which readers that do not know it ignore.

Starting maps

The browser's Start from: row creates a map already laid out for a job — SWOT, project, meeting, study — in the interface language, because a map that starts in the wrong language is a map someone has to retype. A skeleton is just an outline in the same indented form quick entry reads, so adding one is writing a few lines of text.

The Theme menu lists every theme the map knows — the built-in palettes included — with a check on the one in use, so a whole new colour scheme is two clicks away from the map itself. Settings → Map theme shows the same palettes as cards of swatches, for picking by colour rather than by name. Either way, shapes, spacing and fonts stay where they were.

Hand-drawn style

The toolbar's Sketch button (or View → Hand-drawn style) redraws the map as if by hand: wobbling outlines, drawn twice the way a pen goes back over a line it just made. It is a property of the map, not of its theme — the same colours can be wanted clean or sketched — and it saves as sketch: true in the frontmatter.

The wobble is generated once in rustmind-scene and read by every renderer, so the screen, the SVG and the PNG draw the same line, and it is seeded from each node's id, so a map looks the same on every open and every export.

The text is written by hand too, in Caveat (SIL Open Font License, embedded), a topic that asked for a face of its own keeping it. A hand-drawn box with typeset text in it looks like a drawing somebody gave up on halfway.

Presentation

F5 turns the map into slides: the whole map first, then one slide per branch of the root, in the order the root's children are in — so rehearsing a talk is rearranging the map. The camera eases from one slide to the next rather than cutting, and the slide's topic is selected as it arrives, which is what points at what is being talked about.

Arrows, Space, PageUp/PageDown, Home and End move; Esc leaves and puts the camera back where it was. While presenting, the keyboard drives the deck and nothing else — a stray Tab in front of an audience must not add a node.

Zen mode

F10 clears everything but the map: no menu bar, no toolbar, no panels, and fullscreen. Esc (or the ✕ in the corner) leaves and puts the panels back the way they were found, not the way the defaults describe them.

The one strip that stays carries a night toggle, which darkens the canvas background without touching the map's theme — it dims the room, not the drawing — and a clock: a stopwatch, or a 25- or 5-minute countdown. The clock only ticks while it runs, so an idle app stays idle.

Outline view

Ctrl + Shift + O swaps the canvas for the same topics as an indented list, and back. The selection is shared, so switching keeps you where you were, and a collapsed node hides its subtree in both views. With a branch focused, the outline lists that branch alone.

In the outline, Tab and Shift + Tab indent and outdent (indenting makes the node a child of the sibling above it), the arrow keys walk the list, and Alt + ↑ / Alt + ↓ reorder — the same commands the map uses, so undo works across both views.

Controls

Action Control
Pan Left-click and drag on empty canvas.
Zoom Mouse wheel (zooms around cursor) or Ctrl + / Ctrl -.
Select node Left-click on a node, or click an entry in the navigator sidebar.
Drag node Left-click and drag a selected node.
Add child / sibling Toolbar buttons, Tab (child), Enter (sibling), or context menu.
Duplicate node Context menu.
Edit node Toolbar Edit button, F2, or context menu.
Delete node Toolbar Delete button, Delete, or context menu.
Collapse / expand Toolbar Collapse button or context menu.
Cycle shape Toolbar Shape button or context menu.
Lock / unlock position Context menu.
Reset position Context menu.
Apply layout Toolbar Radial / Tree buttons.
Add relation Inspector panel.
Filter nodes Toolbar filter field or Ctrl + F.
Focus a branch Toolbar Focus button or Ctrl + B (press again to leave).
Quick entry Ctrl + E, or the Quick entry button in the browser.
Map / outline Toolbar toggle or Ctrl + Shift + O.
Zen mode Toolbar Zen button or F10; Esc leaves.
Presentation Toolbar Present button or F5; arrows move, Esc leaves.
Indent / outdent Tab / Shift + Tab, in the outline.
Undo / redo Toolbar buttons or Ctrl + Z / Ctrl + Y.
Fit scene Toolbar Fit button or context menu.
Reset camera Toolbar Reset button or context menu.
Context menu Right-click on the canvas or a node.
Save map Ctrl + S.

Architecture

The workspace is organized into focused crates:

Crate / App Responsibility
rustmind-core Document model, geometry, node styles, themes, commands, undo/redo primitives.
rustmind-parser Extended Markdown parser and deterministic serializer.
rustmind-scene Intermediate scene graph and text measurement heuristics.
rustmind-layout Layout engines: FreeformLayout, TreeLayout, RadialLayout.
rustmind-svg Deterministic SVG renderer.
rustmind-cli Command-line interface built with clap.
rustmind-iced Reusable Iced UI state, canvas program, and message loop.
apps/rustmind-desktop Desktop application entry point.

Domain crates never depend on Iced or any GUI library. The GUI crate depends on domain crates and only converts between screen and document space at the rendering and input boundaries.

Development workflow

# Format, check, test, and lint (run these before committing)
cargo fmt --all -- --check
cargo check --workspace
cargo test --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo doc --workspace

The Woodpecker CI pipeline runs the same steps.

Testing

  • Unit tests live next to the code they exercise.
  • Snapshot tests use insta for parser output, layouts, and SVG.
  • Property-based tests use proptest for geometry and serialization.
  • CLI integration tests live in crates/rustmind-cli/tests/cli.rs and exercise the binary via assert_cmd.

Documentation

Every public API item is documented with rustdoc comments. Build and open the workspace docs with:

cargo doc --workspace --open

Roadmap

This project is under active development. Near-term work includes:

  • Persistent desktop settings and recent files.
  • Terminal renderer stub replacement.
  • Import/export plugins (future, out of MVP scope).

See .agents/backlog/mvp.md and .omo/plans/rustmind-mvp.md for the current backlog and detailed plan.

Contributing

See CONTRIBUTING.md for guidelines.

License

Dual-licensed under MIT OR Apache-2.0.