A markdown-native sticky-notes app for Linux, built with Tauri + React + TipTap.
Multiple independent, always-visible note windows (like Apple's Stickies), but
every note is markdown, and typing / on a new line opens a command menu
(headings, lists, to-dos, quotes, code blocks, dividers, Notion/Confluence style).
- Multiple independent, always-visible note windows, each its own small draggable/resizable window (like Apple's Stickies).
- Every note is markdown under the hood: standard syntax converts as you
type, or press
/for a command menu (headings, lists, to-dos, quotes, code blocks, dividers). - Selecting text pops up a formatting toolbar: bold, italic, strikethrough, code, plus quick toggles for heading/bullet list/to-do list.
- Paste an image (e.g. a screenshot) straight into a note.
- 6 note colors, and a pin toggle to keep a note always-on-top.
- Ctrl+Alt+N creates a new note from anywhere, even when Jotter isn't focused (see "Global shortcut" below).
Built with Tauri (Rust backend, one native window per
note) + React + TipTap (the
editor), with note content stored as plain markdown via tiptap-markdown.
Grab the latest build from the Releases page. Three formats are published on every release:
- AppImage — works on any distro, including immutable ones like Bazzite:
no install, no root, no
rpm-ostreelayering.chmod +x Jotter_*.AppImage ./Jotter_*.AppImage - .deb — for Debian/Ubuntu-based distros:
sudo apt install ./jotter_*.deb - .rpm — for traditional (non-atomic) Fedora-based distros:
sudo dnf install ./jotter-*.rpm. On Bazzite/other rpm-ostree hosts, prefer the AppImage instead ofrpm-ostree install, which layers the package onto the base image and requires a reboot.
git clone https://github.com/danieltucker/jotter.git
cd jotter
npm install
npm run tauri build
This needs Rust, Node, and Tauri's Linux prerequisites (GTK/WebKit dev
headers) installed; see Tauri's Linux setup
guide if npm run tauri build
fails on missing system libraries. The built binary ends up at
src-tauri/target/release/jotter, runnable directly, no install step needed.
For the day-to-day dev workflow on this project's own machine (a Bazzite host, distrobox toolchain, exported launcher), see "Development" below.
Notes are stored as JSON (id, color, position, size, markdown content) in:
~/.local/share/com.danielgt.jotter/notes/
This path is derived from identifier in src-tauri/tauri.conf.json. If you
ever change it, the app looks in a new, empty directory: copy
~/.local/share/<old identifier>/notes/ into
~/.local/share/<new identifier>/notes/ first, or existing notes won't show
up (they aren't lost, just invisible until the directory is migrated).
Closing a note's window deletes that note (same destructive action as the trash icon in the note's hover toolbar, with the same confirmation dialog). The app quits normally once the last note window closes; there's no tray icon or background-running mode.
The build toolchain (Rust, Node, GTK/WebKit dev headers) lives in a Fedora 44
distrobox container rather than on the immutable Bazzite host; this avoids
rpm-ostree layering. The project source lives on the host and is
bind-mounted into the container automatically. The container is still named
stickaroos-dev (a leftover from before the app was renamed); it's just a
build environment, not part of the shipped app, so it was left alone rather
than risk breaking the container to chase a cosmetic match.
Once a release binary has been exported (see distrobox-export --bin
below), it also shows up in the GNOME app grid as Jotter, and can be run
directly from anywhere with just jotter. The launcher forces
GDK_BACKEND=x11; see "Wayland caveat" below for why.
distrobox enter stickaroos-dev
npm run tauri dev
To rebuild the release binary and re-export the launcher after backend changes:
distrobox enter stickaroos-dev
npm run tauri build
distrobox-export --bin "$(pwd)/src-tauri/target/release/jotter" --export-path ~/.local/bin
Use npm run tauri build (or cargo tauri build), not a plain cargo build --release; the Tauri CLI enables the custom-protocol cargo feature that
makes the binary load its bundled frontend instead of trying to reach the
Vite dev server at localhost:1420. A plain cargo build --release produces
a binary that fails with "connection refused" the moment the dev server isn't
running.
(distrobox-export --bin must be run from inside the container.)
The first distrobox-export --bin for a given binary generates
~/.local/share/applications/com.danielgt.jotter.desktop without a
StartupWMClass line. Without it, GNOME can't match the running window back
to the desktop entry, so the taskbar/dash/alt-tab icon falls back to a
generic one even though Icon=com.danielgt.jotter is correct. Add it once:
echo 'StartupWMClass=Jotter' >> ~/.local/share/applications/com.danielgt.jotter.desktop
(Verified: re-running distrobox-export --bin afterwards is idempotent and
does not strip this line; you don't need to re-add it after every rebuild,
only after a first-time export or if the .desktop file is ever deleted.)
GNOME's native Wayland session does not let client apps set or persist an absolute window position; that's a deliberate Wayland restriction, not a bug here. Under native Wayland every note would ignore its saved x/y and get placed by the compositor instead, breaking both new-note cascading and position-memory across restarts.
Running under XWayland (GDK_BACKEND=x11, which the exported launcher already
sets) restores normal positioning, since X11's window model allows it. This is
why the desktop entry and ~/.local/bin/jotter both force it. Dragging,
resizing, and everything else works the same either way.
Ctrl+Alt+N creates a new note from anywhere.
It's registered in src-tauri/src/lib.rs (new_note_shortcut()); if it
conflicts with an existing GNOME/IBus binding on your system, change the
Modifiers/Code there and rebuild.
-
Note colors: yellow, pink, blue, green, purple, gray (
src/colors.ts). -
Pin/always-on-top toggle per note.
-
Drag by the header strip to move. Resizing from the border uses only Tauri's built-in undecorated-window border grab (a 5px inset on Linux, hardcoded, not configurable, see
undecorated_resizing.rsintauri-runtime-wry); no custom resize-handle component. Two different attempts at a customResizeHandles.tsxlayered on top of that built-in grab (to make the ~5px margin easier to hit) both reintroduced the same family of bug: the window visibly shrinking/growing erratically mid-drag. The first attempt overlapped the built-in 0-5px zone outright; the second offset the custom handles to start exactly at 5px to avoid that overlap, and the erratic resizing still came back (this time reported as the top jittering while dragging the bottom edge), so whatever the real conflict is, it isn't just pixel overlap with the documented 5px inset, and it wasn't fully understood before being reverted. If you want to revisit widening the grab margin, that's the trap to debug first, ideally with a way to test a real mouse drag, since neither Wayland's blocking of synthetic pointer input nor this project's screenshot-based verification loop caught either regression before a human did.ResizeCursorHints.tsxis a separate, safe addition on top of that native grab: thin overlays at the same 5px edges/corners that only set the CSScursor(ns-resize/ew-resize/nwse-resize/nesw-resize) on hover, with no mouse handlers of their own. The native GTK handler that actually starts the resize-drag fires at the window-widget level regardless of what's in the DOM, so this can't reintroduce the drag-conflict bug above; it only fixes the resize border showing a text-select cursor instead of a resize one. -
Minimize iconifies via the WM. The close (×) button is deliberately the same destructive action as "Delete note" in the hamburger menu, same confirmation dialog, same
delete_notecommand. There is no separate "just hide the window" close anymore; there was aclose_note_windowcommand for that, but it's gone, and don't bring it back without also bringing back a non-destructive way to close from the header, since the × icon alone no longer means "just hide." Every saved note still reopens automatically on the next full app launch regardless of what was open at exit (app::load_startup_notesreopens everything in the notes directory). "Show all notes" in the hamburger menu (show_all_notescommand) reopens any note that's merely minimized, without a full app restart. -
Note windows are created with
.visible(false)and shown by the frontend (App.tsx, oncenotestate settles either way) rather than at creation, so there's no flash of Tauri's default black background while the webview loads. An earlier attempt fixed the flash by settingbackground_colorto the note's own color while keepingtransparent(true); that broke transparency instead: the rounded corners rendered opaque white rather than see-through. Don't combine those two again; if you touch this, the hidden-until-ready pattern is the one that's actually been shown to keep transparency intact. -
Slash menu items: Text, Heading 1-3, Bullet/Numbered/To-do lists, Quote, Code Block, Divider (
src/extensions/slashCommandItems.ts); add more there. -
Markdown is stored directly (via
tiptap-markdown), so note files are plain, greppable markdown wrapped in a small JSON envelope. Links (@tiptap/extension-link) render but don't open on a plain click (openOnClick: falseso you can click into link text to edit it), only on ctrl/cmd+click, handled manually inEditor.tsxsince the extension has no built-in modifier-key fallback. -
"About" in the hamburger menu opens a new sticky note (
note::ABOUT_CONTENT,create_about_notecommand) with author links, rather than opening a browser directly, consistent with the app's own note-taking medium. Opening links from within a note goes through@tauri-apps/plugin-opener'sopenUrl, permitted by theopener:defaultcapability insrc-tauri/capabilities/default.json. Building this surfaced a sharp debugging trap created by the.visible(false)pattern above: a binary built with a plaincargo build --release(skipping the Tauri CLI, so nocustom-protocolfeature, see "Development" above) tries to load its UI from thelocalhost:1420dev server instead of the bundled frontend. Before.visible(false), that failure was at least visible: a blank or connection-refused window. Now the window is created invisible and only shown once the frontend mounts and calls.show(); since acustom-protocol-less build never gets that far, the window silently never appears at all: process alive, 0% CPU, one 10×10 unmapped X window, looks exactly like a genuine hang. It happened twice in a row while building the About note feature, and was mistaken for a real startup-race bug both times before the actual cause (the wrong build command) was found. If a note window ever seems to hang on creation, check the build command before anything else.
Source icons live in icons/ (see icons/README.md for the palette and
design notes). src-tauri/icons/ is generated from
icons/io.github.you.Jotter.svg via npx tauri icon <path>; rerun that
after editing the source SVG, rather than hand-editing the generated PNGs.
MIT. See LICENSE.



