diff --git a/README.md b/README.md index 9bb1b53..9e08873 100644 --- a/README.md +++ b/README.md @@ -25,47 +25,141 @@

- - - Lucid: a page of notes with a diagram, the list of changes on the right and the input box at the bottom - + Lucid in a dark app with a light page: a page of notes with a diagram, the list of changes on the right and the input box at the bottom

## Why I made it -I'm the author. For a while I couldn't type comfortably. I wasn't working, but I still wanted to use the time to keep learning, -so I went back to system design and other concepts I wanted to revise. Taking notes was the hard part. +For a while I couldn't type comfortably, so I built Lucid for myself: a productivity and accessibility tool for taking notes and +learning system design by voice, or with one hand. -There are good open-source tools that turn voice into text, and others that draw diagrams. I couldn't find one project where I -could just speak and get something *structured*: sections, a list of options with the rejected ones crossed out, a table, a -diagram. I like minimal themes, so I built the one I wanted, for myself first. +I learn by thinking out loud, and I want to see it as I say it: the diagram when I explain a system, the notes taking shape when I +revise, and a way to cut what I got wrong and say it again. There are good open-source tools that turn voice into text, and others +that draw diagrams, but I couldn't find one project where I could simply speak and get something *structured*, entirely on my own +computer. I like minimal themes, so I built the one I wanted, for myself first. This is the first version, and it doesn't listen by itself. You dictate with an app that pastes text, like [Wispr Flow](https://wisprflow.ai), [OpenWhispr](https://github.com/OpenWhispr/openwhispr) or [VoiceNative](https://github.com/triggeredcode/voicenative) (my own open-source voice-to-text for macOS), and Lucid turns what lands in the box into the page. A fully local voice pipeline will take me some time to get right; you can start with those. -The hardest part was the drawing: a diagram engine and a table engine good enough to build from speech. Command mode edits the -page by talking to it, and the history keeps what you said, what the model understood and what it changed, so you can always see -why a page looks the way it does. The model runs on your own computer. I hold it to a bar of 90% on my own evaluation -([docs/evals.md](docs/evals.md)), and Edit mode and undo are for the rest. - -Where it might go, in no particular order: a faster, more live mode with smaller local models (5B to 8B, maybe Gemma) or with -cloud models through Ollama; fine-tuned models; and fields other than system design. Voice stays in its own project, -VoiceNative, and I may integrate the two without merging them. These are ideas, not promises. - -## What you get - -- **Speech becomes structure, not a wall of text.** Dictation gives you the words. Lucid gives you the document: an outline of - sections, a list of options with the rejected ones crossed out, a table that compares them, a diagram of how the parts connect. -- **By voice or by keyboard.** Notes, commands (*"cross out Memcached"*, *"add a CDN in front of the web server"*, *"undo"*), - diagrams and tables can all be spoken. See [dictation](docs/dictation.md) for the apps and for using Lucid with one hand. -- **Made for system design, open to other fields.** The note formats are plain data: a concept note, a system design, a - comparison, a blank page, and one more entry for anything else. [See them](docs/formats.md). -- **You stay in charge.** Every change by the model is one undo step, any earlier change can be reverted on its own, and your - words are never lost. -- **Local and private.** No account, no cloud service. The model runs in [LM Studio](https://lmstudio.ai) on your computer. -- **A toolbox for AI agents.** An MCP server lets Claude Code, or a model in LM Studio's chat, read and edit the same pages. +## How it works + +```mermaid +flowchart LR + you["You, by voice or keyboard"] --> box["The input box"] + box --> model["A local model in LM Studio
(for example Qwen 3.8)
sees the page and its history"] + model --> page["Blocks on your page:
headings, lists, tables, diagrams"] + page --> history["The history keeps what you said,
what was understood, what changed"] +``` + +The model runs on your computer and never leaves it. It always has the page in front of it, so it does not only add text: it +puts what you said under the right heading and builds the right kind of block. Every change is exactly one undo step, and your +words are never lost. + +## The two parts I like most + +### Diagram mode: keywords, connections, editing + +A diagram of ten parts is hard to say in one breath, so there is a guided builder with three phases. First the **keywords**: say +the parts of the system. Then the **connections**: say how they connect, one at a time. Then **editing**: everything has a number, +so a fix is one short sentence (*"connection 3 is reads and writes"*, *"rename 3 to Cache"*). Say *"done"* and the diagram goes +onto your page. A database is drawn as a cylinder, a queue as a pipe, a CDN as a cloud, a load balancer as a hexagon. + + + + + + + +
+ + + Diagram mode, phase 1: the keywords, the parts of the system, numbered + +
1. Keywords +
+ + + Diagram mode, phase 2: the connections between the parts, numbered + +
2. Connections +
+ + + Diagram mode, phase 3: editing by number + +
3. Editing +
+ +### Command mode: talk to the page + +Because the model has the whole page in context, you can talk about it and it edits the page for you: *"add a cache in front of +the database"*, *"cross out Memcached, we are choosing the local LRU"*, *"add a row for cost to this table"*, *"undo"*. Crossing +out keeps the idea on the page, struck through, so you can still see what you decided against. Hold the right ⌥ key and +everything you say is one command, which you can do with one hand. + +## History, edit mode and how well it works + +The history keeps what you said, what the model understood and what it changed, so you can always see why a page looks the way +it does, take back any single change (not just the last one), or edit what you said and send it again. When the model gets +something wrong there is **Edit mode**: you just type in the page. + +

