Merge pull request #1615 from avhn/fix/1396-cli-completions-man-pages

M0.9: generate shell completions and man pages for figcraft-cli (part of #1396)
This commit is contained in:
BFlat authored and GitHub committed 2026-10-30 15:09:34 +07:00
commit e110f23d8c
8 files changed
+273 -3

No files matched your search

Generated
+27
View File
@@ -768,6 +768,15 @@ dependencies = [
"strsim",
]
[[package]]
name = "clap_complete"
version = "4.5.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1248e420506b523e97ebd7b610656114abadbe73b04279a86768a12ce1860837"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.6.7"
@@ -786,6 +795,16 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
[[package]]
name = "clap_mangen"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "211d617eaa4b735c96c9e0228fcbdb5120ef623f2b8cb67ffb84c3e02dbc28a4"
dependencies = [
"clap",
"roff",
]
[[package]]
name = "clipboard-win"
version = "5.4.3"
@@ -1779,6 +1798,8 @@ name = "figcraft-cli"
version = "0.12.1"
dependencies = [
"clap",
"clap_complete",
"clap_mangen",
"figcraft-doc",
"figcraft-engine",
"figcraft-format",
@@ -4468,6 +4489,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "roff"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189"
[[package]]
name = "roxmltree"
version = "0.20.0"
+2 -1
View File
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (`.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); German terminology corrections (M10): gradient stops, OpenType stylistic sets and the Home-screen preference; every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; the server key is checked by comparing SHA-256 hashes of the presented and the kept key, as the deployment guide says (#1130); opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rLine truncated
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (a development example generates figcraft-cli's shell completions and man pages from its clap definitions (part of #1396); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); German terminology corrections (M10): gradient stops, OpenType stylistic sets and the Home-screen preference; every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; the server key is checked by comparing SHA-256 hashes of the presented and the kept key, as the deployment guide says (#1130); opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405)Line truncated
FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow,
native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through
@@ -356,6 +356,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | M0.9, part of #1396: a development example generates completions for Bash, Elvish, Fish, PowerShell and Zsh and a man page for the CLI and each subcommand, nested ones included, from figcraft-cli's clap definitions, without running commands. Generating these files doesn't install them; package installation and the other RFC-0012 exceptions remain outstanding. Regression tests in `apps/figcraft-cli/tests/it/generated_docs.rs` cover artifacts, command names and options, matching Bash dispatch states, repeat generation and filesystem errors. Numbers unchanged |
| 2026-10-30 | minor | M8, #1076: bound total `.fig` ZIP image reads to 4 GiB (1 GiB on 32-bit targets) of decompressed ZIP-member bytes containing encoded images, keeping the 256 MiB member cap. Members that do not fit are skipped before allocation and counted in the import warnings; failed reads still consume their bytes, and repeated references keep their cached resolution. Embedded message images, decoded pixel memory and native `.figcraft` reads are unchanged. Regression tests in `fig::tests::image_budget` (format). Numbers unchanged. |
| 2026-10-30 | minor | M8, #1114 (the rest): `file.open` and `file.import` (and recovery copies), `image.place {path}`, the image fill's file picker and `figcraft-cli run`'s script read the path they were given whole, so a device (`/dev/zero`), a pipe or an absurd file could fill memory or wait forever. Every whole-file read of a path now goes through `file_access::read_capped`, which looks at the path before opening it and refuses a folder, a device or pipe, or a file over its cap, with a message saying which, and reads at most the cap even if the file grows; the uncapped `file_access::read` is gone. Caps: documents 16 GiB (a `.figcraft` keeps its images as they are: #1313's file is 7.4 GB on disk), placed images 256 MiB (what a document takes from outside, `ImageData::MAX_BYTES`), scripts 1 GiB; fonts (256 MiB), plugin files and variables files already had theirs. Numbers unchanged. Tests: `path_read_caps_tests`, `file_access` tests, the CLI's `a_script_that_isnt_a_regular_file_is_refused`. |
| 2026-10-30 | minor | M8 (found while retaking the README screenshots): at the default (and narrowest) 240 px right panel, a component set's variant property rows ran off the panel: values past the second ("Outline", "Disabled") were cut off, and the + after them and the − of each property sat past the edge; a shared boolean property's − was pushed out too. Values now wrap as many to a line as fit (two at 240 px, three from about 320 px as before), the property name field gives way first so the default and − stay in view, and a boolean's checkbox no longer takes the whole row. Numbers unchanged. Tests: `component_set_narrow_tests`. |
+5
View File
@@ -24,3 +24,8 @@ figcraft-mcp = { workspace = true }
serde_json = { workspace = true }
clap = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
# 4.6 breaks Bash subcommand completion for hyphenated binary names (clap-rs/clap#6421).
clap_complete = "~4.5.67"
clap_mangen = "0.3"
@@ -0,0 +1,44 @@
//! Writes figcraft-cli's shell completions and recursive man pages into a directory:
//!
//! ```sh
//! cargo run -p figcraft-cli --example man-and-completions -- <out-dir>
//! ```
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#[path = "../src/args.rs"]
#[expect(dead_code, reason = "Generation uses clap's command metadata, not the parsed fields or runtime parser.")]
mod args;
use std::path::Path;
use std::process::ExitCode;
use clap::{CommandFactory, ValueEnum};
use clap_complete::Shell;
fn main() -> ExitCode {
let mut args = std::env::args_os().skip(1);
let (Some(dir), None) = (args.next(), args.next()) else {
eprintln!("usage: man-and-completions <out-dir>");
return ExitCode::from(2);
};
let dir = Path::new(&dir);
match generate(dir) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {}: {e}", dir.display());
ExitCode::FAILURE
}
}
}
pub(crate) fn generate(dir: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dir)?;
let mut command = args::Cli::command();
let name = command.get_name().to_owned();
for &shell in Shell::value_variants() {
clap_complete::generate_to(shell, &mut command, &name, dir)?;
}
// Completions build the command and add synthetic help subcommands. A fresh command keeps
// those out of the man pages (such as figcraft-cli-help-export.1).
clap_mangen::generate_to(args::Cli::command(), dir)
}
@@ -0,0 +1,161 @@
//! Generated CLI documentation uses the parser's command definition, without running commands.
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
#[path = "../../examples/man-and-completions.rs"]
#[expect(dead_code, reason = "These tests call generate directly; the example's main is smoke-tested separately.")]
mod generator;
struct TempDir(PathBuf);
impl TempDir {
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn tmp(name: &str) -> std::io::Result<TempDir> {
static NEXT: AtomicUsize = AtomicUsize::new(0);
loop {
let n = NEXT.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("figcraft-cli-test-{}-{n}-{name}", std::process::id()));
match fs::create_dir(&dir) {
Ok(()) => return Ok(TempDir(dir)),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
}
}
#[test]
fn completions_name_the_cli_its_subcommands_and_options() -> std::io::Result<()> {
let temp = tmp("completions")?;
let dir = temp.path();
generator::generate(dir)?;
for name in ["figcraft-cli.bash", "figcraft-cli.elv", "figcraft-cli.fish", "_figcraft-cli.ps1", "_figcraft-cli"] {
let text = fs::read_to_string(dir.join(name))?;
for word in [
"figcraft-cli",
"info",
"export",
"convert",
"render",
"run",
"commands",
"mcp",
"plugin",
"perf",
"publish",
"no-system-fonts",
"format",
"automation-read-root",
"server",
] {
assert!(text.contains(word), "{name} does not contain {word}");
}
assert!(!text.contains("man-and-completions"), "{name} names the example instead of the CLI");
}
Ok(())
}
#[test]
fn bash_dispatch_states_have_matching_option_blocks() -> std::io::Result<()> {
let temp = tmp("bash-completion-states")?;
let dir = temp.path();
generator::generate(dir)?;
let text = fs::read_to_string(dir.join("figcraft-cli.bash"))?;
let states: Vec<_> =
text.lines().filter_map(|line| line.trim().strip_prefix("cmd=\"")?.strip_suffix('"')).filter(|state| !state.is_empty()).collect();
assert!(states.len() > 1, "the completion has no subcommand dispatch states");
for state in states {
let block = format!("{state})");
assert!(text.lines().any(|line| line.trim() == block), "Bash dispatch state {state} has no option block");
}
Ok(())
}
#[test]
fn man_pages_cover_every_subcommand_without_synthetic_help_pages() -> std::io::Result<()> {
let temp = tmp("man-pages")?;
let dir = temp.path();
generator::generate(dir)?;
let mut pages = Vec::new();
for entry in fs::read_dir(dir)? {
let path = entry?.path();
if path.extension().is_some_and(|ext| ext == "1") {
pages.push(path.file_name().unwrap_or_default().to_string_lossy().into_owned());
}
}
pages.sort();
assert_eq!(
pages,
[
"figcraft-cli-commands.1",
"figcraft-cli-convert.1",
"figcraft-cli-export.1",
"figcraft-cli-info.1",
"figcraft-cli-mcp.1",
"figcraft-cli-perf.1",
"figcraft-cli-plugin-publish.1",
"figcraft-cli-plugin.1",
"figcraft-cli-render.1",
"figcraft-cli-run.1",
"figcraft-cli.1",
]
);
for name in &pages {
let text = fs::read_to_string(dir.join(name))?.replace("\\-", "-");
assert!(text.contains(".SH NAME"), "{name} has no NAME section");
assert!(text.contains(name.trim_end_matches(".1")), "{name} has the wrong command name");
assert!(!text.contains("man-and-completions"), "{name} names the example instead of the CLI");
}
for (page, option) in [
("figcraft-cli.1", "--no-system-fonts"),
("figcraft-cli-export.1", "--format"),
("figcraft-cli-export.1", "--automation-read-root"),
("figcraft-cli-plugin-publish.1", "--server"),
] {
let text = fs::read_to_string(dir.join(page))?.replace("\\-", "-");
assert!(text.contains(option), "{page} does not contain {option}");
}
Ok(())
}
#[test]
fn generation_creates_the_output_directory_and_can_run_again() -> std::io::Result<()> {
let temp = tmp("generated-docs-repeat")?;
let dir = temp.path();
let output = dir.join("new").join("docs");
assert!(!output.exists());
generator::generate(&output)?;
let page = output.join("figcraft-cli.1");
let first = fs::read(&page)?;
assert!(!first.is_empty());
fs::write(&page, "old page")?;
generator::generate(&output)?;
assert_eq!(fs::read(page)?, first);
assert_eq!(fs::read_dir(output)?.count(), 16);
Ok(())
}
#[test]
fn generation_returns_filesystem_errors() -> std::io::Result<()> {
let temp = tmp("generated-docs-errors")?;
let dir = temp.path();
let file = dir.join("not-a-directory");
fs::write(&file, "keep me")?;
assert!(generator::generate(&file).is_err());
assert_eq!(fs::read_to_string(&file)?, "keep me");
// A valid directory can still fail when writing one of the generated pages.
fs::create_dir(dir.join("figcraft-cli.1"))?;
assert!(generator::generate(dir).is_err());
Ok(())
}
+1
View File
@@ -6,6 +6,7 @@ mod cli;
mod export_depth;
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod fonts;
mod generated_docs;
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod pdf;
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
+30 -1
View File
@@ -29,7 +29,36 @@ Not yet as RFC-0012 asks:
function the tests call in-process.
- `FIGCRAFT_NO_SYSTEM_FONTS` and `FIGCRAFT_SERVER_KEY` are read where they're used, not declared
on a flag, so `--help` names them only in the flag descriptions.
- There are no generated shell completions or man pages, and no ast-grep checks in CI yet.
- Packages don't install the generated shell completions or man pages yet.
- There are no ast-grep checks in CI yet.
## Generating shell completions and man pages
From the workspace root, run:
```sh
cargo run -p figcraft-cli --example man-and-completions -- target/cli-docs
```
The example reuses the CLI's clap definitions without running a command, scanning fonts or opening
a window. It creates the output directory if needed and overwrites its generated files on a
second run. The output directory is its only argument. A missing or extra argument is a usage
error (exit 2); a filesystem error exits with 1.
The directory contains completions for five shells:
| Shell | File |
|---|---|
| Bash | `figcraft-cli.bash` |
| Elvish | `figcraft-cli.elv` |
| Fish | `figcraft-cli.fish` |
| PowerShell | `_figcraft-cli.ps1` |
| Zsh | `_figcraft-cli` |
It also contains `figcraft-cli.1` and one man page per subcommand, including nested commands such
as `figcraft-cli-plugin-publish.1`. For example, `man target/cli-docs/figcraft-cli-export.1` shows
the export subcommand's options. Generating these files doesn't install them; package installation
is still outstanding in [#1396](https://github.com/storytold/figcraft/issues/1396).
## Fonts
+3 -1
View File
@@ -1,6 +1,6 @@
# FigCraft roadmap: milestones and what's next
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (`.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); compact layer trees with fields and outline (M0.9, part of #996); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; the server key is checked by comparing SHA-256 hashes of the presented and the kept key, as the deployment guide says (#1130); opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and commenLine truncated
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (a development example generates figcraft-cli's shell completions and man pages from its clap definitions (part of #1396); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); compact layer trees with fields and outline (M0.9, part of #996); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; the server key is checked by comparing SHA-256 hashes of the presented and the kept key, as the deployment guide says (#1130); opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows wherLine truncated
Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and
[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours
@@ -48,6 +48,7 @@ blockers.
| Milestone | Status | Left | Hours |
|---|---|---|---|
| M0 Foundation: workspace, model, renderer, engine, UI shell, app, control channel | done (2026-10-09) | — | — |
| M0.9 CLI + MCP, web build, release packaging, PDF export | done (v0.2.0 released 2026-10-10 for every platform; a development example generates CLI completions and man pages, part of #1396) | Package installation of the generated files is still outstanding | — |
| M0.9 CLI + MCP, web build, release packaging, PDF export | done (v0.2.0 released 2026-10-10 for every platform). Added since: compact inspection with `fields` and `outline` (part of #996) | — | — |
| M1 Core editing completeness (tools, panels, menus, clipboard, files) | ~77% | interaction pass (Open recent, the outline mode variants and keyboard box selection landed 2026-10-11; repeat commands preserve the underlying edit and undo steps, #181; the Layout panel's constrain-proportions link scales the other side, #220) | 6–12 + pass |
| M2 Layout depth | ~80% | grid `fr` rows, real-file QA | 6–12 |
@@ -75,6 +76,7 @@ blockers.
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | M0.9, part of #1396: a development example generates completions for Bash, Elvish, Fish, PowerShell and Zsh and a man page for the CLI and each subcommand, nested ones included, from figcraft-cli's clap definitions, without running commands. Generating these files doesn't install them; package installation and the other RFC-0012 exceptions remain outstanding. Regression tests in `apps/figcraft-cli/tests/it/generated_docs.rs` cover artifacts, command names and options, matching Bash dispatch states, repeat generation and filesystem errors. Numbers unchanged |
| 2026-10-30 | minor | M8, #1076: `.fig` ZIP image reads share a total budget, 4 GiB on 64-bit targets and 1 GiB on 32-bit targets, keeping the 256 MiB per-image cap. Members that do not fit are skipped before allocation with an import warning; failed reads consume their bytes, repeated references keep their cached resolution. Embedded message images, decoded pixel memory and native `.figcraft` reads are unchanged. Regression tests in `fig::tests::image_budget` (format). Numbers unchanged. |
| 2026-10-30 | minor | M3, follow-up to #1542: stored variable and collection references use exact ids in export, library copies and swaps, imported aliases, alias deletion and cycle checks, Dev Inspect, the library mode picker, and prototype actions and expressions. Interaction edits resolve only incoming names and leave stored references alone. Command arguments still accept names as before. Tests: the `stored_variable_ids_*` tests in the doc, engine, format and ui-egui present and responsive suites. Numbers unchanged |
| 2026-10-30 | minor | M8, #1114 (the rest): documents, placed images and CLI scripts read by path are regular files within caps (16 GiB, 256 MiB, 1 GiB); folders, devices and pipes are refused before anything is read. Tests: `path_read_caps_tests` |