flatpak-builder cannot run inside the job container. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Ink
A calm, offline Markdown notes app. Electron + SvelteKit (Svelte 5), plain .md files on disk, SQLite full-text search.
Run it
Requires Bun and Node 20.19+ (22 LTS recommended; tests run on Node's test runner).
bun install # also downloads Electron and rebuilds better-sqlite3 for it
bun run dev # Vite dev server + Electron; the page hot-reloads, Electron restarts when electron/ changes
Other scripts:
| Script | What it does |
|---|---|
bun run dev:web |
UI only, in a browser, with an in-memory mock instead of files |
bun run start |
Production build of the UI, then Electron loads it |
bun run dist |
Packaged app in dist/ (dmg / nsis / AppImage via electron-builder) |
bun run icons |
Re-render resources/icon.svg into every icon size (needs rsvg-convert and ImageMagick) |
bun run test |
Markdown round-trip, vault, front matter and search index tests |
If bun install fails while rebuilding better-sqlite3, there was no prebuilt binary for your Electron version and it tried to compile one. Install build tools (Xcode CLT on macOS, "Desktop development with C++" on Windows, build-essential + python3 on Linux) and run bunx electron-rebuild -f -w better-sqlite3.
How it fits together
renderer (SvelteKit SPA, sandboxed) preload main process (Node)
───────────────────────────────── ───────── ────────────────────
Editor (contenteditable blocks)
└─ onChange ─> store.edit()
└─ 600 ms ─> toMarkdown() ─> window.vault.write() ─> ipcMain ─> vault.cjs ─> ~/Notes/*.md
└─> db.cjs ─> .notes/index.sqlite (FTS5)
Palette ─> window.vault.search() ───────────────────────────> ipcMain ─> db.cjs
store <─ 'vault:changed' <───────────────────────────────── fs.watch (edits from other editors)
- Files are the source of truth.
Projects.mdis a page;Projects/Roadmap.mdis its child. A folder without a matching.mdstill shows as a page. Delete.notes/any time and the index rebuilds on launch. - Front matter holds
icon,cover,tags,created. Unknown keys are preserved, so Obsidian vaults are safe to open. - Renaming a page renames the file and its folder. Moving (drag it onto another page in the sidebar, or onto empty space for the top level) moves both, plus the images it shows. Deleting moves both to
.notes/trash/, where the Trash dialog (sidebar footer, orCtrl/⌘ K→ Open trash) can restore them; the toast after a delete also has Undo. Entries older than 30 days, or deleted from the Trash dialog, go to the OS trash. - Web links
[text](https://…)and barehttps://…URLs are clickable (Ctrl/⌘-click, or the bubble that shows while the caret is in one). Pasting a URL over selected text links it; a typed URL becomes a link at the next space. Onlyhttp(s)andmailtolinks open; others stay plain text. - Tables are GitHub-style Markdown tables, edited cell by cell (right-click a cell for rows, columns and alignment;
/tablemakes one). A table you don't touch is saved exactly as it was written; an edited one is written back with aligned columns. - Links are Obsidian-style
[[Page]],[[Folder/Page]]or[[Page|label]]. A bare title matches the page with that title (shortest path wins). Renaming or moving a page rewrites the links that point to it. Each page lists its backlinks at the bottom. - Canvas (like Obsidian's): an endless surface of cards, notes, pictures, web links and groups, joined by arrows. Make one with
Ctrl/⌘ N→ Canvas, "New canvas" in the sidebar's right-click menu, or the search palette. It is a.canvasfile in the JSON Canvas format Obsidian uses, written the way Obsidian writes it, so canvases open in both apps (unknown fields are kept; a file that can't be read is shown as such and never saved over).- Move around: scroll (Shift+scroll sideways), Space+drag or middle-drag;
Ctrl/⌘+scroll orCtrl/⌘ +/−zooms,Ctrl/⌘ 0is 100%,Shift+1shows everything. Each canvas remembers where you were looking. - Add: double-click empty space for a card (Markdown, shown rendered; double-click or Enter to edit, Escape or click away to finish; a new card left empty disappears), or use the bar at the bottom: card, a note from your notes (shown as a live preview;
[[links]]in cards and notes open), a picture (or drop pictures from the file manager, or paste one), a group (around the selection, if any). Pasting a web address makes a link card. - Arrange: drag to move (snaps to the grid; Alt for free), the corner to resize; dragging a group takes what's inside along. Drag over empty space to select with a box, Shift+click to add;
Ctrl/⌘ A, arrow keys nudge,Deleteremoves,Ctrl/⌘ Dduplicates,Ctrl/⌘ C/X/Vcopy, cut and paste (as canvas text, like Obsidian). The bar over a selection sets one of six colours, opens a note, edits an arrow's label, or deletes. - Arrows: drag one of the dots on a card's sides onto another card; let go on empty space to make a new card there. Double-click an arrow to label it.
- Undo/redo (
Ctrl/⌘ Z,Ctrl/⌘ Shift Z). Canvases count as links: the notes on them list the canvas under backlinks, they appear in the graph, their text is searchable, and renaming or moving a note (or renaming one of its headings) updates every canvas showing it.[[Name.canvas]]links to a canvas;[[Name]]never means one.
- Move around: scroll (Shift+scroll sideways), Space+drag or middle-drag;
- New page (
Ctrl/⌘ N, the + buttons) opens a picker: Blank page, or a template. Templates are Markdown files in<notes folder>/.notes/templates/(front matter such asiconandtagscarries over, and the file name becomes the page's title); each shows as a card with its icon and a preview. The first card is focused, soCtrl/⌘ NthenEntergives a blank page; arrow keys move, Open folder opens the templates folder (creating it), and the list refreshes when you come back to the window. - Right-click opens the app's own menu instead of the system one: spelling suggestions and "Add to dictionary" for a misspelled word; open / copy / edit / remove for a web link (open for a page link); a row of format buttons for selected text in a note; Cut, Copy, Paste and Select all; and "Search notes for …" for a short selection. It doesn't take focus, so the selection stays; arrow keys, Enter and Escape work. (Electron's main process reports what was clicked, since only it has the spelling suggestions, and does the clipboard and spelling actions;
TextMenu.sveltedraws the menu.) Pages in the sidebar and table cells keep their own menus. - Math and diagrams: a
$$ … $$block is a LaTeX formula (KaTeX) and a```mermaidblock is a Mermaid diagram (/math,/mermaid). Each shows the rendered result on top and its source underneath, and the result updates as you type; a mistake shows an error message instead. Diagrams follow the light/dark theme when they're drawn. Embeds show math but show a diagram's source. - Highlight:
==text==shows highlighted. Select text and use the toolbar's H button orCtrl/⌘ Shift Hto add or remove it. - Footnotes:
[^id]marks a reference and a[^id]: textline holds its text (indented lines continue it);^[text]is an inline note. References are numbered in reading order, hovering one shows its text, clicking it jumps to the text, and clicking a footnote's number jumps back./footnoteadds one with the next free number and takes you to its text; Enter at the end of a footnote text takes you back to the sentence, just after the reference (Shift+Enter for a new line inside the footnote). Footnote texts stay at the bottom: clicking below the page adds a line above them; typing[^id]or^[…]turns into a reference as you type, and[^id]:at the start of a line starts a footnote text. A reference with no text is shown grey (click it to write one); a text nothing points to keeps its id. HTML export numbers and links them. - Heading links:
[[Page#Heading]]opens the page and scrolls to that heading (matched ignoring case and spacing);[[#Heading]]jumps within the page you're on. They're shown asPage › Heading, and typing#inside[[suggests the page's headings. Renaming a page keeps the#Headingpart. Renaming a heading updates them too: once you stop typing for ~2 seconds (or at once when you leave the page or window), every[[Page#Heading]]link and![[Page#Heading]]embed in your notes, including those in board cards, and the[[#Heading]]links in the page itself, are re-pointed at the new name, keeping aliases, and a toast says how many. Only the first heading of a given name counts (links point at it), a change of case alone leaves links alone, a brand-new or emptied heading isn't a rename, and characters that would break a link (# | ^ [ ]) become spaces in the link. Ctrl+Z on the heading takes the links back. - Embeds:
![[Page]]on a line of its own shows that note's content inline, read-only, under its title (click the title to open it);![[Page#Heading]]shows just that section, down to the next heading of the same or higher level. Long ones fold with a "Show more" button. Type![[for a page picker, or use/embed. Embeds count as links for backlinks, the graph, and rename/move rewriting.![[photo.png]]still shows a picture; other files (pdf, audio…) stay plain text. A note can't embed itself, boards aren't shown, and an embed inside an embed appears as a link. - Cover pictures: a page's cover is a gradient, or your own picture. Hover the cover → Change cover → Upload an image… or From a link… (paste an image address; the app downloads it, public http(s) addresses only), or drop an image file onto the cover. The picture is saved next to the page and named in its front matter (
cover: header.png); it moves with the page when you move it, and database galleries show it on the page's card. "Remove the image" goes back to a gradient (the file stays where it is). - Images pasted, dropped or added with
/imageare saved next to the note and written as;![[name.png]]is read too. They are served to the page through anotes-media://protocol that only reads inside the notes folder. - Split view: a second page beside the one you're on, editable, with its own saving. Right-click a page → Open in split, Alt+click it in the sidebar, or drag it from the sidebar onto the right half of the page (the left half makes it the main page and moves the current one to the side), or use the top bar's columns button /
Ctrl/⌘ Shift \(which opens the page you were on before). The side pane's title swaps the panes; ✕ closes it. Boards, databases and canvases work in the side pane too; links clicked in either pane open on the left. - Favorites (the star in the top bar, or right-click a page) and recent pages (shown in
Ctrl/⌘ Kbefore you type) are kept per notes folder in the app settings. - Kanban boards are pages with
kanban-plugin: basicin their front matter, in the Obsidian Kanban plugin's format: each## Headingis a column, each- [ ]item a card (indented lines continue it), and a trailing%% kanban:settings %%block is kept. Create one with New board (Ctrl/⌘ K, or right-click in the sidebar), or turn any page into a board and back from the page's⋯menu. Cards and columns drag around;Ctrl/⌘ Zundoes board changes. Cards are drawn with the editor's own Markdown, so bold / italic / highlight /code/ math,[[links]], bullet and numbered lists,>quotes, headings, fenced code, tables and images (put them on the lines indented under the card) look as they do on a page; clicking a card edits its Markdown as written. Column titles are colour pills: the column's⋯menu picks one of eight colours (or leaves it automatic, from the name). Colours are kept in the page's front matter (ink-colors), never in the board text, so the file stays what the Kanban plugin writes; renaming a column keeps its colour. - Databases are pages whose sub-pages are records, with typed properties and several views of the same records. Make one with Database in the new-page dialog (
Ctrl/⌘ N), "New database" in the sidebar's right-click menu, or the search palette. A record is an ordinary note: its property values are keys in its own front matter (status: Doing,due: 2026-03-01,done: true,owner: [Ana, Ben]), so Obsidian reads them too, and a record can hold text like any page. The database page holds the schema (properties and views) in a%% ink-database … %%comment block and shows the records instead of a text editor; its records aren't listed under it in the sidebar.- Properties: text, number, select, multi-select, date, checkbox, people, files and URL. Add one with + in the table header; its menu renames it, changes its type, edits select options (renaming an option updates the records), sorts, hides, moves or deletes it. Deleting a property or option leaves the values in the files. Files are copied next to the record and open with the system's app.
- Views: table, board (set up like a Kanban board: drag cards between groups; each group's header has its colour pill and a menu to rename it, pick its colour, add a record or delete it; drag a header to reorder the groups, "Add a group" adds an option, a card's
⋯menu moves, renames or deletes it. New databases start with a Table and a Calendar; add a board with the+beside the view tabs), calendar (drag a record to another day,+on a day adds one), gallery (cover picture from a Files property, else the record's cover), timeline (bars from a start date to an optional end date; drag a bar to move it) and list. Each view keeps its own sort, filters, hidden properties and grouping or date property. Right-click a view's tab to rename, change its layout, duplicate or delete it. - Table: drag a column's edge to resize it (kept per view) and its header to reorder; arrow keys move between cells (hold Alt where the arrows mean something in the field, e.g. a text caret or a number), Enter finishes and drops down. Tick rows (Shift-click for a range) for a bar that sets one property on all of them or trashes them in one Undo-able step.
- Option colours (select and multi-select): in the property menu, click an option's dot. Number formats: number with commas, percent, dollar, euro or a progress bar (set what a full bar is); the file always holds the plain number.
- Calendar has Month and Week; drag the right edge of a record to give it an end date (an "End" date property is added if there is none). Timeline: drag a bar's edges to change its start or end. A Relation property holds the titles of other records; point a timeline's Dependencies at one and it draws arrows (red when the earlier record ends after the later one starts). Renaming a record updates relations that name it.
- In a page:
![[Tasks]]shows the database live, in place: filter, sort, switch views and edit values right there.![[Tasks#Board]]picks the saved view called Board (typing#after the name lists the views). An embed's own sort, filters and hidden properties are kept in its alias, e.g.![[Tasks#Board|sort=due:desc; filter=status:is:Done; hide=owner]]; they sit on top of the saved view's and never change the database. Embeds can't add properties or views; open the database for that. - CSV: Export as CSV (page
⋯menu, or the search palette) writes a Name column and one per property, every record. Import CSV… (same menu, the palette, or the sidebar's right-click menu for a new database) reads,,;or tab separated files with quoted cells and lets you choose what each column becomes: the name, an existing property (matched by header), a new property of a guessed type (number, date, checkbox, select, multi-select, URL, text) or nothing. Dates like3/9/2025and amounts like$5,200are understood; select options are collected from the values. - Search and graph: property values are indexed. Plain words match them (
Ana),property:valuefilters (status:Done,status:"In progress",owner:!Ana,points:>=5,due:2026-03,has:due) and combines with words,tag:andin:. With only filters, every matching record is listed with the values that matched.[[links]]written in a property value count as links (graph, backlinks, renames), and so do relation properties between records. - Moving a record out of its database (the record page's
⋯menu has "Move out of …"; dragging a page onto a database makes it a record) says which of its properties stayed in the file, with a Remove button; moving between databases only mentions the ones the new database doesn't have. - Formulas: a Formula property is worked out from the record's other values and never stored in the file.
prop("Price") * prop("Quantity")(or{Price}),if(cond, a, b),concat(…),round(x, 2),dateBetween(prop("Expiry"), today(), "days"),dateAdd(d, 1, "months"),contains,min/max/sum/averageand more;+ - * / % ^, comparisons andand / or / not. An empty number counts as 0. Dates work with+and-(date - dateis days). The property's menu has the editor with a live result for the first record, a number format (euro, dollar, percent, commas), and a list of functions; a mistake shows what is wrong andErrorin the cells. Formulas can use each other, sort and filter numerically (>,≥…) and show on boards, galleries, record pages and in CSV exports. - Totals: the table's bottom row has a Calculate cell under every column (hover it): count, count values / empty / unique, percent filled, and for numbers and formulas sum, average, median, min, max and range; earliest / latest / span for dates; checked and percent checked for checkboxes. They follow the view's filters, use the column's number format and are kept in the view.
- Undo (
Ctrl/⌘ Z,Ctrl/⌘ Shift Z) steps through changes to the schema: properties, options, views, sorts, filters, widths. Changes to record values aren't part of it (use the editor's own undo, or version history on the record). - Records open as pages with the database's properties on top, edited with the same controls. In the table, rename a record in place and use the trash button to delete one (Undo is in the toast).
- Outline (the list button in the top bar,
Ctrl/⌘ Shift O, or Ctrl+K) docks a list of the page's#/##/###headings on the right, indented by level. Click one to scroll there; the section you're reading stays highlighted, and the list updates as you type. Pages only, not boards. Whether it's open is remembered. - Graph view draws every note as a dot and every
[[link]]as a line, laid out by a small force simulation (src/lib/graph.js: springs for links, Barnes–Hut repulsion, a pull to the centre). Drag dots, drag the background to pan, scroll to zoom, hover to light a note's neighbours, click to open. The gear shows filters: highlight, this page only (1–3 links away), notes without links, links to missing pages, page hierarchy, spacing. - Tasks (checklist button in the sidebar footer,
Ctrl/⌘ Shift T, or Ctrl+K → Open tasks) lists every- [ ]/- [x]item in your notes, grouped by page. Filter by open/done, due date (📅 2026-03-01ordue:2026-03-01in the task: overdue, today, next 7 days, none), tag (the page's tags and#tagsin the task) and text. Ticking one edits the line in its file; if the line changed on disk since the list was made, nothing is written. Code blocks, boards and canvases are skipped. - Unlinked mentions: under Linked from, "Mentioned without a link" lists pages that write this page's title in plain text (whole words, not in code, links or URLs; titles under 3 letters are ignored). Link it turns the first mention on that page into
[[Page]](or[[Page|as written]]). - Saved searches: type a query in the palette (
tag:work in:plan status:Done) and choose Save this search to the sidebar. The saved search is listed under Favorites, per notes folder; clicking it opens the palette with the query, and ✕ removes it. - Quick capture: a global shortcut (default
Ctrl/⌘ Alt N, change it in Settings → General; also Ctrl+K → Quick capture) opens a small window. Enter adds what you typed to today's note or to a top-level Inbox page (your choice, also in the window): a single line becomes a bullet, several lines are added as written. Some Wayland desktops don't let apps register global shortcuts; the palette command still works. - Daily notes: previous / next and calendar. On a daily note the top bar shows ‹ 📅 ›: the arrows go to the nearest existing daily note before or after (past the last one: today), the calendar marks days that have a note and opens or creates any day (also Ctrl+K → Open a daily note by date). A template for new daily notes is in Settings → Notes (
{{date}},{{weekday}}). - Properties on any page are typed by their value:
true/falseis a checkbox, a number a number,YYYY-MM-DDa date, a list a multi-select (choices come from the same property on other pages), anhttps://address a link. The icon before a property's name changes its type; Add property asks for the type first. Only the value is written to the front matter. - Tag manager (the gear by Tags, a tag's right-click menu, or Ctrl+K → Manage tags): every tag with its page count; rename (into an existing tag it merges) or remove a tag from all pages.
- Import: Settings → General → Import notes, or Ctrl+K. An Obsidian vault or a Notion "Markdown & CSV" export (folder or .zip) goes into a new top-level folder: Notion's ids are removed from names, its relative links become
[[wiki links]], pictures keep working,.obsidianis skipped. Clip a web page (Ctrl+K) saves a public page's main text as a note withsource:in its front matter. Pasting from a web page or rich editor keeps headings, lists, tables, links and bold as Markdown (Settings → Editor turns it off). - Export: Markdown, HTML and PDF per page (page
⋯menu); Export all notes as a zip (Settings → General, or Ctrl+K) writes the whole folder, without history and trash. - Daily notes live under
Daily/asYYYY-MM-DD.md;Ctrl/⌘ Shift Dor Today in the sidebar opens or creates today's. - Selecting across blocks works the way it does in a text editor: drag from one block into another (up or down; the page scrolls along at its edges), Shift+click to extend, Shift+arrows, Ctrl/⌘+Shift+arrows (words, paragraphs), Shift+Home/End and Ctrl/⌘+Shift+Home/End (start or end of the page). Delete, typing and copy then act on the whole selection.
- Undo (
Ctrl/⌘ Z,Ctrl/⌘ Shift ZorCtrl Y) works across blocks: typing in one block is one step, and every new, deleted, converted or moved block is another. - Edits made elsewhere (another editor, a sync tool,
git) show up in the open page as soon as the file changes, unless you have unsaved edits there. If you do, and the file changed on disk too, your version is saved and the other one is first put into the page's version history, with a toast that opens it. Nothing is overwritten without a copy. (Notes folders are watched folder by folder; dot folders such as.notesand.gitare ignored.) - Saving is debounced (600 ms) and flushed on page switch, window blur,
Ctrl/⌘ S, and before the window closes. - Security:
contextIsolation,sandbox, nonodeIntegration; the UI is served from a customapp://protocol with a hashed CSP and no network access. Fonts are bundled.
Layout
electron/
main.cjs window, app:// and notes-media:// protocols, IPC handlers, safe close
preload.cjs window.vault bridge (the only API the page has)
vault.cjs read / write / create / rename / move / remove / watch Markdown files, save images
links.cjs find, rewrite and backlink [[page links]] on disk
canvas.cjs .canvas files on disk: read/write, text for search, keeping them in step with renames
frontmatter.cjs minimal YAML front matter
db.cjs SQLite FTS5 search index
settings.cjs theme, window size, last page (in the app's userData folder)
seed.cjs "Getting started" page for an empty folder
src/
routes/ +layout (SPA, fonts, css) and +page (the app shell)
lib/
editor.js block editor engine (plain DOM, mounted by Editor.svelte)
markdown.js Markdown <-> blocks, HTML export
links.js how a [[link]] finds its page (same rules as electron/links.cjs)
kanban.js Kanban board <-> Markdown
database.js database schema <-> Markdown, property values, filters, sorting, calendar and timeline maths
canvas.js .canvas format (same as electron/canvas.cjs), arrow and zoom geometry
graph.js force simulation for the graph view
embed.js headings, sections and read-only HTML for ![[embeds]]
scroll.js scroll a block into view and flash it (outline, heading links, footnotes)
footnotes.js number footnote references and texts on screen
state.svelte.js the store (Svelte 5 runes)
api.js / mock.js Electron bridge, browser mock
components/ Sidebar, TreeNode, Topbar, PageView, Editor, Backlinks, Board, Database, PropCell, CanvasView, FindBar, Outline, GraphView, SettingsDialog, Palette, ContextMenu, TrashDialog, ExportDialog, Toast, Welcome, WindowControls
resources/
icon.svg the app icon (source); icon.png, icon.ico and icons/NxN.png are rendered from it
scripts/icons.sh renders the icon files
test/ node --test suites
Keyboard
| Keys | Action |
|---|---|
Ctrl/⌘ K |
Search notes and run commands |
Ctrl/⌘ N |
New page |
Ctrl/⌘ Shift D |
Today's daily note |
Ctrl/⌘ G |
Graph view (Esc goes back) |
Ctrl/⌘ Shift H |
Highlight the selection (==text==) |
Ctrl/⌘ Shift O |
Show / hide the page outline |
Ctrl/⌘ , |
Settings: theme, text size, page width, notes folder, spellcheck |
Ctrl/⌘ \ |
Show or hide the sidebar |
Ctrl/⌘ S |
Save now |
Ctrl/⌘ Z, Ctrl/⌘ Shift Z |
Undo, redo (across blocks) |
Ctrl/⌘ F |
Find in page (Enter / Shift Enter to step, Esc to close) |
Ctrl/⌘ B, I, E, Shift S |
Bold, italic, code, strikethrough (a toolbar also appears over selected text) |
Ctrl/⌘ K with text selected |
Link the selection to a web address (on a link: edit it) |
Ctrl/⌘ click a link |
Open it in the browser |
Tab, Shift Tab, Enter in a table |
Next / previous cell, cell below (adding a row at the end) |
Ctrl/⌘ D |
Duplicate the current block |
Click ⋮⋮ |
Block menu: turn into, duplicate, delete |
/ |
Block menu |
[[ |
Link to a page (or create one) |
#, ##, ###, -, 1., [], >, ```, --- |
Markdown shortcuts at the start of a line |
Packaging
bun run dist builds for the current OS into dist/: an AppImage on Linux (Ink-<version>-x86_64.AppImage, with a desktop entry and icons), an NSIS installer on Windows (lets you pick the install folder, adds Start menu and desktop shortcuts), a dmg on macOS. Installer settings live in the build field of package.json; resources/ holds their icons (build/ is Vite's output, so it can't be used for that).
On Linux, docks find a window's icon through a .desktop file named after its app id (ink, from desktopName in package.json). An AppImage installs nothing itself, so when Ink runs from one it writes ~/.local/share/applications/ink.desktop (pointing at the AppImage's current location) and ~/.local/share/icons/hicolor/*/apps/ink.png. Delete those to remove it.
The app was called "Notes" before; on first launch Ink copies the old settings (notes folder, favorites, window size) from the Notes settings folder.