+ + + Block numbers in the page margin; block 4, a diagram, lists its numbered parts and connections + +

+ +I test the model against my own set of cases with the real model ([docs/evals.md](docs/evals.md)) and hold it to a bar of 90%. +In my latest run it got 98 of 99 cases right. To get the most from it: say things in short pieces, use diagram mode for diagrams +and command mode to change the page, and look at the history whenever something surprises you. + +## Keep your notes + +- **PDF**, typeset on your computer, with real diagrams. Settings → **PDF look** sets the paper, the type, the margins and an + accent colour, and you can edit the template itself. +- **A whole note**, with its history, to back up or move to another computer, and **Markdown**. +- **Three note formats** to start from (a concept note, a system design, a comparison) or a blank page; formats are plain data, so + one of your own is one more entry. [See them](docs/formats.md). +- **The page has its own light and dark**, apart from the app's, so a dark app can show a light page, like paper. + + + + + + + + + +
+ + + The New page dialog: three note formats and a small Blank page button + +
Start from a format, or a blank page +
+ + + The Export note dialog + +
Export a whole note, with its history +
+ A light app with a dark page +
The page has a look of its own: a dark page in a light app (and, at the top of this page, a light page in a dark app) +
+ +## Built-in MCP server + +Lucid has a built-in MCP server, so an AI agent such as Claude Code can read your pages and add to them. Everything an agent does +is one undo step and shows up in the history as the agent's. The tools and the set-up are in [docs/mcp.md](docs/mcp.md). + +## Where it might go + +Ideas, not promises: smaller, faster local models (5B to 8B, maybe Gemma) or cloud models through Ollama for a more live mode, +models fine-tuned for notes, and other subjects than system design. Voice stays in its own project, VoiceNative, and I may +integrate the two without merging them. ## Install diff --git a/docs/features.md b/docs/features.md index 15f3934..32cd1e7 100644 --- a/docs/features.md +++ b/docs/features.md @@ -59,14 +59,14 @@ A diagram of ten parts is hard to say in one breath, so there is a guided builde cylinder, a queue as a pipe, a CDN as a cloud, a load balancer as a hexagon. Big diagrams stay readable in the page and in the PDF. Tables have a builder in the same spirit. -![The diagram builder: numbered components and connections beside the drawing](images/diagram-builder.png) +![The diagram builder: numbered components and connections beside the drawing](images/readme/diagram-builder.png) ## Say which block you mean Every block has a light number in the margin (B1, B2, …). Click one to see its parts, then say *"in block 3, add a row for cost"* or *"cross out item 2 of block 6"*. The numbers can be hidden in Settings. -![Block numbers in the page margin; block 4, a diagram, lists its numbered parts and connections](images/block-numbers.png) +![Block numbers in the page margin; block 4, a diagram, lists its numbered parts and connections](images/readme/block-numbers.png) ## History you can trust @@ -83,7 +83,7 @@ changes to other blocks alone. - **Markdown** (diagrams as Mermaid blocks). - **A `.lucid` file**, the whole note with its history, to back up, move to another computer or share. -![A page of the exported PDF: a numbered section, a diagram and a footer](images/pdf-page.png) +![A page of the exported PDF: a numbered section, a diagram and a footer](images/readme/pdf-page.png) Light and dark themes, three typefaces for the page on screen, and a zoom for the page (⌘+, ⌘−, ⌘0) are in the app. Pages are plain files in `~/Lucid` (one JSON file per page). diff --git a/docs/images/block-numbers-dark.png b/docs/images/block-numbers-dark.png new file mode 100644 index 0000000..93f1169 Binary files /dev/null and b/docs/images/block-numbers-dark.png differ diff --git a/docs/images/builder-connections-dark.png b/docs/images/builder-connections-dark.png new file mode 100644 index 0000000..0e90016 Binary files /dev/null and b/docs/images/builder-connections-dark.png differ diff --git a/docs/images/builder-connections.png b/docs/images/builder-connections.png new file mode 100644 index 0000000..e916cbc Binary files /dev/null and b/docs/images/builder-connections.png differ diff --git a/docs/images/builder-edit-dark.png b/docs/images/builder-edit-dark.png new file mode 100644 index 0000000..334a2d5 Binary files /dev/null and b/docs/images/builder-edit-dark.png differ diff --git a/docs/images/builder-edit.png b/docs/images/builder-edit.png new file mode 100644 index 0000000..0f2ec69 Binary files /dev/null and b/docs/images/builder-edit.png differ diff --git a/docs/images/builder-keywords-dark.png b/docs/images/builder-keywords-dark.png new file mode 100644 index 0000000..c3342b4 Binary files /dev/null and b/docs/images/builder-keywords-dark.png differ diff --git a/docs/images/builder-keywords.png b/docs/images/builder-keywords.png new file mode 100644 index 0000000..78e6bdc Binary files /dev/null and b/docs/images/builder-keywords.png differ diff --git a/docs/images/export-note-dark.png b/docs/images/export-note-dark.png new file mode 100644 index 0000000..f6ea3dc Binary files /dev/null and b/docs/images/export-note-dark.png differ diff --git a/docs/images/export-note.png b/docs/images/export-note.png new file mode 100644 index 0000000..7000006 Binary files /dev/null and b/docs/images/export-note.png differ diff --git a/docs/images/page-look-dark-page.png b/docs/images/page-look-dark-page.png new file mode 100644 index 0000000..ab07b93 Binary files /dev/null and b/docs/images/page-look-dark-page.png differ diff --git a/docs/images/page-look-light-page.png b/docs/images/page-look-light-page.png new file mode 100644 index 0000000..2da6539 Binary files /dev/null and b/docs/images/page-look-light-page.png differ diff --git a/docs/images/readme/block-numbers-dark.png b/docs/images/readme/block-numbers-dark.png new file mode 100644 index 0000000..94e9d29 Binary files /dev/null and b/docs/images/readme/block-numbers-dark.png differ diff --git a/docs/images/readme/block-numbers.png b/docs/images/readme/block-numbers.png new file mode 100644 index 0000000..54750be Binary files /dev/null and b/docs/images/readme/block-numbers.png differ diff --git a/docs/images/readme/builder-connections-dark.png b/docs/images/readme/builder-connections-dark.png new file mode 100644 index 0000000..e2b586c Binary files /dev/null and b/docs/images/readme/builder-connections-dark.png differ diff --git a/docs/images/readme/builder-connections.png b/docs/images/readme/builder-connections.png new file mode 100644 index 0000000..53a5dad Binary files /dev/null and b/docs/images/readme/builder-connections.png differ diff --git a/docs/images/readme/builder-edit-dark.png b/docs/images/readme/builder-edit-dark.png new file mode 100644 index 0000000..e9f262b Binary files /dev/null and b/docs/images/readme/builder-edit-dark.png differ diff --git a/docs/images/readme/builder-edit.png b/docs/images/readme/builder-edit.png new file mode 100644 index 0000000..2b8d886 Binary files /dev/null and b/docs/images/readme/builder-edit.png differ diff --git a/docs/images/readme/builder-keywords-dark.png b/docs/images/readme/builder-keywords-dark.png new file mode 100644 index 0000000..91d0ad4 Binary files /dev/null and b/docs/images/readme/builder-keywords-dark.png differ diff --git a/docs/images/readme/builder-keywords.png b/docs/images/readme/builder-keywords.png new file mode 100644 index 0000000..9b17d88 Binary files /dev/null and b/docs/images/readme/builder-keywords.png differ diff --git a/docs/images/readme/diagram-builder-dark.png b/docs/images/readme/diagram-builder-dark.png new file mode 100644 index 0000000..6e2d44f Binary files /dev/null and b/docs/images/readme/diagram-builder-dark.png differ diff --git a/docs/images/readme/diagram-builder.png b/docs/images/readme/diagram-builder.png new file mode 100644 index 0000000..20eb6bc Binary files /dev/null and b/docs/images/readme/diagram-builder.png differ diff --git a/docs/images/readme/export-note-dark.png b/docs/images/readme/export-note-dark.png new file mode 100644 index 0000000..28ccb94 Binary files /dev/null and b/docs/images/readme/export-note-dark.png differ diff --git a/docs/images/readme/export-note.png b/docs/images/readme/export-note.png new file mode 100644 index 0000000..666913a Binary files /dev/null and b/docs/images/readme/export-note.png differ diff --git a/docs/images/readme/lucid-dark.png b/docs/images/readme/lucid-dark.png new file mode 100644 index 0000000..157721d Binary files /dev/null and b/docs/images/readme/lucid-dark.png differ diff --git a/docs/images/readme/lucid-light.png b/docs/images/readme/lucid-light.png new file mode 100644 index 0000000..bf93a7a Binary files /dev/null and b/docs/images/readme/lucid-light.png differ diff --git a/docs/images/readme/new-page-dark.png b/docs/images/readme/new-page-dark.png new file mode 100644 index 0000000..c1e1aa1 Binary files /dev/null and b/docs/images/readme/new-page-dark.png differ diff --git a/docs/images/readme/new-page.png b/docs/images/readme/new-page.png new file mode 100644 index 0000000..e1ce684 Binary files /dev/null and b/docs/images/readme/new-page.png differ diff --git a/docs/images/readme/page-look-dark-page.png b/docs/images/readme/page-look-dark-page.png new file mode 100644 index 0000000..862a01f Binary files /dev/null and b/docs/images/readme/page-look-dark-page.png differ diff --git a/docs/images/readme/page-look-light-page.png b/docs/images/readme/page-look-light-page.png new file mode 100644 index 0000000..0b404c2 Binary files /dev/null and b/docs/images/readme/page-look-light-page.png differ diff --git a/docs/images/readme/pdf-page.png b/docs/images/readme/pdf-page.png new file mode 100644 index 0000000..388bc1e Binary files /dev/null and b/docs/images/readme/pdf-page.png differ diff --git a/scripts/frame-images.mjs b/scripts/frame-images.mjs new file mode 100644 index 0000000..98885bb --- /dev/null +++ b/scripts/frame-images.mjs @@ -0,0 +1,59 @@ +// Makes the framed copies of the window pictures (docs/images/*.png -> docs/images/readme/*.png) that the README and the docs +// show: rounded corners and a thin edge, so a picture stands out on the page it sits on. A light picture on GitHub's white page +// gets a grey edge, and a dark one, which would melt into GitHub's dark page, gets a light edge (the edge is made to be seen on +// either page). The website frames its own pictures in CSS and uses the plain ones. +// +// node scripts/frame-images.mjs (needs Python with Pillow, as format-samples.mjs does) +import { execFileSync } from "node:child_process"; +import { mkdirSync, readdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const from = join(root, "docs/images"); +const to = join(from, "readme"); +mkdirSync(to, { recursive: true }); + +// The social preview is a card on its own, not a window picture. +const skip = new Set(["social-preview.png", "poster.png"]); +const files = readdirSync(from).filter((f) => f.endsWith(".png") && !skip.has(f)); +// The first picture of the README, a dark app with a light page, has a white edge: white, the dark app, the light page. +const white = ["page-look-light-page.png"]; + +execFileSync( + "python3", + [ + "-c", + [ + "import sys, os", + "from PIL import Image, ImageChops, ImageDraw, ImageStat", + "src, dst, white = sys.argv[1], sys.argv[2], sys.argv[3].split(',')", + "for name in sys.argv[4:]:", + " im = Image.open(os.path.join(src, name)).convert('RGB')", + " w, h = im.size", + " r = max(10, round(w / 150))", + " bw = max(2, round(w / 700))", + // Light or dark is read from the picture's own edge, not its name: a light app can hold a dark page. + " strips = [im.crop((0, 0, w, 6)), im.crop((0, h - 6, w, h)), im.crop((0, 0, 6, h)), im.crop((w - 6, 0, w, h))]", + " lum = sum(ImageStat.Stat(s.convert('L')).mean[0] for s in strips) / 4", + " edge = (214, 222, 232, 255) if lum < 110 else (170, 178, 189, 255)", + " if name in white: edge = (255, 255, 255, 255)", + " k = 4", + " def rounded(inset, radius):", + " m = Image.new('L', (w * k, h * k), 0)", + " ImageDraw.Draw(m).rounded_rectangle([inset * k, inset * k, (w - inset) * k - 1, (h - inset) * k - 1], radius=radius * k, fill=255)", + " return m.resize((w, h), Image.LANCZOS)", + " outer, inner = rounded(0, r), rounded(bw, max(r - bw, 1))", + " ring = ImageChops.subtract(outer, inner)", + " out = Image.composite(Image.new('RGBA', (w, h), edge), im.convert('RGBA'), ring)", + " out.putalpha(outer)", + " out.save(os.path.join(dst, name), optimize=True)", + " print(name, 'white' if name in white else 'dark' if lum < 110 else 'light')", + ].join("\n"), + from, + to, + white.join(","), + ...files, + ], + { stdio: "inherit" }, +); diff --git a/scripts/screenshots.mjs b/scripts/screenshots.mjs index b4b3aee..168715b 100644 --- a/scripts/screenshots.mjs +++ b/scripts/screenshots.mjs @@ -1,7 +1,8 @@ /* global window, document */ -// Makes the window pictures of the README and the site (docs/images/lucid-light.png, lucid-dark.png, diagram-builder.png, -// diagram-builder-dark.png, block-numbers.png) with the real app: a library of three pages, the first with a diagram and some -// history, the diagram builder half way through a diagram, and block 4 opened in the margin. +// Makes the window pictures of the README, the docs and the site (docs/images) with the real app, in the light and the dark +// theme: a library of three pages, the first with a diagram and some history; the diagram builder in its three phases (the +// keywords, the connections, edits by number: the builder card of each); the New page and Export note dialogs; block 4 opened in the margin; and the page +// with a look of its own, apart from the app's. Then frame-images.mjs makes the framed copies the README and the docs show. // // pnpm build && node scripts/screenshots.mjs // @@ -170,20 +171,23 @@ try { }); return png; }; - const resized = async (png, name) => { - // The window is as big as the screen, at the screen's density: bring every picture to 1800 pixels wide. - const data = await app.evaluate(({ nativeImage }, base64) => { - const image = nativeImage.createFromBuffer(Buffer.from(base64, "base64")); - const size = image.getSize(); - return image - .resize({ - width: 1800, - height: Math.round((1800 * size.height) / size.width), - quality: "best", - }) - .toPNG() - .toString("base64"); - }, png.toString("base64")); + const resized = async (png, name, wide = 1800) => { + // The window is as big as the screen, at the screen's density: bring every picture to 1800 pixels wide (a crop, less). + const data = await app.evaluate( + ({ nativeImage }, { base64, wide }) => { + const image = nativeImage.createFromBuffer(Buffer.from(base64, "base64")); + const size = image.getSize(); + return image + .resize({ + width: wide, + height: Math.round((wide * size.height) / size.width), + quality: "best", + }) + .toPNG() + .toString("base64"); + }, + { base64: png.toString("base64"), wide }, + ); writeFileSync(join(out, name), Buffer.from(data, "base64")); console.log(name); }; @@ -211,46 +215,91 @@ try { } await resized(await shoot(), scheme === "light" ? "lucid-light.png" : "lucid-dark.png"); - // The diagram builder, half way through a diagram. + const dark = scheme === "dark"; + const named = (name) => (dark ? `${name}-dark.png` : `${name}.png`); + + // The diagram builder, in its three phases: the keywords, the connections, then edits by number. const bar = page.getByLabel("Dictate or type your notes"); + const say = async (text) => { + await bar.fill(text); + await bar.press("Enter"); + }; await bar.focus(); await page.keyboard.press("Meta+4"); - await bar.fill( - "add keywords: browser, CDN, load balancer, order service, Redis, PostgreSQL, Kafka", - ); - await bar.press("Enter"); - await bar.fill("connections"); - await bar.press("Enter"); - for (const line of ["1 to 2", "1 to 3", "3 to 4", "4 to 5", "4 to 6", "4 to 7"]) { - await bar.fill(line); - await bar.press("Enter"); - } + await say("add keywords: browser, CDN, load balancer, order service, Redis, PostgreSQL, Kafka"); await page.locator(".wb-svg svg g.node").first().waitFor({ timeout: 20_000 }); await page.waitForTimeout(1200); - await resized( - await shoot(), - scheme === "light" ? "diagram-builder.png" : "diagram-builder-dark.png", - ); + const card = page.locator(".workbench"); + await resized(await card.screenshot(), named("builder-keywords"), 1000); + await say("connections"); + for (const line of ["1 to 2", "1 to 3", "3 to 4", "4 to 5", "4 to 6", "4 to 7"]) + await say(line); + await page.waitForTimeout(1200); + await resized(await shoot(), named("diagram-builder")); + await resized(await card.screenshot(), named("builder-connections"), 1000); + await say("edit"); + for (const line of [ + "connection 1 is static files", + "connection 4 is cache reads", + "rename 7 to Event bus", + ]) + await say(line); + await page.waitForTimeout(1200); + await resized(await card.screenshot(), named("builder-edit"), 1000); await page.getByRole("button", { name: "Cancel" }).click(); await page.waitForTimeout(500); + // The New page dialog, as the guide shows it. await page.getByRole("button", { name: "New from a format…" }).click(); await page.locator(".np-dialog").waitFor(); await page.waitForTimeout(500); - await page.locator(".np-dialog").screenshot({ - path: join(out, scheme === "light" ? "new-page.png" : "new-page-dark.png"), - }); + await page.locator(".np-dialog").screenshot({ path: join(out, named("new-page")) }); await page.keyboard.press("Escape"); await page.waitForTimeout(300); - if (scheme === "light") { - // Block 4 opened in the margin. - await page.locator(".block-no").nth(3).click(); - await page.waitForTimeout(600); - await resized(await shoot(), "block-numbers.png"); - await page.keyboard.press("Escape"); - await page.waitForTimeout(300); + + // The Export note dialog. + await page.getByRole("button", { name: "Export note" }).click(); + const exporting = page.getByRole("dialog", { name: "Export note" }); + await exporting.waitFor(); + await page.waitForTimeout(500); + await exporting.screenshot({ path: join(out, named("export-note")) }); + await page.keyboard.press("Escape"); + await page.waitForTimeout(300); + + // Block 4 opened in the margin. + await page.locator(".block-no").nth(3).click(); + await page.waitForTimeout(600); + await resized(await shoot(), named("block-numbers")); + await page.keyboard.press("Escape"); + await page.waitForTimeout(300); + + // The page's own look, the other way round from the app's. + const look = dark ? "light" : "dark"; + const switchLook = page.locator(".page-look"); + for (let i = 0; i < 3; i++) { + if ( + ((await switchLook.getAttribute("aria-label")) ?? "").startsWith( + `Look of the page: ${look}.`, + ) + ) + break; + await switchLook.click(); + } + await page.waitForTimeout(800); + await resized(await shoot(), `page-look-${look}-page.png`); + for (let i = 0; i < 3; i++) { + if ( + ((await switchLook.getAttribute("aria-label")) ?? "").startsWith( + "Look of the page: like the app.", + ) + ) + break; + await switchLook.click(); } } } finally { await app.close(); } + +// The framed copies the README and the docs show. +await import("./frame-images.mjs"); diff --git a/site/index.html b/site/index.html index 87027d5..fb21108 100644 --- a/site/index.html +++ b/site/index.html @@ -182,9 +182,9 @@

## The database

Why it exists

Made so you can take notes without typing.

- For a while I couldn't type comfortably, but I still wanted to keep learning system - design. A dictation app gives you a wall of text, so I built Lucid to give me the - structure. + For a while I couldn't type comfortably, so I built Lucid for myself, as a + productivity and accessibility tool. A dictation app gives you a wall of text, and + Lucid gives you the structure.