Files
Your NameandClaude Sonnet 5.5 0b5f8bfa8b chore: project scaffold, build config, scripts and README
Bun + SvelteKit (static adapter) + Electron project files, icon resources,
dev/e2e/bench scripts and the README documenting the app.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 11:57:09 +02:00

28 KiB
Raw Permalink Blame History

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.md is a page; Projects/Roadmap.md is its child. A folder without a matching .md still 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, or Ctrl/⌘ 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 bare https://… 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. Only http(s) and mailto links open; others stay plain text.
  • Tables are GitHub-style Markdown tables, edited cell by cell (right-click a cell for rows, columns and alignment; /table makes 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 .canvas file 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 or Ctrl/⌘ +/− zooms, Ctrl/⌘ 0 is 100%, Shift+1 shows 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, Delete removes, Ctrl/⌘ D duplicates, Ctrl/⌘ C/X/V copy, 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.
  • 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 as icon and tags carries 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, so Ctrl/⌘ N then Enter gives 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.svelte draws 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 ```mermaid block 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 or Ctrl/⌘ Shift H to add or remove it.
  • Footnotes: [^id] marks a reference and a [^id]: text line 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. /footnote adds 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 as Page › Heading, and typing # inside [[ suggests the page's headings. Renaming a page keeps the #Heading part. 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 /image are saved next to the note and written as ![](name.png); ![[name.png]] is read too. They are served to the page through a notes-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/⌘ K before you type) are kept per notes folder in the app settings.
  • Kanban boards are pages with kanban-plugin: basic in their front matter, in the Obsidian Kanban plugin's format: each ## Heading is 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/⌘ Z undoes 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 like 3/9/2025 and amounts like $5,200 are understood; select options are collected from the values.
    • Search and graph: property values are indexed. Plain words match them (Ana), property:value filters (status:Done, status:"In progress", owner:!Ana, points:>=5, due:2026-03, has:due) and combines with words, tag: and in:. 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/average and more; + - * / % ^, comparisons and and / or / not. An empty number counts as 0. Dates work with + and - (date - date is 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 and Error in 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-01 or due:2026-03-01 in the task: overdue, today, next 7 days, none), tag (the page's tags and #tags in 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/false is a checkbox, a number a number, YYYY-MM-DD a date, a list a multi-select (choices come from the same property on other pages), an https:// 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, .obsidian is skipped. Clip a web page (Ctrl+K) saves a public page's main text as a note with source: 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/ as YYYY-MM-DD.md; Ctrl/⌘ Shift D or 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 Z or Ctrl 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 .notes and .git are ignored.)
  • Saving is debounced (600 ms) and flushed on page switch, window blur, Ctrl/⌘ S, and before the window closes.
  • Security: contextIsolation, sandbox, no nodeIntegration; the UI is served from a custom app:// 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.