From 435194f7c2f1ed009e9ada47bdcf7b4780807c60 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 5 Oct 2026 23:21:35 -0400 Subject: [PATCH] =?UTF-8?q?M0.9=E2=80=93M0.12:=20CLI,=20MCP=20server,=20xt?= =?UTF-8?q?ask=20(layers/assets/parity/ci),=20icon=20placeholder,=20AGENTS?= =?UTF-8?q?/ATTRIBUTION/NOTICE,=20protocol=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 62 +++++ ATTRIBUTION.md | 51 ++++ CLAUDE.md | 62 +++++ Cargo.lock | 93 ++++++- Cargo.toml | 2 +- NOTICE | 28 +++ apps/wordcraft-cli/Cargo.toml | 21 ++ apps/wordcraft-cli/src/main.rs | 184 ++++++++++++++ apps/wordcraft/src/main.rs | 14 +- .../128x128/apps/ai.storyteller.wordcraft.png | Bin 0 -> 3350 bytes .../16x16/apps/ai.storyteller.wordcraft.png | Bin 0 -> 604 bytes .../24x24/apps/ai.storyteller.wordcraft.png | Bin 0 -> 1001 bytes .../256x256/apps/ai.storyteller.wordcraft.png | Bin 0 -> 6388 bytes .../32x32/apps/ai.storyteller.wordcraft.png | Bin 0 -> 1255 bytes .../48x48/apps/ai.storyteller.wordcraft.png | Bin 0 -> 1569 bytes .../512x512/apps/ai.storyteller.wordcraft.png | Bin 0 -> 13836 bytes .../64x64/apps/ai.storyteller.wordcraft.png | Bin 0 -> 1904 bytes .../apps/ai.storyteller.wordcraft.svg | 16 ++ assets/app-icon/wordcraft-1024.png | Bin 0 -> 30941 bytes assets/app-icon/wordcraft-macos-512.png | Bin 0 -> 12242 bytes assets/app-icon/wordcraft.icns | Bin 0 -> 110290 bytes assets/app-icon/wordcraft.ico | Bin 0 -> 18378 bytes assets/app-icon/wordcraft.svg | 16 ++ crates/docx/Cargo.toml | 21 ++ crates/docx/src/lib.rs | 1 + crates/mcp/Cargo.toml | 16 ++ crates/mcp/src/backend.rs | 107 ++++++++ crates/mcp/src/lib.rs | 65 +++++ crates/mcp/src/server.rs | 168 +++++++++++++ crates/mcp/src/tests.rs | 69 +++++ crates/mcp/src/tools.rs | 188 ++++++++++++++ docs/control-protocol.md | 34 +++ docs/mcp.md | 32 +++ docs/parity.md | 238 ++++++++++++++++++ packaging/icons.sh | 58 +++++ xtask/src/assets.rs | 16 +- xtask/src/ico.rs | 2 +- xtask/src/layers.rs | 86 +++---- xtask/src/main.rs | 6 +- xtask/src/parity.rs | 107 ++------ 40 files changed, 1612 insertions(+), 151 deletions(-) create mode 100644 AGENTS.md create mode 100644 ATTRIBUTION.md create mode 100644 CLAUDE.md create mode 100644 NOTICE create mode 100644 apps/wordcraft-cli/Cargo.toml create mode 100644 apps/wordcraft-cli/src/main.rs create mode 100644 assets/app-icon/hicolor/128x128/apps/ai.storyteller.wordcraft.png create mode 100644 assets/app-icon/hicolor/16x16/apps/ai.storyteller.wordcraft.png create mode 100644 assets/app-icon/hicolor/24x24/apps/ai.storyteller.wordcraft.png create mode 100644 assets/app-icon/hicolor/256x256/apps/ai.storyteller.wordcraft.png create mode 100644 assets/app-icon/hicolor/32x32/apps/ai.storyteller.wordcraft.png create mode 100644 assets/app-icon/hicolor/48x48/apps/ai.storyteller.wordcraft.png create mode 100644 assets/app-icon/hicolor/512x512/apps/ai.storyteller.wordcraft.png create mode 100644 assets/app-icon/hicolor/64x64/apps/ai.storyteller.wordcraft.png create mode 100644 assets/app-icon/hicolor/scalable/apps/ai.storyteller.wordcraft.svg create mode 100644 assets/app-icon/wordcraft-1024.png create mode 100644 assets/app-icon/wordcraft-macos-512.png create mode 100644 assets/app-icon/wordcraft.icns create mode 100644 assets/app-icon/wordcraft.ico create mode 100644 assets/app-icon/wordcraft.svg create mode 100644 crates/docx/Cargo.toml create mode 100644 crates/docx/src/lib.rs create mode 100644 crates/mcp/Cargo.toml create mode 100644 crates/mcp/src/backend.rs create mode 100644 crates/mcp/src/lib.rs create mode 100644 crates/mcp/src/server.rs create mode 100644 crates/mcp/src/tests.rs create mode 100644 crates/mcp/src/tools.rs create mode 100644 docs/control-protocol.md create mode 100644 docs/mcp.md create mode 100644 docs/parity.md create mode 100755 packaging/icons.sh diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..c4125c1c7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,62 @@ +# WordCraft — instructions for agents + +WordCraft is a clean-room, open-source, Rust-native word processor targeting Microsoft Word parity — and going further on speed, openness and agent control. It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../printcraft` (Acrobat), `../effectcraft` (After Effects), `../designcraft` (InDesign). Shared rules and learnings: [`storytold/craftrules`](https://github.com/storytold/craftrules) (`../../craftrules`) — read its `AGENTS.md`. + +## Start every session here +1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` and the relevant `plan/architecture.md` section. `plan/` is gitignored (local only); if it's missing, start from `ROADMAP.md` and `docs/parity.md`. +2. Follow the autonomous operation protocol (`plan/execution-plan.md` §5). Don't stop to ask unless a decision is genuinely the owner's (licensing, publishing, pushing to new remotes, secrets). + +## Never crash +People trust WordCraft with their writing; a crash loses their work. **This outranks feature work**: never ship a feature through a panic path, and fix a crash before building on top of it. Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md). +- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). +- **Errors are `Result`** through the crate's error type and `?`. An unfinished feature returns an error or is disabled; it never panics. +- **Input-derived numbers are hostile** (files, commands, MCP/control params): `get()` instead of `[i]`, slice strings only at char boundaries, checked/saturating arithmetic, clamp sizes, cap allocations, bound recursion. +- **Last-resort guard:** `Session::run` turns an escaped panic into an error and restores the document. It's a safety net, not a licence. +- **Prove it:** every crash fix lands with a regression test. `crates/engine/src/tests.rs::hostile_params_never_panic` runs every command with junk params — keep it green. +- Production crate roots carry `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`; new crates start with it. Tests may `unwrap` (`clippy.toml`). + +## Assets: absolutely no Microsoft (or Adobe/Avid/Autodesk) iconography or images +**This rule is non-negotiable.** Breaking it is the worst mistake you can make in this repo. +- Never copy, trace, screenshot-crop or recreate icons, images, templates, themes, clip art, fonts, cursors, sounds or any other asset from Microsoft Word/Office or any Microsoft, Adobe, Avid or Autodesk product. Not even "temporarily". +- Every asset must be **original** (drawn in code — prefer this: `crates/ui-egui/src/icons.rs`), **public domain / CC0**, **OSI open source**, or **Creative Commons that allows redistribution**, or licensed open source by a contributor who made it. +- **Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md)** (author, source, licence). `cargo xtask assets` (part of `cargo xtask ci`) fails otherwise. Add the row in the same commit as the file. +- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts. +- Templates, sample documents and themes are our own text and colours (`crates/engine/src/sample.rs`, `cmd/design.rs`). Demo images are public domain, with sources recorded. + +## Clean room +- Microsoft Word is installed on the dev machine and may be **observed** black-box: run it, use its UI with synthetic documents, take screenshots **of its window only** (by window id — the machine runs other work; never capture the whole desktop) stored only under `plan/word/screenshots/` (gitignored, never committed). +- Never read, disassemble or copy anything inside the Word bundle (file names/listings only). Never commit files produced by Word. Never copy Microsoft wording beyond feature names. +- File formats come from public specs: ECMA-376 / ISO 29500 (OOXML), the published RTF spec, OASIS ODF 1.3. Never copy GPL/LGPL/AGPL code (LibreOffice, AbiWord, pandoc…). + +## Architecture (see `plan/architecture.md`) +| Layer | Crate | Job | +|---|---|---| +| L0 | `geom` | units, rects, measurement parsing | +| L1 | `doc`, `fonts` | document model + edit primitives; font DB, shaping, Word font substitution | +| L2 | `layout`, `docx`, `formats` | line breaking, pagination, tables, headers/footers, hit testing; OOXML I/O; TXT/MD/HTML/RTF/ODT | +| L3 | `render`, `pdf`, `proof` | vello_cpu rasteriser; krilla PDF; proofing | +| L4 | `engine` | Session, undo, **command registry**, Word feature catalog, samples, I/O dispatch | +| L5 | `mcp` | MCP server (headless or bridged to the app) | +| L6 | `ui-egui` | Word-style egui front end (swappable) | +| apps | `apps/wordcraft`, `apps/wordcraft-cli`, `apps/wordcraft-web` | binaries | + +- **Everything is a command.** User-visible behaviour = a `CommandSpec` in `crates/engine/src/cmd/*.rs` (id, label, ribbon location, shortcut, params doc, `enabled`, `run`) + tests. UI-only commands (`ui.*`) live in `crates/ui-egui/src/lib.rs`. The ribbon, shortcuts, command search, CLI, control channel and MCP all dispatch by id. Programmatic calls never open dialogs. +- **Parity is measured:** `crates/engine/src/catalog.rs` lists Word's ribbon/menu features with command ids. `cargo xtask parity` writes `docs/parity.md`; a test enforces a floor that only rises. Implement missing ids to raise it. +- **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. The UI is thin and reads `Session` state; colours come from `theme::Tokens`. +- **Rust only** (no handwritten JS/TS). **Never break wasm** (`cargo xtask wasm`). + +## Quality gates +Before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, assets, layers, wasm). Commit after every arc of work that builds, with a task id in the message (`M2.1: line spacing dialog`). + +## Running and looking at the app +- `cargo run --release -p wordcraft -- --sample --control 7981` (sample document + control channel). +- Drive it with JSON lines on `127.0.0.1:7981`, e.g. `{"id":1,"method":"engine.execute","params":{"command":"text.insert","params":{"text":"Hello"}}}` then `{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods: `crates/ui-egui/src/control.rs`; docs: `docs/control-protocol.md`. +- **Headless window screenshots** (no focus stealing, works with a locked screen): `cargo run --release -p wordcraft-ui-egui --example ui_shot -- script.jsonl` (see the example's header). +- **For UI work, look at the result** (screenshot, read the PNG) and compare with Word side by side. +- CLI: `wordcraft-cli convert in.docx out.pdf`, `wordcraft-cli run --template sample --cmd 'select.text={"text":"Studio"}' --cmd format.bold --save out.docx`, `wordcraft-cli commands`, `wordcraft-cli parity`. +- MCP: `wordcraft-cli mcp` (headless) or `wordcraft-cli mcp --connect 127.0.0.1:7981` (drives the running app). See `docs/mcp.md`. +- Shell gotcha: `mv`/`cp` may be aliased interactive — use `/bin/mv -f` / `/bin/cp -f`. +- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done (disk). + +## Roadmap +`ROADMAP.md` (committed) tracks status, milestones and estimates. Update it whenever a milestone task lands. diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md new file mode 100644 index 000000000..8e1e8a398 --- /dev/null +++ b/ATTRIBUTION.md @@ -0,0 +1,51 @@ +# Asset attribution + +Every non-code asset in this repository (fonts, icons, images, example documents) is listed here with its author, source and licence. `cargo xtask assets` (part of `cargo xtask ci`) fails if an asset file is missing from this table. + +**Policy (mandatory):** WordCraft contains **no Microsoft, Adobe, Avid or Autodesk iconography, images, templates, themes, clip art, fonts or other assets**. Every asset is original work by WordCraft contributors or third-party material under an open licence (OSI open source, public domain / CC0, or a Creative Commons licence that allows redistribution). Screenshots of other vendors' software are never committed. New font files go to [storytold/craft-fonts](https://github.com/storytold/craft-fonts), not this repo. The one exception is the ArtCraft brand files in `docs/brand/`: ArtCraft trademarks, not open source, used under `docs/brand/LICENSE-brand.txt`. + +Generated-in-code art is original and has no file to list: the UI icon set (`crates/ui-egui/src/icons.rs`), the sample documents and templates (`crates/engine/src/sample.rs`), the table and style previews, and theme colours (`crates/engine/src/cmd/design.rs`). + +| Asset | Author | Source | Licence | Notes | +|---|---|---|---|---| +| `assets/app-icon/wordcraft-1024.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/wordcraft-macos-512.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/wordcraft.icns` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/wordcraft.ico` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/wordcraft.svg` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.wordcraft.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.wordcraft.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.wordcraft.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.wordcraft.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.wordcraft.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.wordcraft.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.wordcraft.svg` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.wordcraft.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.wordcraft.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | +| `assets/fonts/Inter-Medium.ttf` | Rasmus Andersson / The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/Inter-Regular.ttf` | Rasmus Andersson / The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/Inter-SemiBold.ttf` | Rasmus Andersson / The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/JetBrainsMono-Regular.ttf` | The JetBrains Mono Project Authors | https://github.com/JetBrains/JetBrainsMono | OFL-1.1 (`assets/fonts/OFL-JetBrainsMono.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/OFL-Inter.txt` | (licence text) | upstream project | — | | +| `assets/fonts/OFL-JetBrainsMono.txt` | (licence text) | upstream project | — | | +| `assets/fonts/OFL-SourceSans3.txt` | (licence text) | upstream project | — | | +| `assets/fonts/OFL-SourceSerif4.txt` | (licence text) | upstream project | — | | +| `assets/fonts/SourceSans3-Bold.ttf` | Paul D. Hunt / Frank Grießhammer, Adobe (released as open source) | https://github.com/adobe-fonts/source-sans · https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSans3.txt`, `OFL-SourceSerif4.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/SourceSans3-It.ttf` | Paul D. Hunt / Frank Grießhammer, Adobe (released as open source) | https://github.com/adobe-fonts/source-sans · https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSans3.txt`, `OFL-SourceSerif4.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/SourceSans3-Regular.ttf` | Paul D. Hunt / Frank Grießhammer, Adobe (released as open source) | https://github.com/adobe-fonts/source-sans · https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSans3.txt`, `OFL-SourceSerif4.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/SourceSans3-Semibold.ttf` | Paul D. Hunt / Frank Grießhammer, Adobe (released as open source) | https://github.com/adobe-fonts/source-sans · https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSans3.txt`, `OFL-SourceSerif4.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/SourceSerif4-Bold.ttf` | Paul D. Hunt / Frank Grießhammer, Adobe (released as open source) | https://github.com/adobe-fonts/source-sans · https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSans3.txt`, `OFL-SourceSerif4.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/SourceSerif4-BoldIt.ttf` | Paul D. Hunt / Frank Grießhammer, Adobe (released as open source) | https://github.com/adobe-fonts/source-sans · https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSans3.txt`, `OFL-SourceSerif4.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/SourceSerif4-It.ttf` | Paul D. Hunt / Frank Grießhammer, Adobe (released as open source) | https://github.com/adobe-fonts/source-sans · https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSans3.txt`, `OFL-SourceSerif4.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/SourceSerif4-Regular.ttf` | Paul D. Hunt / Frank Grießhammer, Adobe (released as open source) | https://github.com/adobe-fonts/source-sans · https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSans3.txt`, `OFL-SourceSerif4.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `assets/fonts/SourceSerif4-Semibold.ttf` | Paul D. Hunt / Frank Grießhammer, Adobe (released as open source) | https://github.com/adobe-fonts/source-sans · https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSans3.txt`, `OFL-SourceSerif4.txt`) | Open-source typeface (Latin UI/document font, carried over from DesignCraft) | +| `docs/brand/LICENSE-brand.txt` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | +| `docs/brand/artcraft-logo-white.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | +| `docs/brand/artcraft-logo-white.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | +| `docs/brand/artcraft-logo.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | +| `docs/brand/artcraft-logo.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | +| `docs/brand/artcraft-mark-black.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | +| `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | +| `docs/brand/artcraft-mark.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | +| `docs/brand/artcraft-mark.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | +| craft-fonts (optional build input, not files in this repo): BIZ UDPGothic, BIZ UDMincho, Shippori Mincho | see https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | embedded only when built with `CRAFT_FONTS_DIR` (all official releases) | OFL-1.1 | Japanese UI and document fallback | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..c4125c1c7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,62 @@ +# WordCraft — instructions for agents + +WordCraft is a clean-room, open-source, Rust-native word processor targeting Microsoft Word parity — and going further on speed, openness and agent control. It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../printcraft` (Acrobat), `../effectcraft` (After Effects), `../designcraft` (InDesign). Shared rules and learnings: [`storytold/craftrules`](https://github.com/storytold/craftrules) (`../../craftrules`) — read its `AGENTS.md`. + +## Start every session here +1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` and the relevant `plan/architecture.md` section. `plan/` is gitignored (local only); if it's missing, start from `ROADMAP.md` and `docs/parity.md`. +2. Follow the autonomous operation protocol (`plan/execution-plan.md` §5). Don't stop to ask unless a decision is genuinely the owner's (licensing, publishing, pushing to new remotes, secrets). + +## Never crash +People trust WordCraft with their writing; a crash loses their work. **This outranks feature work**: never ship a feature through a panic path, and fix a crash before building on top of it. Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md). +- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). +- **Errors are `Result`** through the crate's error type and `?`. An unfinished feature returns an error or is disabled; it never panics. +- **Input-derived numbers are hostile** (files, commands, MCP/control params): `get()` instead of `[i]`, slice strings only at char boundaries, checked/saturating arithmetic, clamp sizes, cap allocations, bound recursion. +- **Last-resort guard:** `Session::run` turns an escaped panic into an error and restores the document. It's a safety net, not a licence. +- **Prove it:** every crash fix lands with a regression test. `crates/engine/src/tests.rs::hostile_params_never_panic` runs every command with junk params — keep it green. +- Production crate roots carry `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`; new crates start with it. Tests may `unwrap` (`clippy.toml`). + +## Assets: absolutely no Microsoft (or Adobe/Avid/Autodesk) iconography or images +**This rule is non-negotiable.** Breaking it is the worst mistake you can make in this repo. +- Never copy, trace, screenshot-crop or recreate icons, images, templates, themes, clip art, fonts, cursors, sounds or any other asset from Microsoft Word/Office or any Microsoft, Adobe, Avid or Autodesk product. Not even "temporarily". +- Every asset must be **original** (drawn in code — prefer this: `crates/ui-egui/src/icons.rs`), **public domain / CC0**, **OSI open source**, or **Creative Commons that allows redistribution**, or licensed open source by a contributor who made it. +- **Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md)** (author, source, licence). `cargo xtask assets` (part of `cargo xtask ci`) fails otherwise. Add the row in the same commit as the file. +- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts. +- Templates, sample documents and themes are our own text and colours (`crates/engine/src/sample.rs`, `cmd/design.rs`). Demo images are public domain, with sources recorded. + +## Clean room +- Microsoft Word is installed on the dev machine and may be **observed** black-box: run it, use its UI with synthetic documents, take screenshots **of its window only** (by window id — the machine runs other work; never capture the whole desktop) stored only under `plan/word/screenshots/` (gitignored, never committed). +- Never read, disassemble or copy anything inside the Word bundle (file names/listings only). Never commit files produced by Word. Never copy Microsoft wording beyond feature names. +- File formats come from public specs: ECMA-376 / ISO 29500 (OOXML), the published RTF spec, OASIS ODF 1.3. Never copy GPL/LGPL/AGPL code (LibreOffice, AbiWord, pandoc…). + +## Architecture (see `plan/architecture.md`) +| Layer | Crate | Job | +|---|---|---| +| L0 | `geom` | units, rects, measurement parsing | +| L1 | `doc`, `fonts` | document model + edit primitives; font DB, shaping, Word font substitution | +| L2 | `layout`, `docx`, `formats` | line breaking, pagination, tables, headers/footers, hit testing; OOXML I/O; TXT/MD/HTML/RTF/ODT | +| L3 | `render`, `pdf`, `proof` | vello_cpu rasteriser; krilla PDF; proofing | +| L4 | `engine` | Session, undo, **command registry**, Word feature catalog, samples, I/O dispatch | +| L5 | `mcp` | MCP server (headless or bridged to the app) | +| L6 | `ui-egui` | Word-style egui front end (swappable) | +| apps | `apps/wordcraft`, `apps/wordcraft-cli`, `apps/wordcraft-web` | binaries | + +- **Everything is a command.** User-visible behaviour = a `CommandSpec` in `crates/engine/src/cmd/*.rs` (id, label, ribbon location, shortcut, params doc, `enabled`, `run`) + tests. UI-only commands (`ui.*`) live in `crates/ui-egui/src/lib.rs`. The ribbon, shortcuts, command search, CLI, control channel and MCP all dispatch by id. Programmatic calls never open dialogs. +- **Parity is measured:** `crates/engine/src/catalog.rs` lists Word's ribbon/menu features with command ids. `cargo xtask parity` writes `docs/parity.md`; a test enforces a floor that only rises. Implement missing ids to raise it. +- **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. The UI is thin and reads `Session` state; colours come from `theme::Tokens`. +- **Rust only** (no handwritten JS/TS). **Never break wasm** (`cargo xtask wasm`). + +## Quality gates +Before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, assets, layers, wasm). Commit after every arc of work that builds, with a task id in the message (`M2.1: line spacing dialog`). + +## Running and looking at the app +- `cargo run --release -p wordcraft -- --sample --control 7981` (sample document + control channel). +- Drive it with JSON lines on `127.0.0.1:7981`, e.g. `{"id":1,"method":"engine.execute","params":{"command":"text.insert","params":{"text":"Hello"}}}` then `{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods: `crates/ui-egui/src/control.rs`; docs: `docs/control-protocol.md`. +- **Headless window screenshots** (no focus stealing, works with a locked screen): `cargo run --release -p wordcraft-ui-egui --example ui_shot -- script.jsonl` (see the example's header). +- **For UI work, look at the result** (screenshot, read the PNG) and compare with Word side by side. +- CLI: `wordcraft-cli convert in.docx out.pdf`, `wordcraft-cli run --template sample --cmd 'select.text={"text":"Studio"}' --cmd format.bold --save out.docx`, `wordcraft-cli commands`, `wordcraft-cli parity`. +- MCP: `wordcraft-cli mcp` (headless) or `wordcraft-cli mcp --connect 127.0.0.1:7981` (drives the running app). See `docs/mcp.md`. +- Shell gotcha: `mv`/`cp` may be aliased interactive — use `/bin/mv -f` / `/bin/cp -f`. +- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done (disk). + +## Roadmap +`ROADMAP.md` (committed) tracks status, milestones and estimates. Update it whenever a milestone task lands. diff --git a/Cargo.lock b/Cargo.lock index 053950cf5..a95959dea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arboard" version = "3.6.1" @@ -763,6 +772,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "dify" version = "0.8.0" @@ -2717,6 +2737,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.41.0" @@ -3865,7 +3894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.41.0", "quote", ] @@ -4470,6 +4499,17 @@ dependencies = [ "wordcraft-ui-egui", ] +[[package]] +name = "wordcraft-cli" +version = "0.1.0" +dependencies = [ + "serde_json", + "wordcraft-doc", + "wordcraft-engine", + "wordcraft-mcp", + "wordcraft-render", +] + [[package]] name = "wordcraft-doc" version = "0.1.0" @@ -4482,6 +4522,20 @@ dependencies = [ "wordcraft-geom", ] +[[package]] +name = "wordcraft-docx" +version = "0.1.0" +dependencies = [ + "image", + "log", + "proptest", + "quick-xml 0.38.4", + "thiserror 2.0.21", + "wordcraft-doc", + "wordcraft-geom", + "zip", +] + [[package]] name = "wordcraft-engine" version = "0.1.0" @@ -4530,6 +4584,17 @@ dependencies = [ "wordcraft-geom", ] +[[package]] +name = "wordcraft-mcp" +version = "0.1.0" +dependencies = [ + "log", + "serde_json", + "wordcraft-doc", + "wordcraft-engine", + "wordcraft-render", +] + [[package]] name = "wordcraft-render" version = "0.1.0" @@ -4840,6 +4905,20 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "zip" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + [[package]] name = "zlib-rs" version = "0.6.8" @@ -4852,6 +4931,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zune-core" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index c681dba6a..c43cd3521 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "3" members = ["crates/*", "apps/*", "xtask"] -default-members = ["crates/*", "apps/wordcraft", "xtask"] +default-members = ["crates/*", "apps/wordcraft", "apps/wordcraft-cli", "xtask"] [workspace.package] version = "0.1.0" diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..dcc9ef435 --- /dev/null +++ b/NOTICE @@ -0,0 +1,28 @@ +WordCraft +Copyright (c) 2026 ArtCraft Team and the WordCraft contributors. +Licensed under the MIT License or the Apache License, Version 2.0, at your option +(LICENSE-MIT, LICENSE-APACHE). + +The ArtCraft name, wordmark and logos in docs/brand/ are trademarks of the ArtCraft Team. They are +not open source and are not covered by the license above; their terms are in +docs/brand/LICENSE-brand.txt. + +Third-party material + +This product bundles third-party material under its own licenses, listed below. The per-file +list (author, source, license) is in ATTRIBUTION.md. + +- Source Sans 3 and Source Serif 4 (assets/fonts/Source*) — SIL Open Font License 1.1. See assets/fonts/OFL-SourceSans3.txt, OFL-SourceSerif4.txt. +- Inter (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt. +- JetBrains Mono (assets/fonts/JetBrainsMono-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-JetBrainsMono.txt. +- Japanese fonts (craft-fonts): builds made with craft-fonts (CRAFT_FONTS_DIR; all official + releases) embed the fonts listed in + https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (BIZ UDPGothic, BIZ UDMincho, + Shippori Mincho), under the SIL Open Font License 1.1. Their licence texts ship with the + packages as OFL-.txt. Builds without CRAFT_FONTS_DIR do not include them. +- All UI icons are drawn in code (crates/ui-egui/src/icons.rs) and are original work, MIT OR Apache-2.0. + +Rust dependencies (egui, eframe, vello_cpu, skrifa, harfrust, krilla, quick-xml, zip, image, …) +are permissively licensed (MIT/Apache-2.0/BSD/ISC/Zlib) and fetched through crates.io. + +No Microsoft, Adobe, Avid or Autodesk icons, images, templates or artwork are included. diff --git a/apps/wordcraft-cli/Cargo.toml b/apps/wordcraft-cli/Cargo.toml new file mode 100644 index 000000000..782aaf129 --- /dev/null +++ b/apps/wordcraft-cli/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wordcraft-cli" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false +description = "WordCraft command line: convert, inspect, render, run commands, MCP server" + +[[bin]] +name = "wordcraft-cli" +path = "src/main.rs" + +[lints] +workspace = true + +[dependencies] +wordcraft-doc = { workspace = true } +wordcraft-engine = { workspace = true } +wordcraft-render = { workspace = true } +wordcraft-mcp = { workspace = true } +serde_json = { workspace = true } diff --git a/apps/wordcraft-cli/src/main.rs b/apps/wordcraft-cli/src/main.rs new file mode 100644 index 000000000..bde3c42e2 --- /dev/null +++ b/apps/wordcraft-cli/src/main.rs @@ -0,0 +1,184 @@ +//! `wordcraft-cli`: WordCraft from the command line. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + +use std::process::ExitCode; + +use serde_json::{Value, json}; +use wordcraft_engine::Session; + +const USAGE: &str = "\ +wordcraft-cli — WordCraft from the command line + +USAGE: + wordcraft-cli convert convert between formats (docx, pdf, odt, rtf, html, md, txt, json, png) + wordcraft-cli info pages, words, paragraphs, properties (JSON) + wordcraft-cli text plain text + wordcraft-cli inspect document structure (JSON) + wordcraft-cli render [--page N] [--scale S] + wordcraft-cli run [--file F | --template T] --cmd 'id={json}' [--cmd …] [--save OUT] [--print] + run commands headlessly, then save + wordcraft-cli commands [--json] list every command + wordcraft-cli parity [--markdown] feature-catalog parity + wordcraft-cli mcp [--connect HOST:PORT] MCP server on stdio (headless, or bridged to the app) + wordcraft-cli --version +"; + +fn open(path: &str) -> Result { + let doc = wordcraft_engine::io::open_path(std::path::Path::new(path))?; + let mut s = Session::new(doc); + s.path = Some(path.into()); + Ok(s) +} + +fn arg_value(args: &[String], name: &str) -> Option { + args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).cloned() +} + +fn run(args: &[String]) -> Result<(), String> { + let cmd = args.first().map(String::as_str).unwrap_or(""); + let rest: Vec = args.iter().skip(1).cloned().collect(); + let pos = |i: usize| rest.iter().filter(|a| !a.starts_with("--")).nth(i).cloned().ok_or_else(|| format!("missing argument\n\n{USAGE}")); + match cmd { + "convert" => { + let (i, o) = (pos(0)?, pos(1)?); + let mut s = open(&i)?; + let r = if o.to_ascii_lowercase().ends_with(".png") { + s.run("file.exportPng", &json!({"path": o, "page": 1})) + } else { + s.run("file.save", &json!({"path": o})) + }; + r.map_err(|e| e.to_string())?; + eprintln!("wrote {o}"); + Ok(()) + } + "info" => { + let mut s = open(&pos(0)?)?; + println!("{}", serde_json::to_string_pretty(&s.run("file.info", &json!({})).map_err(|e| e.to_string())?).unwrap_or_default()); + Ok(()) + } + "text" => { + let s = open(&pos(0)?)?; + println!("{}", s.doc.plain_text(wordcraft_doc::StoryRef::Body)); + Ok(()) + } + "inspect" => { + let mut s = open(&pos(0)?)?; + println!("{}", serde_json::to_string_pretty(&s.run("document.inspect", &json!({})).map_err(|e| e.to_string())?).unwrap_or_default()); + Ok(()) + } + "render" => { + let (i, o) = (pos(0)?, pos(1)?); + let mut s = open(&i)?; + let page = arg_value(&rest, "--page").and_then(|p| p.parse::().ok()).unwrap_or(1); + let scale = arg_value(&rest, "--scale").and_then(|p| p.parse::().ok()).unwrap_or(2.0); + let r = s.run("file.exportPng", &json!({"path": o, "page": page, "scale": scale})).map_err(|e| e.to_string())?; + println!("{r}"); + Ok(()) + } + "run" => { + let mut s = match (arg_value(&rest, "--file"), arg_value(&rest, "--template")) { + (Some(f), _) => open(&f)?, + (None, Some(t)) => { + let mut s = Session::new(wordcraft_doc::Document::new()); + s.run("file.new", &json!({"template": t})).map_err(|e| e.to_string())?; + s + } + _ => Session::new(wordcraft_doc::Document::new()), + }; + let mut i = 0; + while i < rest.len() { + if rest.get(i).map(String::as_str) == Some("--cmd") { + let spec = rest.get(i + 1).ok_or("--cmd needs a value")?; + let (id, params) = match spec.split_once('=') { + Some((id, p)) => (id.to_string(), serde_json::from_str::(p).map_err(|e| format!("{id}: bad JSON params: {e}"))?), + None => (spec.clone(), json!({})), + }; + let r = s.run(&id, ¶ms).map_err(|e| format!("{id}: {e}"))?; + if rest.iter().any(|a| a == "--print") { + println!("{id}: {r}"); + } + i += 2; + } else { + i += 1; + } + } + if let Some(out) = arg_value(&rest, "--save") { + let r = if out.to_ascii_lowercase().ends_with(".png") { s.run("file.exportPng", &json!({"path": out})) } else { s.run("file.save", &json!({"path": out})) }; + r.map_err(|e| e.to_string())?; + eprintln!("wrote {out}"); + } + Ok(()) + } + "commands" => { + let s = Session::new(wordcraft_doc::Document::new()); + if rest.iter().any(|a| a == "--json") { + println!("{}", serde_json::to_string_pretty(&s.registry.describe()).unwrap_or_default()); + } else { + for c in s.registry.all() { + println!("{:<32} {:<36} {:<24} {}", c.id, c.label, c.shortcut, c.location); + } + } + Ok(()) + } + "parity" => { + let s = Session::new(wordcraft_doc::Document::new()); + let p = wordcraft_engine::catalog::parity(&s.registry); + if rest.iter().any(|a| a == "--markdown") { + print!("{}", parity_markdown(&p)); + } else { + println!("{}", serde_json::to_string_pretty(&p).unwrap_or_default()); + } + Ok(()) + } + "mcp" => { + let backend: Box = match arg_value(&rest, "--connect") { + Some(addr) => Box::new(wordcraft_mcp::Remote::connect(&addr).map_err(|e| format!("can't reach the WordCraft app at {addr}: {e}"))?), + None => Box::new(wordcraft_mcp::Headless::default()), + }; + let mut server = wordcraft_mcp::Server::new(backend); + let stdin = std::io::stdin(); + server.serve(stdin.lock(), std::io::stdout()).map_err(|e| e.to_string()) + } + "--version" | "-V" => { + println!("wordcraft-cli {}", env!("CARGO_PKG_VERSION")); + Ok(()) + } + "" | "help" | "--help" | "-h" => { + print!("{USAGE}"); + Ok(()) + } + other => Err(format!("unknown command `{other}`\n\n{USAGE}")), + } +} + +fn parity_markdown(p: &Value) -> String { + let mut s = String::from("# WordCraft feature parity\n\nGenerated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-processor feature catalog (`crates/engine/src/catalog.rs`) compared with the live command registry.\n\n"); + s += &format!("**{} of {} catalog features have commands ({}%).**\n\n| Tab | Live | Total |\n|---|---|---|\n", p["live"], p["total"], p["percent"]); + for t in p["tabs"].as_array().cloned().unwrap_or_default() { + s += &format!("| {} | {} | {} |\n", t["tab"].as_str().unwrap_or(""), t["live"], t["total"]); + } + s += "\n## Missing\n\n"; + for t in p["tabs"].as_array().cloned().unwrap_or_default() { + let m = t["missing"].as_array().cloned().unwrap_or_default(); + if m.is_empty() { + continue; + } + s += &format!("### {}\n\n", t["tab"].as_str().unwrap_or("")); + for x in m { + s += &format!("- {}\n", x.as_str().unwrap_or("")); + } + s += "\n"; + } + s +} + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + match run(&args) { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("wordcraft-cli: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/apps/wordcraft/src/main.rs b/apps/wordcraft/src/main.rs index a14270e1b..052eb6e24 100644 --- a/apps/wordcraft/src/main.rs +++ b/apps/wordcraft/src/main.rs @@ -91,6 +91,15 @@ fn services() -> Services { } } +/// Window, Dock and taskbar icon. +fn app_icon() -> Option { + #[cfg(target_os = "macos")] + let png: &[u8] = include_bytes!("../../../assets/app-icon/wordcraft-macos-512.png"); + #[cfg(not(target_os = "macos"))] + let png: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.wordcraft.png"); + eframe::icon_data::from_png_bytes(png).map_err(|e| log::warn!("app icon: {e}")).ok() +} + fn main() -> eframe::Result { let mut control_port: Option = std::env::var("WORDCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok()); let mut files = Vec::new(); @@ -107,7 +116,7 @@ fn main() -> eframe::Result { _ => files.push(a), } } - let options = eframe::NativeOptions { + let mut options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_title("WordCraft") .with_app_id("ai.storyteller.wordcraft") @@ -119,6 +128,9 @@ fn main() -> eframe::Result { .with_title_shown(false), ..Default::default() }; + if let Some(icon) = app_icon() { + options.viewport = options.viewport.with_icon(icon); + } eframe::run_native( "WordCraft", options, diff --git a/assets/app-icon/hicolor/128x128/apps/ai.storyteller.wordcraft.png b/assets/app-icon/hicolor/128x128/apps/ai.storyteller.wordcraft.png new file mode 100644 index 0000000000000000000000000000000000000000..35bb5e03bcf42b923b361e7afd3047b242c6141b GIT binary patch literal 3350 zcmV+x4e9cUP) zZH!b`8ONVHy(5x~6fdC?!Bx5^YI{1*25cCA!L*U}C~T#828I3134qN_4vX0 zuZ4M6C<82SJ8uh5ouuJR*U9b$TZ6thkO9!;7YfB=_F)TYRG>GXH&+K45^}X(fGJ&} zPnO> zRfmAmm~S+ZMsbX$a{T6rrvX-M?C&By=4XkPvZfQ5Fa(t5`PQ#VlR0vR@dbCDV4MN; zxSw?<#slMq06V?gsm0GSMpH>+=@D_@An^Z;;%7C-wyW4RZ| zNzZH{&Aox7wPOH8{<`(wJ3wn_j$_9F_O!2^G``#|+@xFA{!0HJK1*3ZXl~+%9T=># z=-vgQ-Ywj6L>2E%t@ah)q6|_J^qg~C z68YvhL50XsxPSU0n!nUASSbbo z@4dUn5w#Jm7EGI35)>?Ze2H^Ed)7lVXZAxCvAux!8%}k2K6mlW=bQi$MoOG=k75B!vGi(<}KZ+NL*kK0hA){ma^fCPdTF2tJTI~ zj>`ZyZr-GATQ)nr&bIB(I==%9WBn(-=}Z$fKrjmep_Y3C1jIKzbi;==^PSC0`lSoHwh2^x?hjzMpLnTi0zz=hM>oV*7oQa8Yd zCXandB8cLOEGT%#r^iBEaO8I{IkN;A