## 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.
+
+
+
+
+
+
+
+
+ 1. Keywords
+
+
+
+
+
+
+ 2. Connections
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+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.
+
+
+
+
+
+
+
+
+ Start from a format, or a blank page
+
+
+
+
+
+
+ Export a whole note, with its history
+
+
+
+
+
+ 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.
-
+
## 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.
-
+
## 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.
-
+
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.