Skip to content

commands: a command says whether it applies, and menus grey what does not - #121

Open
zmaril wants to merge 1 commit into
targets/linkedfrom
commands/poll
Open

commands: a command says whether it applies, and menus grey what does not#121
zmaril wants to merge 1 commit into
targets/linkedfrom
commands/poll

Conversation

@zmaril

@zmaril zmaril commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Stacked on #120. First of the Blender-review sequence.

Blender's UI is four ideas: operators, properties, poll(), and context. We had the first two and neither of the last two — every surface offered every command unconditionally. "Close area" sat live in the menu of the only area and failed on the click; the whole workspace.* family stayed live with one workspace.

The field

pub struct Command {
    ...
    /// Whether this could run against the workbench as it is — Blender's
    /// `poll()`. The params may be `Null`: a surface deciding whether to
    /// *offer* a command asks before it has any.
    pub poll: fn(&Workspaces, &Value) -> bool,
    pub run: fn(&mut Workspaces, &Value) -> Result<()>,
}

Most are always — there is no workbench where splitting an area makes no sense, which is exactly why it needs no poll of its own. The ones that aren't:

poll commands
many_areas join, join_into, ratio, swap
many_workspaces workspace.close / cycle / switch / move

run checks it too

Not just the chrome. The chrome isn't the only caller — an agent reaches the same registry — and a command that can't apply should say so once, in one place, rather than failing differently depending on who asked. It refuses before touching anything, so a refused command is never a half-applied one.

Grey, not gone

A row that disappears teaches nothing; a row that's there and dim says this exists, not now. MenuItem::when(available), disabled on the wire type (ts-rs regenerated), and the shim draws it dim and inert.

The detail that matters: disabled rows are excluded from the list the arrows walk — the same list Enter fires and the letter accelerators match. Leaving them in would have let the keyboard run what the pointer refuses.

The library asks the host through a new can callback, because the host holds the registry. A host that offers no answer gets every row live, which is what these menus did before there was a poll to ask.

What I did not do, deliberately

The command palette is still a hardcoded list of five entries plus workspace switches. It never enumerated the registry at all — that's not a design decision anyone made, it's a stub. But enumerating it needs commands to declare their parameters, since most can't run without an area id or a direction, and that's its own piece of work rather than a line in this one. Flagging it rather than half-doing it.

Four new tests, including that run refuses what poll refuses (a poll the chrome respects and run doesn't is a poll an agent walks straight through) and that available shrinks with the workbench.

165 tests pass; fmt clean, clippy clean under -D warnings, shims and docs/reference.md regenerated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XjCx5QszLXroGYh1YBxybw

@github-actions

Copy link
Copy Markdown

powderman preview

https://powderman-pr-121.fly.dev

The workbench, live: split/join/swap areas, the F3 palette, the widgets, themes, tooltips, the status bar.
The fleet and run execution are inert here (a Fly machine has no herdr) — this is for driving the interface.

On branches with the MCP server, point an agent at it:

claude mcp add --transport http powderman-preview "https://powderman-pr-121.fly.dev/mcp"

Updated on every push. Destroyed when this PR closes.

… not

Blender's UI is operators, properties, poll and context. We had the first
two and neither of the last two: every surface offered every command
unconditionally, so "Close area" sat live in the menu of the only area and
failed on the click, and the whole workspace family stayed live with one
workspace.

`Command` grows a `poll`. Most are `always` — there is no workbench where
splitting an area makes no sense, which is exactly why it needs no poll of
its own. The ones that do: join, join_into, ratio and swap want a second
area; close, cycle, switch and move want a second workspace.

`run` checks it too, not just the chrome. The chrome is not the only
caller — an agent reaches the same registry — and a command that cannot
apply should say so once, in one place, rather than failing differently
depending on who asked. It refuses before touching anything, so a refused
command is never a half-applied one.

Menu rows grey rather than vanish. A row that disappears teaches nothing; a
row that is there and dim says "this exists, not now". The shim excludes
disabled rows from the list the arrows walk, which is the same list Enter
fires and the letter accelerators match — leaving them in would have let
the keyboard run what the pointer refuses.

The library asks the host, through a new `can` callback, because the host
holds the registry. A host that offers no answer gets every row live, which
is what these menus did before there was a poll to ask.

Not done here, and worth saying: the command palette is still a hardcoded
list of five entries plus workspace switches — it never enumerated the
registry at all. Enumerating it needs commands to declare their parameters,
since most cannot run without an area id or a direction, and that is its
own piece of work rather than a line in this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjCx5QszLXroGYh1YBxybw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant