Adding a Menu Action
Flux has two context-menu surfaces:
- Primary right-click - built from
~/.config/flux/menu.rsandsrc/ui/menu/context.rs. Every entry is aCustomAction. Shell commands run viabuiltin::dispatch, user commands go throughhandle_execute_command. - Secondary Ctrl+right-click - built from
~/.config/flux/menus/menu-<category>-<subtype>.rsandsrc/ui/menu/secondary/. Every entry is also aCustomAction, but the parsing, matching and rendering are separate code paths.
This guide is for builtin:: actions, which are registered in the
first surface only. Adding a user-facing shell command to the default
menu.rs is a different procedure - append a line to
src/utils/config/menu.rs::ensure_config_file and add it to
docs/actions.md manually.
Checklist
-
src/ui/menu/editor/builtins.rs- add aBuiltinDefto theBUILTINStable:BuiltinDef { name: "builtin::my_action", scope: "all", // or "file", "directory", "image/all", ... desc: "One sentence describing what the action does.", },The
descfield is rendered into the existingdocs/actions.mdtable, and thedoc_is_in_synctest insrc/doc_cli.rsfails if the committed page drifts from the table. It is also shown verbatim in the "Pick Built-in" dialog in the menu editor. -
src/ui/menu/context.rs- add the dispatch arm. Thematch action.command.as_str() { ... }block has three shapes, pick the one that fits: - Stateless, no target - most actions. Useregister_simplewith a closure that dispatches anAppMsg:"builtin::my_action" => register_simple( &action_group, "my-action", sender, action_toast.clone(), |s, toast| { s.input(AppMsg::MyActionMsg); emit_toast(s, toast); }, ), - **Path-bearing** - the action operates on the file or directory the menu was opened on. Use `register_path_action`: "builtin::my_action" => register_path_action( &action_group, "my-action", sender, target_path, action_toast.clone(), |s, target, toast| { s.input(AppMsg::MyActionMsg { path: target }); emit_toast(s, toast); }, ), - **Selection-based** - the action operates on the whole selection rather than the clicked item. Resolve `self.get_selection()` (or `get_selection_with_meta()` if you need `is_dir`) before the `register_simple` call, like `builtin::install_fonts` does. -
src/model/msg.rs- add theAppMsgvariant(s) the dispatch arm produces, doc-commented. If the action opens a dialog, the message name is usuallyPrompt<X>, if it runs work in the background,<X>Succeeded/<X>Failedpair. -
src/ui/update/*.rs- route the newAppMsgin the closest domain router (file_ops.rs,window.rs,search.rs,sidebar.rs,git.rs,network.rs,dialogs.rs). Everything below the router is ahandle_*method onFluxApp, living insrc/ui/ops/orsrc/ui/dialog/. -
docs/actions.md- do not edit by hand. Runcargo run --bin gen-docsto regenerate it fromBUILTINS. Thegen-docsbinary produces the file, anddoc_is_in_syncinsrc/doc_cli.rs(plus the matching test in the menu editor module) fails CI if the committed file drifts from the generated output. -
po/pt_BR.po- any new user-facing string (toast, dialog title, error message). Group under# src/ui/menu/context.rsif the string originates there, otherwise under the handler file.
Adding a user-visible menu entry to the default menu.rs
If the action should appear in the default context menu without the
user editing menu.rs, add a line to ensure_config_file in
src/utils/config/menu.rs:
" My Action" => "all", "builtin::my_action"
The leading glyph (before the first alphanumeric char) is a Nerd Font
icon. Every existing line in default_config uses one the menu editor
picks it up as a prefix. Do not add a leading space without a glyph,
the alignment breaks.
The line is only written on first run. Existing users will not see the
new entry until they delete ~/.config/flux/menu.rs. Do not attempt to
migrate existing menus automatically - users edit menu.rs by hand,
and silently rewriting it destroys custom entries.
Where the reference code lives
src/ui/menu/context.rs- the dispatch table. Read the fullbuiltin::install_fontsarm, which is the most complex selection-based case, andbuiltin::open_new_tab, which handles multi-tab activation.src/ui/menu/editor/builtins.rs- theBUILTINStable and therender_actions_markdown()renderer that produces the existingdocs/actions.md.src/ui/ops/file/command.rs-handle_execute_command, which is what non-builtin::entries dispatch to. If your action is closer to a shell command than a native operation, consider whether it needs abuiltin::at all.src/ui/menu/secondary/- the Ctrl+right-click surface. Different procedure, every action here comes from a user template file, not fromBUILTINS. Adding abuiltin::to a secondary template works becausehandle_execute_commandhandles both surfaces uniformly.
Behaviour rules
- Never block the main thread. If the action does any I/O, hashing,
or subprocess spawn, dispatch a message to a handler and let the
handler call
relm4::spawn_blockingortokio::spawn. The dispatch arm incontext.rsmust return in microseconds. - Respect the toast contract.
emit_toastfires whenever themenu.rsline has a third argument. Do not suppress it - users rely on it for feedback, and dropping it means a silent action. - Use
get_selection_with_metawhen the result depends onis_dir.get_selectionreturns only paths, if the action branches on file vs directory, resolve the meta once at dispatch time rather than doing astatinside the handler (which races with concurrent renames). - Skip the action when its precondition fails.
context.rsalready does this for%l(line-number) actions andbuiltin::tagfile(which skips directories). Follow the same pattern:continueout of the loop if the target is unsupported, rather than registering an action that will no-op or error. - Do not reuse an action name across the two surfaces. The
action_namefield inCustomActionis what the popover'sSimpleActionregisters against. Two entries with the same name will collide inaction_groupand the second registration wins.
Verifying the result
After wiring up the action, it should:
- appear in the "Pick Built-in" dialog of the menu editor
- appear in
docs/actions.mdaftercargo run --bin gen-docs, with the exactdescstring fromBUILTINS - pass
cargo test --lib(thedoc_is_in_synctest will catch any drift betweenBUILTINSand the committeddocs/actions.md) - run from the primary right-click menu when an entry uses it
- not appear in the primary menu when its
mime_typesrequirement doesn't match the current target - show its toast immediately (for secondary-surface actions) or after
the command exits (for shell commands in
menu.rs), matching the contract documented indocs/actions.md.