sdfsdf
Deploy to GitHub Pages / build (push) Failing after 57s
Deploy to GitHub Pages / deploy (push) Skipped

This commit is contained in:
Your Name committed 2026-10-15 15:05:50 +02:00
1 parent a59be0fc9c
commit 4cd68793c9
41 files changed
+287 -129

No files matched your search

+4 -4
View File
@@ -25,9 +25,9 @@ bun run preview # serve dist/
### Direct downloads
Every download button links straight to the file (`platforms.linux.url` in `src/config.ts`), with no
intermediate page. Only visitors on Windows, macOS or a phone are sent to `/download/`, because the
Linux AppImage is no use to them yet. Point `url` at any direct file URL. By default it is the GitHub
Releases asset, which is a plain download link. Don't host the 157 MB file with the site itself on
intermediate page. Windows visitors get the .exe on the same button; macOS and phone visitors are sent to `/download/`, because the
Linux AppImage is no use to them. Point `url` at any direct file URL. By default it is the release on git.joji.li, which is a plain download link.
Don't host the 157 MB file with the site itself on
GitHub Pages (100 MB per-file limit) or Cloudflare Pages (25 MB); use Releases, R2/S3 or a CDN.
### Releasing a new version
@@ -83,4 +83,4 @@ Add `Sitemap: <SITE_URL>/sitemap-index.xml` to `public/robots.txt` once the URL
- `public/og.png` (1200x630) is a static file; replace it if the tagline changes.
- The `typescript` dependency is pinned to 6.x because `astro check` does not support TypeScript 7 yet.
- Builds are unsigned and Windows/macOS are "coming soon" by design: see `platforms.*.available` in `config.ts`.
- Builds are unsigned and macOS is "coming soon" by design: see `platforms.*.available` in `config.ts`.
+85 -39
View File
@@ -1,72 +1,118 @@
'use strict';
// Runs the real Ink app against a seeded demo vault and saves 16:10 screenshots.
// electron scripts/capture/capture.cjs --ozone-platform=headless --force-device-scale-factor=1.5
// Env: INK_APP (path to the Ink repo, read only), OUT (output dir), THEMES (comma list), SCENES (comma list)
const { app, BrowserWindow } = require('electron');
// Runs the packaged Ink app (the released AppImage, extracted) against a seeded demo vault and saves 16:10
// screenshots, driving it over the Chrome DevTools protocol. Nothing is read from the app's source.
// node scripts/capture/capture.cjs (env: INK_BIN, OUT, THEME, SCENES; run.sh sets these up)
const { spawn } = require('node:child_process');
const path = require('node:path');
const os = require('node:os');
const fs = require('node:fs');
const INK = process.env.INK_APP || path.resolve(__dirname, '../../../../App/Ink');
const BIN = process.env.INK_BIN;
const OUT = process.env.OUT || path.resolve(__dirname, '../../src/assets/screens');
const THEMES = (process.env.THEMES || 'dark,light').split(',');
const ONLY = process.env.SCENES ? process.env.SCENES.split(',') : null;
const theme = process.env.THEME || 'dark';
const palette = process.env.PALETTE || 'default';
const accent = process.env.ACCENT || 'blush';
const SUFFIX = process.env.SUFFIX || theme;
const { seedVault } = require('./seed-vault.cjs');
const scenes = require('./scenes.cjs');
if (!BIN) throw new Error('Set INK_BIN to the extracted AppImage\'s `ink` binary (run.sh does this).');
const W = 1600, H = 1000;
const W = 1600, H = 1000, SCALE = 1.5, PORT = 9300 + (process.pid % 500);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const theme = process.env.THEME || THEMES[0];
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ink-shots-'));
const vault = path.join(tmp, 'Notes');
fs.mkdirSync(vault);
const seeded = seedVault(vault);
// No network: a link card gets a fixed title and description instead of fetching.
const linkpreview = require(path.join(INK, 'electron/linkpreview.cjs'));
linkpreview.hooks.lookup = async () => [{ address: '93.184.216.34' }];
linkpreview.hooks.fetch = async () => new Response(
'<html><head><title>JSON Canvas</title><meta property="og:title" content="JSON Canvas"><meta name="description" content="An open file format for infinite canvas data.">' +
'</head><body></body></html>', { status: 200, headers: { 'content-type': 'text/html' } });
app.setPath('userData', tmp);
fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({
vault, lastPage: seeded.lastPage, theme, bounds: { width: W, height: H },
vault, lastPage: seeded.lastPage, theme, palette, accent, bounds: { width: W, height: H },
fontSize: 16, outline: false, sidebarCollapsed: false, tagsOpen: true,
vaults: { [vault]: seeded.vaultSettings },
}));
app.commandLine.appendSwitch('lang', 'en-US');
require(path.join(INK, 'electron/main.cjs'));
app.whenReady().then(async () => {
let win;
for (let i = 0; i < 100 && !(win = BrowserWindow.getAllWindows()[0]); i++) await sleep(100);
win.setContentSize(W, H);
const wc = win.webContents;
if (wc.isLoading()) await new Promise((r) => wc.once('did-finish-load', r));
const js = (code) => wc.executeJavaScript(`(async () => { ${code} })()`);
const key = async (keyCode, modifiers = []) => { wc.sendInputEvent({ type: 'keyDown', keyCode, modifiers }); wc.sendInputEvent({ type: 'keyUp', keyCode, modifiers }); await sleep(250); };
const type = async (t, d = 45) => { for (const ch of t) { wc.sendInputEvent({ type: 'char', keyCode: ch }); await sleep(d); } await sleep(200); };
const click = async (x, y) => { wc.sendInputEvent({ type: 'mouseMove', x, y }); wc.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount: 1 }); wc.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount: 1 }); await sleep(250); };
const KEYS = { Escape: ['Escape', 27], Return: ['Enter', 13], Enter: ['Enter', 13], Tab: ['Tab', 9], Backspace: ['Backspace', 8], Down: ['ArrowDown', 40], Up: ['ArrowUp', 38] };
const MODS = { alt: 1, control: 2, meta: 4, shift: 8 };
async function connect() {
for (let i = 0; i < 150; i++) {
try {
const list = await (await fetch(`http://127.0.0.1:${PORT}/json`)).json();
const page = list.find((t) => t.type === 'page' && !t.url.startsWith('devtools'));
if (page) {
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
return ws;
}
} catch {}
await sleep(200);
}
throw new Error('Ink did not start');
}
(async () => {
const proc = spawn(BIN, [
`--user-data-dir=${tmp}`, `--remote-debugging-port=${PORT}`, '--ozone-platform=headless', '--no-sandbox', '--lang=en-US',
], { stdio: 'ignore' });
const ws = await connect();
let id = 0;
const pending = new Map();
ws.onmessage = (m) => {
const d = JSON.parse(m.data);
if (d.id && pending.has(d.id)) { const { res, rej } = pending.get(d.id); pending.delete(d.id); d.error ? rej(new Error(d.error.message)) : res(d.result); }
};
const send = (method, params = {}) => new Promise((res, rej) => { pending.set(++id, { res, rej }); ws.send(JSON.stringify({ id, method, params })); });
await send('Emulation.setDeviceMetricsOverride', { width: W, height: H, deviceScaleFactor: SCALE, mobile: false });
await send('Runtime.enable');
const js = async (code) => {
const r = await send('Runtime.evaluate', { expression: `(async () => { ${code} })()`, awaitPromise: true, returnByValue: true });
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
return r.result.value;
};
const key = async (name, modifiers = []) => {
const m = modifiers.reduce((a, k) => a | MODS[k], 0);
const [k, vk] = KEYS[name] || [name, name.length === 1 ? name.toUpperCase().charCodeAt(0) : 0];
const text = !m || m === 8 ? (name.length === 1 ? name : undefined) : undefined;
const code = name.length === 1 && /[a-z]/i.test(name) ? `Key${name.toUpperCase()}` : /^\d$/.test(name) ? `Digit${name}` : k;
await send('Input.dispatchKeyEvent', { type: 'rawKeyDown', key: k, code, windowsVirtualKeyCode: vk, modifiers: m });
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: k, code, windowsVirtualKeyCode: vk, modifiers: m });
await sleep(250);
};
const type = async (t, d = 45) => {
for (const ch of t) {
await send('Input.dispatchKeyEvent', { type: 'keyDown', key: ch, text: ch, unmodifiedText: ch, windowsVirtualKeyCode: ch.toUpperCase().charCodeAt(0) });
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: ch });
await sleep(d);
}
await sleep(200);
};
const click = async (x, y) => {
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y });
await send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
await sleep(250);
};
const shot = async (name) => {
await sleep(500);
const img = await wc.capturePage();
const file = path.join(OUT, `${name}-${theme}.png`);
const r = await send('Page.captureScreenshot', { format: 'png' });
const file = path.join(OUT, `${name}-${SUFFIX}.png`);
fs.mkdirSync(OUT, { recursive: true });
fs.writeFileSync(file, img.toPNG());
const s = img.getSize();
console.log(`saved ${path.basename(file)} ${s.width}x${s.height}`);
fs.writeFileSync(file, Buffer.from(r.data, 'base64'));
console.log(`saved ${path.basename(file)}`);
};
const ctx = { win, wc, js, key, type, click, shot, sleep, vault, W, H };
const ctx = { js, key, type, click, shot, sleep, vault, W, H, send };
try {
await sleep(2200);
await sleep(3000);
for (const [name, fn] of Object.entries(scenes)) {
if (ONLY && !ONLY.includes(name)) continue;
try { await fn(ctx); } catch (e) { console.log(`FAILED ${name}: ${e.message}`); }
}
} finally {
app.exit(0);
ws.close();
proc.kill();
fs.rmSync(tmp, { recursive: true, force: true });
}
});
})();
+9 -7
View File
@@ -1,12 +1,14 @@
#!/usr/bin/env sh
# Captures all website screenshots from the real Ink app (read only) into src/assets/screens/.
# Usage: scripts/capture/run.sh (env: INK_APP, OUT, SCENES)
# Captures all website screenshots from the released Ink AppImage into src/assets/screens/.
# Usage: scripts/capture/run.sh Ink-1.0.0-beta-x86_64.AppImage (env: OUT, SCENES)
set -e
HERE="$(cd "$(dirname "$0")" && pwd)"
INK_APP="${INK_APP:-$HERE/../../../../App/Ink}"
INK_APP="$(cd "$INK_APP" && pwd)"
export INK_APP
APPIMAGE="$(realpath "${1:-${INK_APPIMAGE:?pass the AppImage path}}")"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
# Extract in an empty directory of its own; the AppImage is only unpacked, never run from here.
(cd "$WORK" && cp "$APPIMAGE" ink.AppImage && chmod +x ink.AppImage && ./ink.AppImage --appimage-extract >/dev/null)
export INK_BIN="$WORK/squashfs-root/ink"
for theme in dark light; do
THEME=$theme "$INK_APP/node_modules/.bin/electron" "$HERE/capture.cjs" \
--ozone-platform=headless --force-device-scale-factor=1.5 --no-sandbox 2>&1 | grep -E 'saved|FAILED|Error' || true
THEME=$theme node "$HERE/capture.cjs" 2>&1 | grep -E 'saved|FAILED|Error' || true
done
+36 -1
View File
@@ -110,7 +110,7 @@ module.exports = {
},
async settings(ctx) {
const { sleep, shot, key } = ctx;
const { js, sleep, shot, key } = ctx;
await reset(ctx);
await openFromSidebar(ctx, 'Roadmap');
await key(',', ['control']);
@@ -132,4 +132,39 @@ module.exports = {
await shot('history');
await key('Escape');
},
async databases(ctx) {
const { js, sleep, shot } = ctx;
await reset(ctx);
await openFromSidebar(ctx, 'Feature tracker');
await sleep(1200);
await scrollPage(js, 230);
await sleep(400);
await shot('databases');
},
async 'database-board'(ctx) {
const { js, sleep, shot } = ctx;
await reset(ctx);
await openFromSidebar(ctx, 'Feature tracker');
await sleep(800);
await js(`[...document.querySelectorAll('button, [role="tab"]')].find((b) => b.textContent.trim() === 'Board')?.click()`);
await sleep(1000);
await scrollPage(js, 230);
await sleep(400);
await shot('database-board');
},
// The settings dialog on the color theme and accent pickers. Run once per palette by run.sh.
async themes(ctx) {
const { js, sleep, shot, key } = ctx;
await reset(ctx);
await openFromSidebar(ctx, 'Roadmap');
await key(',', ['control']);
await sleep(900);
await js(`document.querySelector('[aria-label="Color theme"]')?.scrollIntoView({ block: 'center' })`);
await sleep(500);
await shot('themes');
await key('Escape');
},
};
+44 -8
View File
@@ -60,7 +60,7 @@ function seedVault(root) {
- [x] Ship the first public build as an AppImage
- [x] Canvas export to PNG, SVG and PDF
- [x] Version history for pages and canvases
- [ ] Test the Windows installer on a clean machine
- [x] Windows installer and Linux deb
- [ ] Write the docs, starting with [[Keyboard shortcuts]]
- [ ] Follow the [[Launch plan]]
@@ -69,8 +69,8 @@ function seedVault(root) {
\`\`\`js
const release = {
version: '0.1.0',
platforms: ['linux'],
version: '1.0.0-beta',
platforms: ['linux', 'windows'],
notes: 'plain .md files, offline, no account',
};
console.log(\`Shipping Ink \${release.version}\`);
@@ -79,7 +79,7 @@ console.log(\`Shipping Ink \${release.version}\`);
<details>
<summary>Why Linux first?</summary>
The AppImage is the only target built and tested so far. Windows and macOS are configured but still need a proper pass.
The AppImage was the first target built and tested. Windows and the deb followed; macOS is still to do.
</details>
`);
@@ -142,7 +142,6 @@ tags: [work, ink]
## To do
- [ ] Windows installer test pass
- [ ] macOS dmg build
- [ ] Docs: where notes are stored
- [ ] Verify checksum instructions
@@ -151,13 +150,14 @@ tags: [work, ink]
## Doing
- [ ] Website screenshots
- [ ] Changelog for 0.1.0
- [ ] Changelog for 1.0.0-beta
- [ ] Spellcheck language picker polish
## Done
- [x] AppImage build
- [x] AppImage and deb builds
- [x] Windows installer
- [x] Canvas export (PNG, SVG, PDF)
- [x] Version history for canvases
- [x] Graph view filters
@@ -199,6 +199,42 @@ The release is a single AppImage, so the checklist is short: bump the version, w
Once that is done, the website can go live with a plain download button and nothing else.
`);
// A database: the page holds the schema, every record is a sub-page whose properties are front matter.
const dbSchema = {
properties: [
{ key: 'status', name: 'Status', type: 'select', options: ['Backlog', 'In progress', 'Review', 'Done'], colors: { Backlog: 'gray', 'In progress': 'blue', Review: 'yellow', Done: 'green' } },
{ key: 'area', name: 'Area', type: 'select', options: ['Editor', 'Canvas', 'Databases', 'Themes', 'Site'], colors: { Editor: 'pink', Canvas: 'purple', Databases: 'teal', Themes: 'orange', Site: 'blue' } },
{ key: 'owner', name: 'Owner', type: 'people' },
{ key: 'start', name: 'Start', type: 'date' },
{ key: 'due', name: 'Due', type: 'date' },
{ key: 'progress', name: 'Progress', type: 'number', format: 'bar', max: 100 },
{ key: 'shipped', name: 'Shipped', type: 'checkbox' },
],
views: [
{ id: 'vtable', name: 'Table', type: 'table', sort: { key: 'due', dir: 'asc' }, filters: [], hidden: [], widths: {}, calc: {} },
{ id: 'vboard', name: 'Board', type: 'board', groupBy: 'status', sort: null, filters: [], hidden: [], widths: {}, calc: {} },
{ id: 'vcal', name: 'Calendar', type: 'calendar', dateProp: 'due', sort: null, filters: [], hidden: [], widths: {}, calc: {} },
{ id: 'vtime', name: 'Timeline', type: 'timeline', dateProp: 'start', endProp: 'due', sort: null, filters: [], hidden: [], widths: {}, calc: {} },
],
};
w(root, 'Feature tracker.md', fm({ icon: '🧭', tags: ['work', 'ink'], 'ink-database': 'basic' }) + `Everything planned for the next release, one record per feature.\n\n%% ink-database\n${JSON.stringify(dbSchema, null, 2)}\n%%\n`);
const recs = [
['Color themes', 'Themes', 'Done', ['Mara'], '2026-09-14', '2026-09-26', 100, true],
['Accent colors', 'Themes', 'Done', ['Mara'], '2026-09-20', '2026-10-01', 100, true],
['Table view', 'Databases', 'Done', ['Jo'], '2026-09-08', '2026-10-02', 100, true],
['Board view', 'Databases', 'Review', ['Jo', 'Ana'], '2026-09-24', '2026-10-12', 80, false],
['Calendar view', 'Databases', 'In progress', ['Ana'], '2026-10-01', '2026-10-16', 60, false],
['Timeline view', 'Databases', 'In progress', ['Jo'], '2026-10-05', '2026-10-22', 40, false],
['Formula properties', 'Databases', 'Backlog', ['Jo'], '2026-10-14', '2026-10-30', 10, false],
['Canvas alignment guides', 'Canvas', 'Done', ['Ana'], '2026-09-01', '2026-09-18', 100, true],
['Quick capture', 'Editor', 'Review', ['Mara'], '2026-10-03', '2026-10-14', 90, false],
['Website refresh', 'Site', 'In progress', ['Mara', 'Jo'], '2026-10-08', '2026-10-20', 55, false],
];
for (const [t, area, status, owner, start, due, progress, shipped] of recs) {
w(root, `Feature tracker/${t}.md`, fm({ area, status, owner, start, due, progress, shipped }) + `${t}: notes and decisions live here, as an ordinary page.\n`);
}
// Earlier versions of the Roadmap page, so version history has something to show.
const now = Date.now();
const hist = [
@@ -217,7 +253,7 @@ Once that is done, the website can go live with a plain download button and noth
return {
lastPage: 'Roadmap',
vaultSettings: {
pins: ['Roadmap', 'Reading list', 'Sprint board', 'Project map.canvas'],
pins: ['Roadmap', 'Feature tracker', 'Sprint board', 'Project map.canvas'],
recent: ['Roadmap', 'Reading list', 'Sprint board'],
tagColors: { work: 'blue', ink: 'pink', reading: 'yellow', learning: 'teal', personal: 'green', ideas: 'purple' },
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 994 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 900 KiB

After

Width:  |  Height:  |  Size: 950 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1005 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 KiB

After

Width:  |  Height:  |  Size: 1013 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 KiB

After

Width:  |  Height:  |  Size: 891 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 KiB

After

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 691 KiB

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 KiB

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 KiB

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 834 KiB

After

Width:  |  Height:  |  Size: 979 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 KiB

After

Width:  |  Height:  |  Size: 782 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+6 -4
View File
@@ -1,21 +1,23 @@
---
// A direct download: the href is the file itself, in the HTML, so it works without JS and never
// bounces through another page. Only visitors on Windows, macOS or a phone (detected in
// scripts/site.ts) are sent to /download/ instead, because the Linux AppImage is no use to them yet.
// bounces through another page. Only visitors on macOS or a phone (detected in
// scripts/site.ts) are sent to /download/ instead, because the Linux AppImage is no use to them.
import { config, formatSize } from '../config';
import { href } from '../lib/url';
interface Props { variant?: 'primary' | 'default'; note?: boolean; center?: boolean }
const { variant = 'primary', note = true, center = false } = Astro.props;
const linux = config.platforms.linux;
const win = config.platforms.windows;
const meta = `v${config.version} · ${linux.format} · ${formatSize(linux.bytes)}`;
const winMeta = `v${config.version} · ${win.format} · ${formatSize(win.bytes)}`;
---
<div class:list={['dl', { center }]} data-download data-meta={meta}>
<div class:list={['dl', { center }]} data-download data-meta={meta} data-win-url={win.url} data-win-file={win.file} data-win-meta={winMeta}>
<a class:list={['btn', variant]} href={linux.url} download={linux.file} data-dl-link data-info={href('/download/')}>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11m0 0l-4.5-4.5M12 15l4.5-4.5M5 19h14" /></svg>
<span data-download-label>Download for Linux</span>
</a>
{note && <p class="muted" data-download-note>{meta} · Windows &amp; macOS coming soon</p>}
{note && <p class="muted" data-download-note>{meta} · also for Windows and as a deb · macOS coming soon</p>}
</div>
<style>
.dl { display: grid; gap: 0.7rem; justify-items: start; }
+18 -13
View File
@@ -1,12 +1,9 @@
/** Single source of truth for everything release-specific. Edit here, nowhere else. */
const version = '0.1.0';
const version = '1.0.0-beta';
/**
* TODO: set to the real repository once it is public. The download links below derive from it and are
* used as direct links by every download button. Replace `url` with any direct file URL if you host it elsewhere.
*/
const repo = 'https://github.com/OWNER/ink';
/** Release assets are served from this repository's releases page. */
const repo = 'https://git.joji.li/joji/ink-note-app';
export const config = {
name: 'Ink',
@@ -15,7 +12,7 @@ export const config = {
description:
'Ink is a calm, offline-first Markdown notes app for the desktop. Notes are plain .md files in a folder you choose. No account, nothing uploaded.',
version,
releaseDate: '2026-10-11',
releaseDate: '2026-10-15',
repo,
releases: `${repo}/releases`,
license: 'TODO',
@@ -27,18 +24,26 @@ export const config = {
arch: 'x86_64',
file: `Ink-${version}-x86_64.AppImage`,
url: `${repo}/releases/download/v${version}/Ink-${version}-x86_64.AppImage`,
bytes: 164_943_435,
sha256: '0d35758636f6cb7f5eb1c439a75ebd47ae79d31fc7bf91860d26d9eaef06ef3e',
bytes: 165_131_841,
sha256: '34f272809881c036f8a275e1effa432797c3bf82809581cb9e1224410e733193',
deb: {
format: 'deb',
arch: 'amd64',
file: `Ink-${version}-amd64.deb`,
url: `${repo}/releases/download/v${version}/Ink-${version}-amd64.deb`,
bytes: 105_354_452,
sha256: 'c2b7d5fc698447dbc610464933fd4a91a5950dd00a74c4259a29e3e6aaeab964',
},
},
windows: {
label: 'Windows',
format: 'NSIS installer',
available: false,
available: true,
arch: 'x64',
file: `Ink-${version}-x64.exe`,
url: '',
bytes: 0,
sha256: '',
url: `${repo}/releases/download/v${version}/Ink-${version}-x64.exe`,
bytes: 126_749_103,
sha256: '07d1a407957970eadd785a294069535bb8db35443fa19e26931b8eae93355773',
},
macos: {
label: 'macOS',
-9
View File
@@ -1,9 +0,0 @@
---
version: '0.0.1'
date: 2026-08-02
title: The editor
prerelease: true
---
- Block editor with slash menu and Markdown shortcuts.
- Notes stored as plain `.md` files in a folder you choose.
- Dark and light themes.
-9
View File
@@ -1,9 +0,0 @@
---
version: '0.0.2'
date: 2026-08-30
title: Linking and search
prerelease: true
---
- Heading links (`[[Page#Heading]]`) and embeds.
- Renaming a page or heading rewrites the links to it.
- Tag colours, the `tag:` and `in:` search operators, and the command palette.
-10
View File
@@ -1,10 +0,0 @@
---
version: '0.0.3'
date: 2026-09-20
title: Canvas polish
prerelease: true
---
- Alignment guides, align and distribute, and find-in-canvas.
- Groups can be folded away.
- Arrow styles (curved, straight, right-angled) and arrowheads.
- Version history for canvases.
@@ -1,9 +1,10 @@
---
version: '0.1.0'
date: 2026-10-11
title: First public release
version: '1.0.0-beta'
date: 2026-10-15
title: First release
prerelease: true
---
The first build of Ink you can download: a Linux AppImage.
The first build of Ink you can download, for Linux (AppImage and deb) and Windows.
### Writing
- Block editor with a slash menu, floating formatting bar, Markdown shortcuts and drag-to-reorder.
+12
View File
@@ -37,6 +37,18 @@ export const screens = {
alt: 'A kanban board built from a page, with To do, Doing and Done columns of cards.',
capture: 'A board page with 3-4 columns and 3-5 cards each, one card mid-drag if possible.',
},
databases: {
alt: 'A database page shown as a table with status, area, owner, date and progress columns, and tabs for board, calendar and timeline views.',
capture: 'A database page in table view with ~10 records, select, people, date and progress-bar columns, view tabs visible.',
},
'database-board': {
alt: 'The same database as a board, with records grouped into Backlog, In progress, Review and Done columns.',
capture: 'The database in board view grouped by Status, cards showing area, owner, dates and progress.',
},
themes: {
alt: 'The appearance settings with a grid of color themes such as Midnight, Nord, Forest and Catppuccin, and a row of accent colors.',
capture: 'Settings > Appearance scrolled to the Color theme grid and Accent color row.',
},
palette: {
alt: 'The command palette open with a search for a tag, listing matching notes with snippets.',
capture: 'Ctrl+K palette with a query like "tag:work roadmap" typed, 4-6 results with snippets, page dimmed behind it.',
+1 -1
View File
@@ -81,7 +81,7 @@ const shortcuts: [string, string][] = [
<section id="faq" aria-labelledby="faq-h">
<h2 id="faq-h">FAQ</h2>
<h3>Which platforms are supported?</h3>
<p>Linux (AppImage) is available now. Windows and macOS builds are configured but not built or tested yet, so there is no download for them. See <a href={href('/download/')}>Download</a>.</p>
<p>Linux (AppImage and deb) and Windows (installer) are available now. macOS is not built yet, so there is no download for it. See <a href={href('/download/')}>Download</a>.</p>
<h3>Does it sync between devices?</h3>
<p>Ink has no sync of its own. Because your notes are plain files, you can keep the folder in any file-sync or backup tool you already trust.</p>
<h3>Does it need an internet connection?</h3>
+36 -11
View File
@@ -5,19 +5,21 @@ import { href } from '../lib/url';
const p = config.platforms;
const linux = p.linux;
const soon: PlatformId[] = ['windows', 'macos'];
const win = p.windows;
const deb = linux.deb;
const soon: PlatformId[] = ['macos'];
---
<Base
title="Download"
current="/download/"
description={`Download Ink ${config.version} for Linux (AppImage). Windows and macOS builds are coming soon. Unsigned builds, no tracking.`}
description={`Download Ink ${config.version} for Linux (AppImage, deb) and Windows. macOS is coming soon. Unsigned builds, no tracking.`}
>
<div class="head-bg">
<section class="wrap head">
<img class="app-icon rise" src={href('/icon-192.png')} width="88" height="88" alt="" />
<p class="eyebrow">Download</p>
<h1>Get Ink</h1>
<p class="lede">Version {config.version}, released {config.releaseDate}. Linux is available now. Windows and macOS are configured but not built or tested yet.</p>
<p class="lede">Version {config.version}, released {config.releaseDate}. Available for Linux and Windows. macOS is not built yet.</p>
</section>
</div>
@@ -29,12 +31,31 @@ const soon: PlatformId[] = ['windows', 'macos'];
</div>
<h2>Linux</h2>
<p class="meta">{linux.format} · {linux.arch} · {formatSize(linux.bytes)} · v{config.version}</p>
<p>A single file. No install step, no root needed.</p>
<p>An AppImage is a single file, no install step and no root needed. Debian and Ubuntu users can take the deb instead.</p>
<a class="btn primary" href={linux.url} download={linux.file}>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11m0 0l-4.5-4.5M12 15l4.5-4.5M5 19h14" /></svg>
Download AppImage
</a>
<p class="file">{linux.file}</p>
<a class="btn" href={deb.url} download={deb.file}>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11m0 0l-4.5-4.5M12 15l4.5-4.5M5 19h14" /></svg>
Download deb ({formatSize(deb.bytes)})
</a>
<p class="file">{deb.file}</p>
</article>
<article class="card main spot reveal" data-os="windows">
<div class="top">
<span class="os-ic" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="4" y="4" width="7" height="7" rx="1.2" /><rect x="13" y="4" width="7" height="7" rx="1.2" /><rect x="4" y="13" width="7" height="7" rx="1.2" /><rect x="13" y="13" width="7" height="7" rx="1.2" /></svg></span>
<span class="badges"><span class="badge you" hidden>Your system</span><span class="badge ok">Available</span></span>
</div>
<h2>Windows</h2>
<p class="meta">{win.format} · {win.arch} · {formatSize(win.bytes)} · v{config.version}</p>
<p>Run the installer. The build is unsigned, so SmartScreen may warn you first.</p>
<a class="btn primary" href={win.url} download={win.file}>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11m0 0l-4.5-4.5M12 15l4.5-4.5M5 19h14" /></svg>
Download installer
</a>
<p class="file">{win.file}</p>
</article>
{soon.map((id) => (
<article class="card soon spot reveal" aria-labelledby={`${id}-h`} data-os={id}>
@@ -48,7 +69,7 @@ const soon: PlatformId[] = ['windows', 'macos'];
</div>
<h2 id={`${id}-h`}>{p[id].label}</h2>
<p class="meta">{p[id].format}</p>
<p>Not built or tested yet. There is no download to offer, so there is no link here.</p>
<p>Not built yet. There is no download to offer, so there is no link here.</p>
<p>News will show up in the <a href={href('/changelog/')}>changelog</a>. No sign-up, nothing collected.</p>
</article>
))}
@@ -60,7 +81,7 @@ const soon: PlatformId[] = ['windows', 'macos'];
<li class="step reveal">
<span class="n" aria-hidden="true">1</span>
<h3>Download</h3>
<p>Grab <code>{linux.file}</code> above. It's {formatSize(linux.bytes)}.</p>
<p>Grab <code>{linux.file}</code> above. It's {formatSize(linux.bytes)}. With the deb, run <code>sudo apt install ./{deb.file}</code> instead and start Ink from your app menu.</p>
</li>
<li class="step reveal">
<span class="n" aria-hidden="true">2</span>
@@ -81,13 +102,16 @@ const soon: PlatformId[] = ['windows', 'macos'];
<p>When run from an AppImage, Ink adds a launcher entry and icons under <code>~/.local/share</code> so your dock can find it. Delete those files to remove them. If it complains about FUSE on a recent distribution, install FUSE 2 (<code>libfuse2</code>), or run it with <code>--appimage-extract-and-run</code>.</p>
<h2 id="verify">Verify the download</h2>
<p>Expected SHA-256 for <code>{linux.file}</code>:</p>
<pre><code>{linux.sha256}</code></pre>
<p>Check your copy against it:</p>
<p>Expected SHA-256 for each file:</p>
<pre><code>{linux.sha256} {linux.file}
{deb.sha256} {deb.file}
{win.sha256} {win.file}</code></pre>
<p>Check your copy against them (on Linux, in the folder you downloaded to):</p>
<pre><code>echo "{linux.sha256} {linux.file}" | sha256sum -c</code></pre>
<p>On Windows, use <code>Get-FileHash {win.file}</code> in PowerShell.</p>
<h2 id="unsigned">Unsigned builds</h2>
<p>These builds are not code-signed. On Windows and macOS, once those ship, you will likely see a warning from SmartScreen or Gatekeeper. The checksum above is how you confirm you have the file that was published.</p>
<p>These builds are not code-signed. On Windows you will likely see a SmartScreen warning, and macOS will show a Gatekeeper one once it ships. The checksum above is how you confirm you have the file that was published.</p>
<h2>Your notes after installing</h2>
<p>On first launch you either use <code>~/Notes</code> or choose a folder (an existing Obsidian vault works). Ink reads and writes Markdown files there and keeps its own search index, trash and templates in a hidden <code>.notes</code> folder inside it. Nothing is sent anywhere. <a href={href('/docs/')}>Read the docs</a>.</p>
@@ -115,6 +139,7 @@ const soon: PlatformId[] = ['windows', 'macos'];
.card h2 { font-size: 1.6rem; }
.main { border-color: var(--blush-line); background: linear-gradient(160deg, var(--blush-soft), var(--surface) 70%); }
.main .btn { justify-self: start; margin-top: 0.5rem; }
.main .file + .btn { margin-top: 0.8rem; }
.file { font: 0.76rem/1.4 var(--f-mono); color: var(--muted); overflow-wrap: anywhere; }
.top { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-bottom: 0.4rem; }
.os-ic { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 12px; background: var(--hover); border: 1px solid var(--line); color: var(--ink-2); }
@@ -128,7 +153,7 @@ const soon: PlatformId[] = ['windows', 'macos'];
.soon p:not(.meta) { color: var(--ink-2); }
.card.yours:not(.main) { border-color: var(--line-2); }
@media (min-width: 900px) {
.platforms { grid-template-columns: 1.3fr 1fr 1fr; align-items: stretch; }
.platforms { grid-template-columns: 1.3fr 1.3fr 1fr; align-items: stretch; }
}
.steps-sec { padding-top: clamp(3.5rem, 7vw, 5.5rem); }
+14 -4
View File
@@ -56,6 +56,16 @@ const groups: Group[] = [
{ title: 'Columns and cards', text: 'Each heading is a column and each to-do is a card. Drag cards and columns around.' },
],
},
{
id: 'databases', eyebrow: 'Databases', title: 'Structured notes, still plain files',
intro: 'A database is a page whose sub-pages are records. Properties are ordinary front matter, so every record stays a normal note.',
screen: 'databases',
items: [
{ title: 'Typed properties', text: 'Text, number, select, multi-select, date, checkbox, people, files, URL, relation and formula. Numbers can show as a progress bar.' },
{ title: 'Six views', text: 'Table, board, calendar, gallery, timeline and list, each with its own sort, filters and visible properties.' },
{ title: 'Plain Markdown', text: 'The schema lives in the database page, values in each record\'s front matter. Nothing is locked in a hidden store.' },
],
},
{
id: 'organising', eyebrow: 'Organising', title: 'Find things, keep things',
intro: 'Tags, search and a palette for everything else.',
@@ -81,9 +91,9 @@ const groups: Group[] = [
{
id: 'comfort', eyebrow: 'Comfort', title: 'Make it fit how you read',
intro: 'Quiet by default, adjustable when you want it.',
screen: 'settings',
screen: 'themes',
items: [
{ title: 'Themes', text: 'Dark, light, or follow the system.' },
{ title: 'Themes and accents', text: 'Dark, light, or follow the system, with color themes such as Midnight, Nord, Forest, Sepia, Catppuccin and AMOLED, and five accent colors.' },
{ title: 'Text size and page width', text: 'Both adjustable, plus a focus mode and an outline panel for long pages.' },
{ title: 'Spellcheck', text: 'Pick the languages you write in.' },
],
@@ -93,13 +103,13 @@ const groups: Group[] = [
<Base
title="Features"
current="/features/"
description="A tour of Ink: block editor, wiki links and embeds, canvas, kanban boards, graph view, tags, search, version history and export."
description="A tour of Ink: block editor, wiki links and embeds, canvas, kanban boards, databases, graph view, tags, search, version history and export."
>
<div class="head-bg">
<section class="wrap head">
<p class="eyebrow">Features</p>
<h1>Everything Ink does</h1>
<p class="lede">A tour of what ships in version 0.1. Screenshots follow your system's light or dark theme, like the app.</p>
<p class="lede">A tour of what ships in version 1.0 beta. Screenshots follow your system's light or dark theme, like the app.</p>
</section>
</div>
+1 -1
View File
@@ -95,7 +95,7 @@ import { href } from '../lib/url';
<div class="cta-in">
<img src={href('/icon-192.png')} width="72" height="72" alt="" class="cta-icon" loading="lazy" decoding="async" />
<h2>Give your notes a quiet place to live.</h2>
<p class="lede">Ink {config.version} is available now for Linux. Windows and macOS builds are on the way.</p>
<p class="lede">Ink {config.version} is available now for Linux and Windows. macOS is on the way.</p>
<DownloadButton note={false} center />
<p class="fine">Builds are unsigned for now. <a href={href('/download/')}>Details and checksum</a></p>
</div>
+16 -4
View File
@@ -84,18 +84,30 @@ if (/mac|iphone|ipad/i.test(navigator.platform || navigator.userAgent)) {
}
/* Download links are direct file links in the HTML. Visitors on Windows, macOS or a phone can't use the
Linux AppImage yet, so for them (and only them) the same buttons point at the page that explains why. */
Linux AppImage, so for them (and only them) the same buttons point at the downloads page. */
const os = detectOS();
if (os === 'windows' || os === 'macos' || os === 'mobile') {
if (os === 'windows') {
document.querySelectorAll<HTMLElement>('[data-download]').forEach((box) => {
const a = box.querySelector<HTMLAnchorElement>('[data-dl-link]');
if (a && box.dataset.winUrl) {
a.href = box.dataset.winUrl;
a.download = box.dataset.winFile ?? '';
}
const label = box.querySelector<HTMLElement>('[data-download-label]');
const note = box.querySelector<HTMLElement>('[data-download-note]');
if (label) label.textContent = 'Download for Windows';
if (note) note.textContent = `${box.dataset.winMeta} · Unsigned build`;
});
} else if (os === 'macos' || os === 'mobile') {
document.querySelectorAll<HTMLAnchorElement>('[data-dl-link]').forEach((a) => {
if (a.dataset.info) a.href = a.dataset.info;
a.removeAttribute('download');
});
const what = { windows: 'The Windows version is not ready yet.', macos: 'The macOS version is not ready yet.', mobile: 'Ink is a desktop app. Open this page on your computer to download it.' }[os];
const what = { macos: 'The macOS version is not ready yet.', mobile: 'Ink is a desktop app. Open this page on your computer to download it.' }[os];
document.querySelectorAll<HTMLElement>('[data-download]').forEach((box) => {
const label = box.querySelector<HTMLElement>('[data-download-label]');
const note = box.querySelector<HTMLElement>('[data-download-note]');
if (label) label.textContent = 'Download options';
if (note) note.textContent = `${what} The Linux AppImage is available now.`;
if (note) note.textContent = `${what} Linux and Windows builds are available now.`;
});
}