Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Flux

A keyboard-first Linux file manager built with GTK4, Libadwaita, and Relm4.

Flux is designed as a highly responsive, native file manager. It avoids blocking the main GTK thread by offloading all I/O, directory reads, and background processing to isolated thread pools.

Architecture at a glance

  • UI: GTK4 + Libadwaita via Relm4 (MVU pattern).
  • Parallelism: Capped Rayon pool for directory scans, sorting, and metadata extraction.
  • Async I/O: Tokio for subprocesses, network operations, and file transfers.
  • State: SQLite (rusqlite) for view settings, tags, folder icons, and location history.
  • Search: Live Nucleo fuzzy filter over the current directory; optional SQLite FTS5 trigram index for recursive filename searches; Aho-Corasick content scanner.
  • Archives: Native Rust for zip/tar/7z, with external tools for rar/deb/udf-iso.

Documentation

Source Code

Flux is open source. You can find the repository and report issues on GitHub.

Demo

A short tour of the headline features: grid view, the embedded terminal, peek-preview, and fuzzy search.

Shot list

TimeFeature shown
0:00Grid view with thumbnails
0:04Embedded terminal (F4), ls command run
0:09Close terminal, return to grid view
0:11Peek-preview panel on an image (Space)
0:12Arrow through image files in peek-preview
0:18Return to grid view
0:22Navigate into "Videos" folder
0:24Peek-preview on video files, navigating through them
0:33Return to Videos folder grid view
0:35Return to main grid view
0:37Search panel / fuzzy search filtering (Ctrl+F)
0:38Close search panel, return to grid view

Screenshots

For a still overview, see screenshots/ in the repository.

Command line

flux [OPTIONS] [PATH]

The binary is built as flux-fm; --help refers to it as flux. PATH may be a plain path or a file:// URI; the prefix is stripped and the rest is not percent-decoded.

Opening a location

PATHResult
noneOpens the home directory.
a directoryOpens it.
a supported archive fileOpens the archive explorer on it.
#name, :tag:name, :t:name or tags://nameOpens the home directory filtered by the tag name.

Options

OptionDescription
-h, --helpPrint this help message
-v, --versionPrint version information
--no-sidebarDisable sidebar for this session
--no-headerDisable header bar for this session
--no-statusbarDisable statusbar for this session
--terminalStart with the terminal open in the configured position
--file-properties PATHOpen the file properties window for PATH
--set-icon TARGET IMAGESet a custom icon/image for TARGET
--cwd PATHNavigate the running window to PATH (starts a new window if none is running)
--set-icons-stdinRead TARGET<TAB>IMAGE pairs from standard input
--reset-icon TARGETReset custom icon for TARGET back to default
--reset-icons-stdinRead TARGET paths from standard input to reset
--quick-list PATH...Open with quick-panel pre-populated with PATHs
--quick-list-stdinOpen with quick-panel populated from standard input
--preview PATHOpen folder PATH with the preview panel in wide mode
--selectionPrint the paths selected in the running window, one per line (-0: NUL-separated)
--toggleShow or hide the running window
--showBring the running window to the front
--hideHide the running window
--theme NAMESet the theme to NAME, or 'default' to remove it
--menu-editorTo manage menu.rs

Session flags

--no-sidebar, --no-header, --no-statusbar and --terminal may appear anywhere on the command line. They apply to the window opened by PATH, a tag, --preview, --quick-list or --quick-list-stdin, and are ignored by the other options.

Every other option must be the first argument once the session flags are removed. Arguments after the first are read only by the options that take values.

--terminal opens the embedded terminal in the position configured under Settings → Terminal: the bottom pane, the right sidebar panel or the full window. Combined with --preview, both open unless the position is full window, where the terminal takes over the whole window and --preview is superseded.

Custom icons

--set-icon stores the icon in config.toml. For a directory it also updates any matching pinned sidebar entry. TARGET and IMAGE are resolved to canonical absolute paths, and the command fails if IMAGE does not exist.

--set-icons-stdin reads one pair per line. Separate the two paths with a tab, or write two words and quote or escape any that contain spaces. Blank lines and lines starting with # are ignored. A bad line is reported on stderr with its line number and skipped, and the command exits with status 1 after applying the rest.

/home/me/Music /home/me/icons/music.svg
"/home/me/My Videos" /home/me/icons/video.svg

--reset-icons-stdin reads one target path per line, with the same rules for blank lines and comments.

Quick list

--quick-list opens the first path and fills the quick panel with all the paths given. Arguments that start with - are not treated as paths. --quick-list-stdin reads one path per line, skipping blank lines and # comments, and opens the home directory when the input is empty.

find . -name '*.log' | flux --quick-list-stdin

Selection

--selection asks a running window for the paths selected in its active tab and prints them, one per line. It talks to the window named by $FLUX_PID, which Flux sets in its embedded terminal, and otherwise to the window that navigated last. It does not open a window.

A path containing a newline is skipped with a warning; --selection -0 terminates each path with NUL instead, which keeps every path intact for xargs -0.

flux-fm --selection | xargs -d '\n' ls -l

Window visibility

--toggle, --show and --hide take no arguments and control a window that is already running. --toggle flips its visibility, --show makes it visible and brings it to the front, and --hide hides it. --show and --hide are idempotent.

Each window listens on $XDG_RUNTIME_DIR/flux/toggle-<pid>.sock, or under $XDG_CACHE_HOME/flux/ when there is no runtime directory. The command connects to the window with the highest PID, sends one line (toggle, show or hide) and reads one line back, visible or hidden, which is the state the window ended up in.

When no window is running, --toggle and --show open the home directory as if no arguments were given, and --hide does nothing and exits with status 0.

flux-fm --toggle

Theme

--theme NAME stores NAME as the theme in config.toml and exits without opening a window. Running windows apply it as soon as the file changes. NAME is the file name of an installed theme without .css, such as one in ~/.local/share/flux/themes; default removes the theme. An unknown name lists the available themes on stderr and exits with status 1.

flux-fm --theme default

Exit status

StatusMeaning
0Success. Missing values for --file-properties, --set-icon, --reset-icon, --theme and --quick-list print the help text and also exit with 0.
1Unrecognized option, --preview without a directory, an icon file that does not exist, a skipped line in --set-icons-stdin, an unknown --theme name, or --selection finding no running window.

Configuration

Flux reads ~/.config/flux/config.toml. The file is created on first run and reloaded while Flux is running. If it fails to parse, Flux prints the error to stderr and runs with built-in fallback values until the file is fixed.

Every key is optional. Default below is the value used when a key is missing from a file that has a [ui] table. The file written on first run sets some keys differently, so a fresh install can differ from this table.

Reference

Top level

KeyDefaultDescription
default_list_modefalseOpen folders in list mode instead of grid mode.
network_bookmarks[]Saved network locations; see Array tables.
sidebar[]Custom sidebar entries; see Array tables.

[shortcuts]

KeyDefaultDescription
backunsetGo back in navigation history.
change_iconunsetOpen the folder icon picker.
close_tabunsetClose the current tab.
copy_pathunsetCopy the absolute paths of the selected items.
create_hardlinkunsetCreate hardlinks from clipboard paths. Directories are not supported.
create_symlinkunsetCreate symlinks from clipboard paths, auto-renaming conflicts.
cycle_sortunsetRotate through the sort fields.
deleteunsetMove the selected items to the trash.
forwardunsetGo forward in navigation history.
homeunsetGo to the home directory.
menu_editorunsetOpen the menu editor.
new_tabunsetOpen a new tab.
next_tabunsetSwitch to the next tab.
openunsetOpen the selected file or enter the directory.
open_propertiesunsetOpen the properties window.
prev_tabunsetSwitch to the previous tab.
quitunsetQuit the application.
refreshunsetReload the current directory.
renameunsetStart an inline rename.
reset_iconunsetReset a folder icon to the default.
rootunsetGo to the root directory.
searchunsetFocus the search interface.
settingsunsetOpen the settings window.
toggle_folders_firstunsetToggle folder grouping for the active folder.
toggle_headerunsetShow or hide the header bar.
toggle_hiddenunsetShow or hide hidden files.
toggle_sort_orderunsetToggle between ascending and descending order.

[ui]

KeyDefaultDescription
actions_panel_width380Width in pixels of the right-side contextual-actions panel.
archive_thumbnail_cachefalseStore thumbnails of files inside zip archives in a hidden .flux-cache/ folder in the zip itself.
ascendingtrueSort listings in ascending order.
auto_generate_mime_iconstrueGenerate and cache an SVG document icon when the icon theme has none.
auto_list_threshold300Folders with more items than this open in list mode. 0 disables the auto-switch.
auto_mime_accent_color"#1273b2"Accent color of auto-generated document icons.
auto_mime_body_color"#e4e4e4"Hex color of the page body of auto-generated document icons.
auto_mime_font_color"#ffffff"Hex color of the extension label inside auto-generated document icons.
auto_mime_font_size13.0Base font size of auto-generated document icons.
auto_show_difftrueOpen the diff panel when a modified git file is selected.
autoplay_video_previewstruePlay a muted loop preview when a single video card is selected.
bg_alpha_sidebar_left0.65Tint opacity (0.0 to 1.0) over the left sidebar background image.
bg_alpha_sidebar_right0.65Tint opacity (0.0 to 1.0) over the right panel background image.
bg_alpha_window0.65Tint opacity (0.0 to 1.0) over the main window background image.
breadcrumb_count3Maximum number of path segments shown as breadcrumbs in the header bar; only the last ones are kept.
breadcrumb_max_chars20Characters shown in a breadcrumb label before truncation.
content_search_max_file_mb128Skip files larger than this many MiB during content search. 0 means no cap.
current_folders_first{}Per-folder override of folders_first, keyed by absolute path.
default_icon_size0Default pixel size of file and folder icons.
default_sort"Name"Fallback sort field: Name, Date, Size or Type.
device_renames{}Display names and icons for devices and partitions; see Array tables.
diff_editor"nvim"Editor command used when opening a diff hunk location.
diff_panel_width420Width in pixels of the right-side git diff panel.
disable_drag_and_dropfalseDisable drag and drop of files and folders.
enable_file_indexingfalseEnable the FTS5 filename indexer.
expand_labelsfalseWrap filenames over multiple lines instead of truncating them.
ffmpeg_auto_rotatefalseLet FFmpeg rotate portrait video thumbnails.
ffmpeg_seek_seconds5.0Seek position in seconds when grabbing a video thumbnail frame.
ffmpeg_threads1Threads per FFmpeg process when extracting video frames.
file_icons{}Per-file custom icon image, keyed by absolute path. Managed by --set-icon.
fish_themefalseGenerate a fish conf.d snippet so the embedded shell's colors and prompt follow the GTK theme. Off by default.
folder_cache_capacity3Number of recently visited directory snapshots kept in memory.
folder_grid_thumbnailstrueShow image thumbnails in the folder grid of the preview panel.
folder_icon_size{}Per-folder icon size override, keyed by absolute path.
folder_icons{}Per-folder custom icon image, keyed by absolute path. Managed by --set-icon.
folder_sort{}Per-folder sort field override, keyed by absolute path.
folders_firsttrueGroup directories above files.
grid_spacing10Pixel spacing between items in the file grid.
header_show_filtertrueShow the filter button in the header bar. Ctrl+Shift+F toggles the filter bar while it is hidden.
header_show_previewtrueShow the preview-panel toggle in the header bar.
header_show_sorttrueShow the sort indicator in the header bar.
header_show_viewtrueShow the list/grid toggle in the header bar. Ctrl+Shift+M toggles the mode while it is hidden.
header_visibletrueShow the header bar. Persisted across sessions.
hidden_extensions[]Extensions to hide from grid labels, for example ["desktop", "AppImage"].
lazy_thumbnailsfalseGenerate thumbnails only for items scrolled into view.
list_icon_size24Pixel size of icons in list mode.
loader_batch_size10Items dispatched per chunk into the grid while a folder loads.
max_content_search_results0Maximum results returned by content search.
max_history100Back and forward history entries kept.
max_search_results100Maximum matches during recursive filename and extension search.
max_width_chars20Characters shown in a file label before truncation.
panel_menu_requires_ctrlfalseOpen left-sidebar, preview-panel and quick-list context menus only on Ctrl+right-click; a plain right-click does nothing.
preview_panel_width400Width in pixels of the right-side preview panel.
preview_syntax_highlightingtrueSyntax-highlight source files in the preview panel.
recents_row0Zero-based position of the Recents row among the [[sidebar]] entries. No effect when show_recents is false.
scale_font_with_iconsfalseScale label font size together with the grid icons.
search_panel_width350Width in pixels of the right-side search panel.
show_csdfalseDraw client-side window decorations.
show_empty_dir_emblemfalseShow an emblem on folders that contain no visible items.
show_hidden_by_defaultfalseShow hidden files at startup.
show_recentstrueShow the Recents location in the sidebar.
show_symlink_emblemtrueShow a badge and target hint on symbolic links.
show_thumbnailstrueMaster switch for thumbnail generation; overrides thumbnail_types.
show_xdg_dirsfalseShow the XDG user directories, such as Documents and Downloads, in the sidebar.
sidebar_menu_requires_ctrlfalseOpen sidebar context menus only on Ctrl+right-click; a plain right-click does nothing.
sidebar_visibletrueShow the sidebar. Persisted across sessions.
sidebar_width0Width in pixels of the left sidebar.
single_clickfalseActivate items with a single click.
start_maximizedfalseOpen the window maximized.
startup_window_height0Initial window height in pixels.
startup_window_width0Initial window width in pixels.
tag_panel_width350Width in pixels of the right-side tag panel.
themeunsetName of the theme to apply, such as default, os-dark or os-light.
thumbnail_size256Target size in pixels of generated thumbnails.
thumbnail_threads4Maximum concurrent thumbnail workers.
ui_scale1.0Global UI scale multiplier.
wide_previewfalseOpen the preview panel in wide mode, with the sidebar hidden.
window_controls_leftfalsePut window controls on the left instead of the right.

[ui.terminal]

KeyDefaultDescription
bg_color"#1A1A1A"Terminal background color.
fg_color"#E5E5E5"Terminal foreground color.
font"JetBrains Mono 13"Terminal font as a Pango font description.
height30Terminal pane height.
position"bottom"Where F4 opens the terminal: bottom pane, right sidebar panel, or fullscreen window.
shellunsetShell started in the terminal pane; the user's login shell when unset.

[ui.thumbnail_types]

KeyDefaultDescription
audiotrueThumbnails and embedded artwork for audio files.
executablesfalseThumbnails for Windows .exe files.
fontstrueThumbnails for font files.
imagestrueThumbnails for image files.
pdfstrueThumbnails for PDF documents.
videostrueThumbnails for video files.

Array tables

[[sidebar]] adds a sidebar entry. path accepts a leading ~. Set kind = "label" to make the entry a section heading instead of a location.

[[sidebar]]
name = "Projects"
icon = "folder-symbolic"
path = "~/Projects"

[[network_bookmarks]] saves a network location. icon is optional.

[[network_bookmarks]]
name = "NAS"
uri = "smb://nas.local/share"

[ui.device_renames] maps a device path to a display name and an optional icon:

[ui.device_renames]
"/path/to/device/" = { name = "Storage", icon = "drive-harddisk-solid-symbolic" }

Default configuration

The same defaults as one file, as Flux would serialize it. Keys that are unset, such as ui.theme and the [shortcuts] entries, are omitted.

sidebar = []
network_bookmarks = []
default_list_mode = false

[ui]
diff_editor = "nvim"
preview_panel_width = 400
diff_panel_width = 420
auto_show_diff = true
ui_scale = 1.0
content_search_max_file_mb = 128
enable_file_indexing = false
bg_alpha_window = 0.65
bg_alpha_sidebar_left = 0.65
bg_alpha_sidebar_right = 0.65
scale_font_with_icons = false
hidden_extensions = []
tag_panel_width = 350
search_panel_width = 350
actions_panel_width = 380
show_symlink_emblem = true
header_visible = true
header_show_filter = true
header_show_sort = true
header_show_view = true
header_show_preview = true
sidebar_menu_requires_ctrl = false
panel_menu_requires_ctrl = false
auto_mime_body_color = "#e4e4e4"
auto_mime_font_color = "#ffffff"
auto_mime_font_size = 13.0
auto_generate_mime_icons = true
auto_mime_accent_color = "#1273b2"
autoplay_video_previews = true
thumbnail_size = 256
window_controls_left = false
show_empty_dir_emblem = false
ffmpeg_threads = 1
ffmpeg_seek_seconds = 5.0
ffmpeg_auto_rotate = false
loader_batch_size = 10
folder_cache_capacity = 3
thumbnail_threads = 4
max_search_results = 100
max_history = 100
breadcrumb_count = 3
breadcrumb_max_chars = 20
max_content_search_results = 0
default_icon_size = 0
list_icon_size = 24
sidebar_width = 0
show_xdg_dirs = false
single_click = false
default_sort = "Name"
folders_first = true
show_hidden_by_default = false
show_csd = false
start_maximized = false
startup_window_width = 0
startup_window_height = 0
max_width_chars = 20
grid_spacing = 10
ascending = true
expand_labels = false
sidebar_visible = true
show_recents = true
recents_row = 0
show_thumbnails = true
lazy_thumbnails = false
disable_drag_and_drop = false
preview_syntax_highlighting = true
wide_preview = false
folder_grid_thumbnails = true
archive_thumbnail_cache = false
fish_theme = false
auto_list_threshold = 300

[ui.file_icons]

[ui.terminal]
height = 30
fg_color = "#E5E5E5"
bg_color = "#1A1A1A"
font = "JetBrains Mono 13"
position = "bottom"

[ui.current_folders_first]

[ui.folder_sort]

[ui.folder_icon_size]

[ui.device_renames]

[ui.folder_icons]

[ui.thumbnail_types]
images = true
videos = true
fonts = true
pdfs = true
executables = false
audio = true

[shortcuts]

Renamed and removed keys

Old keyReplaced bySinceNote
ui.terminal_in_sidebarui.terminal.position0.6.0Replaced by the three-way position enum. true loads as "sidebar", false or missing as "bottom".

Actions

The right-click menu is defined by ~/.config/flux/menu.rs. Ctrl+right-click and the contextual actions panel read a menu-*.rs template instead, chosen by the MIME type of the item: menu-<category>-<subtype>.rs, else menu-<category>-all.rs, else menu-all.rs. Templates are looked up in ~/.config/flux/menus/ first, then in ~/.config/flux/. Every line of these files is:

"Label" => "mime_types", "command"[, "toast"][, "no_command_dialog"]

command is either a shell command or one of the builtin::* actions listed below.

Placeholders

Shell commands may contain the following placeholders. Which one is used depends on how many items the command targets: the selection, or the item the menu was opened on when it is not selected, or the current directory when there is neither.

PlaceholderOne targetSeveral targets
%pAbsolute path of the itemAll the paths, shell-quoted and separated by spaces
%dParent directory of the itemThe current directory
%fFile name of the itemNot substituted
%lLine number of the content-search hit, 0 when the item is not a hit0

Every substituted path is wrapped in single quotes, with any ' inside it escaped. A "%p" or '%p' written in the command is replaced together with its quotes, so don't add your own. If a path contains a newline, a carriage return or a NUL byte, the command is not run and Flux shows a toast.

A menu entry whose command contains %l is shown only for a content-search hit, and never in the contextual actions panel.

The toast of a menu.rs shell command is shown after it exits, and a failed command without a toast shows Command failed. An entry from a menu-*.rs template shows its toast as soon as it is clicked.

Builtin actions

The following actions are recognised when command starts with builtin::. They are handled directly by the application and never spawn a shell.

ActionScopeDescription
builtin::copyallCopies the selected files or folders to the clipboard.
builtin::cutallCuts the selected files or folders to the clipboard.
builtin::pastedirectoryPastes items from the clipboard into the current directory.
builtin::open_new_tabdirectoryOpens the selected folder or current location in a new tab.
builtin::renameallTriggers inline filename renaming on the selected item.
builtin::deleteallMoves the selected files or folders to the system trash.
builtin::new_folderdirectoryOpens the new folder creation prompt.
builtin::new_filedirectoryOpens the new file creation prompt.
builtin::toggle_pindirectoryPins or unpins the target folder in the sidebar.
builtin::add_to_quick_listallAdds the selected folder or current directory to the Quick List.
builtin::quick_list_transferallOpens a submenu to move or copy items directly to Quick List slots.
builtin::tagfilefileOpens the file tag editor popover.
builtin::inspect_dirdirectoryOpens the Directory Inspector with disk usage and largest files.
builtin::install_fontsfont/allInstalls the selected font files into ~/.local/share/fonts.
builtin::open_withfilePopulates a dynamic submenu of registered applications for the MIME type.
builtin::open_with_dialogfileOpens the full system application chooser dialog.
builtin::select_folder_icondirectoryOpens the folder icon picker to assign a theme icon.
builtin::set_custom_iconallOpens a file chooser to assign a custom image as file/folder icon.
builtin::reset_custom_iconallRestores the default icon for the selected file or folder.
builtin::set_extension_iconfileSets a custom image icon for the target file's extension globally.
builtin::reset_extension_iconfileResets the custom icon for the target extension back to theme default.
builtin::set_bg_windowimage/allSets the selected image as the main window background.
builtin::set_bg_sidebar_leftimage/allSets the selected image as the left sidebar background.
builtin::set_bg_sidebar_rightimage/allSets the selected image as the right panel background.
builtin::clear_backgroundsallClears all custom background images and resets styling to default.

Worked examples

Flat action - an entry that appears in the top-level context menu:

"Open in Terminal" => "directory", "alacritty --working-directory=%p", "no_command_dialog"

Submenu - the part before > becomes a submenu group:

"Image Convert > To PNG" => "image/webp", "magick %p %p.png"

Placeholder plus toast - %p is substituted, then the toast is shown after the command exits:

"Optimise Image" => "image/png", "oxipng %p", "Image optimised"

Search

Flux has four search surfaces:

  • Live filter - the search bar at the top of the window. A fuzzy, case-insensitive match on the file names of the current directory, updated as you type. No disk walk.
  • Recursive search - starts from the search bar when the query contains * or ? and is longer than one character, or from the advanced panel. Walks the current directory tree.
  • Content search - :term or :.ext:term in the search bar, started with Enter, or the Inside files field of the advanced panel. Reads file contents.
  • Tag filter - triggered by #name, :tag:name, or :t:name. Looks up files by extended-attribute tag.

Size filters

Prepend > or < to a size token, or use a .. range, to filter by file size. A unit is mandatory - a bare number is rejected, because >10 matches everything and would lock the UI on large trees.

UnitByte multiplier
k1024
m1048576
g1073741824
t1099511627776

The ib and b suffixes are stripped before the unit is looked up, so 10MB, 10MiB, and 10M all mean 10 MiB.

In the live filter, the rest of the query is a case-insensitive substring match on the name, and folders always pass the size filter. A query containing * or ? is read as one glob, so it cannot be combined with a size token.

Examples:

>10MB
<500KB
10MB..50MB
report >10MB

Tag filters

Prefix a comma-separated tag list with one of the following. All three are equivalent.

PrefixExample
:tag::tag:work,urgent
:t::t:work,urgent
##work,urgent

Multiple tags are AND-ed. A free-text query after the tag list is also AND-ed:

#work                  - files tagged "work"
#work,urgent           - files tagged both "work" and "urgent"
#work report           - files tagged "work" whose name contains "report"

Prefix the query with : to search inside file contents, then press Enter. Content search never starts while you type. It walks the current directory recursively and returns the first match of each file.

SyntaxMeaning
:TERMSearch all files for TERM
:.EXT:TERMSearch only files with extension EXT for TERM

The term must be at least 3 bytes long, not 3 characters. Multiple extensions can be comma-separated:

:hello
:.rs:hello
:.rs,py:hello

Details:

  • Matching ignores case for ASCII letters only.
  • Files are read in blocks of 256 KiB.
  • A file is skipped when a NUL byte appears in its first 1024 bytes.
  • A file larger than ui.content_search_max_file_mb is skipped. 0 removes the limit.
  • The walk honors .gitignore, .ignore, the git global and exclude files and the ignore files of parent directories. It follows symlinks and skips hidden files unless the session shows them.

Recursive walk

The search bar starts a recursive walk when the query contains * or ? and is longer than one character. The whole query is then one glob pattern. A bare .ext does not start a walk, it is a fuzzy name filter.

The advanced panel starts a walk when Search inside subfolders, Only search folders or Regular expression is on, or when File name contains * and Exact match is off.

The walk matches files only, never directories, unless Only search folders is on. It honors the same ignore files and symlink rules as content search, and it stops at ui.max_search_results results.

Results are shown in list mode, one row per file, with a path relative to the current directory. They reach the window in batches: a batch is flushed when it holds 500 results or after 16 ms, whichever comes first, so the GTK main loop is never saturated. The batching applies to this walk, not to content search.

Advanced panel

Ctrl+F (shortcuts.search) toggles the Search panel on the right and switches the header to the search bar. The panel also opens from the Search entry in the sidebar, a place with the path search:// that the first-run config adds.

FieldBehavior
File name,-separated terms. A plain term becomes *term*, and words separated by spaces become *a*b*. A term that starts or ends with * is used as typed. A * makes the search recursive unless Exact match or Regular expression is on.
Exact matchThe term, lowercased, with no wildcards added.
Regular expressionA case-insensitive regex matched against the file name. Makes the search recursive. An invalid expression shows an error and nothing runs.
Inside filesContent search term, at least 3 bytes. When it is filled, File name, Exact match, Regular expression, Search inside subfolders, Only search folders and Include hidden files are ignored. Pattern / Extension is passed as the extension filter of the content search, so it takes bare extensions such as rs, py.
Pattern / Extension,-separated. A bare word such as rs becomes *.rs, and an entry containing / is a MIME type expanded to its extension globs. When File name is also filled, the two are combined into one pattern.
Search inside subfoldersOn by default. When off, the search only sets an extension filter on the current folder.
Only search foldersMakes the search recursive and matches directory names.
Include hidden filesAdds hidden files to the walk. It cannot hide files that the session's hidden-files toggle shows. Content search ignores this switch and follows the session toggle.
ModifiedAny time, last hour, last 24 hours (Today), last 7, 30 or 365 days. Applied by the recursive walk.
File sizeLarger or smaller than a number of KB, MB or GB, in units of 1024. The comparison is strict. Applied by the recursive walk.

Shell integration

A shell can ask a running Flux window what is selected and which folder it shows, and it can tell the window to navigate. The command-line pieces work in any shell. A set of fish helpers is built on top of them.

In any shell

CommandResult
flux-fm --selectionPrints the paths selected in the active tab, one per line. -0 ends each path with NUL instead.
flux-fm --cwd PATHNavigates the running window to PATH. Starts a window if none is running.
flux-fm --toggle, --show, --hideShow or hide the running window.

--selection needs no display and talks to the window named by $FLUX_PID, which Flux sets in its embedded terminal. Outside it, the window that navigated last answers. See Command line for the exit statuses.

# Long listing of the selected files
flux-fm --selection | xargs -d '\n' ls -l

# Safe for any file name, including ones with a newline
flux-fm --selection -0 | xargs -0 tar -cf selection.tar

# Open a folder in the running window
flux-fm --cwd ~/Projects

A path that contains a newline is skipped with a warning unless you use -0.

Fish helpers

HelperDescription
flux_selectionPrints the selected paths, one per line. -n or --names prints base names. -d or --dir prints the folders that contain them, sorted and unique. Prints nothing when no window is running or nothing is selected.
flux_cwdPrints the folder shown in the grid. Archives, Trash, Recents and network locations are skipped, so you get the last real folder.
flux_goto [PATH]Navigates the open window to PATH, default .. A file means its parent folder. If no live Flux window is found it starts one at that folder. Fails with status 1 when the path does not exist.
$FAbbreviation that expands to (flux_selection) as you type, in any position on the line.
$FNExpands to (flux_selection --names).
$FDExpands to (flux_selection --dir).
$FCWDEnvironment variable set before every command to the folder shown in the grid.
$FLUX_SELECTIONEnvironment variable holding the escaped selection, set only for a command line that mentions FLUX_SELECTION.

Because the abbreviations expand in the command line before you press Enter, you see the real paths first.

cp $F ~/backup/        # copy what is selected in Flux
ls -l $FN              # names only
cd (flux_cwd)          # follow the folder Flux shows
flux_goto ~/Downloads  # make Flux follow you

Where the helpers are available

Embedded terminal. F4 opens the terminal in the position set under [ui.terminal]. The shell is ui.terminal.shell, else $SHELL, else /bin/bash. The helpers need fish.

The embedded terminal only defines the helpers when ui.fish_theme is true. They come from the generated ~/.config/fish/conf.d/flux-theme.fish, and Flux writes that file and exports its FLUX_THEME_* variables only when the option is on. The option is off by default.

Any terminal. Install the integration file. It is generated into data/fish/flux-integration.fish in the source tree and defines the same helpers:

cp data/fish/flux-integration.fish ~/.config/fish/conf.d/flux-integration.fish

You can also source it from config.fish. Loading it together with the theme snippet only redefines the same functions.

completions/flux-fm.fish completes every option, for flux-fm and for flux. Copy it to ~/.config/fish/completions/.

Matching the embedded prompt to the theme

With ui.fish_theme = true, Flux writes ~/.config/fish/conf.d/flux-theme.fish and rewrites it on every theme change. It maps the GTK theme colors to fish's fish_color_* variables and redefines fish_prompt so the ❯❯❯ glyph follows the theme. Every line is guarded by FLUX_THEME_FG, so the file does nothing outside Flux's terminal.

If a flux-theme.fish that Flux did not write already exists, it is renamed to flux-theme.fish.bak once and never overwritten again. Turning the option off stops Flux from exporting the theme variables, so shells started afterwards are not themed.

How it works

The shell side is plain files and sockets, so you can use them from any language.

PathPurpose
$XDG_CACHE_HOME/flux/cwdThe folder shown in the window that navigated last. XDG_CACHE_HOME defaults to ~/.cache.
$XDG_CACHE_HOME/flux/pidThe process id of that window.
$XDG_CACHE_HOME/flux/gotoA shell writes a folder here. The window named in pid navigates to it and clears the file, so a request is never replayed.
$XDG_RUNTIME_DIR/flux/selection-<pid>.sockA window answers a connection with its selection, each path ending in NUL. Falls back to the cache folder when there is no runtime folder.
$XDG_CACHE_HOME/flux/ipc.sockServes --cwd. The folder is 0700, the socket 0600, and the client checks that the server runs as the same user.

Selection is not cached. The window resolves it on its main thread when asked, so the answer always matches what you see.

Troubleshooting

SymptomCause and fix
$F expands to nothingNo window is running or nothing is selected. Run flux-fm --selection. It prints flux: no running window or flux: window N is not answering and exits with 1.
$F or flux_goto is undefined in the embedded terminalTurn on ui.fish_theme, or install flux-integration.fish.
Another window answersWith several windows, the one that navigated last answers, except in Flux's own terminal, where $FLUX_PID pins the window.
flux_goto opens a new windowThe pid file is stale or names a process that is not flux-fm. Navigate once in the window you want to refresh it.

CSS variables

Flux styles its interface with a built-in stylesheet (src/ui/style.css). Two kinds of variables are available to it, and to your own CSS:

  • Flux variables - custom properties such as --shadow, declared once and read with var(--name).
  • GTK and Adwaita colors - named colors such as @accent_bg_color, provided by the toolkit and read with @name.

Overriding

Put overrides in ~/.config/flux/style.css. It loads on top of the active theme, always wins, and is reloaded when the file changes. To retune a Flux variable, redeclare it in :root:

:root {
  --font-mono: "Fira Code", monospace;
  --shadow: alpha(black, 0.4);
}

Flux variables

Declared in the built-in stylesheet. Uses counts the var() reads in that file.

VariableDefaultDeclared inUsesPurpose
--border-coloralpha(var(--color-fg), 0.1):root16Hairline borders on the sidebar, list rows, inputs, tooltips and toasts.
--color-accent@accent_bg_color:root24Primary accent background for selected and active elements.
--color-accent-fg@accent_fg_color:root34Text and icons drawn on top of --color-accent.
--color-accent-text@accent_color:root47Accent color for text, icons and borders on a plain background.
--color-card-bg@card_bg_color:root1Background color of file cards in the grid view.
--color-danger@red_3:root22Errors and destructive actions.
--color-fg@theme_fg_color:root60Default foreground color used across the UI.
--color-git-mod@blue_3:root2Git modified and mixed file states.
--color-git-staged@purple_3:root2Git staged file states.
--color-git-untracked@dark_2:root2Git untracked file states.
--color-headerbar-bg@headerbar_bg_color:root4Background color of the header bar and status bar.
--color-headerbar-fg@headerbar_fg_color:root9Foreground text and icons in the header bar.
--color-popover-bg@popover_bg_color:root1Background color of popovers and context menus.
--color-popover-fg@popover_fg_color:root12Foreground text and icons in popovers.
--color-sidebar-bg@sidebar_bg_color:root0Background color of the left sidebar.
--color-success@green_4:root4Success states and git additions.
--color-warn@orange_3:root8Warnings, restricted items and git conflicts.
--color-window-bg@window_bg_color:root6Main window background color.
--color-window-fg@window_fg_color:root14Main window foreground text and icons.
--disabled-opacity0.4:root4Opacity of disabled controls and of items being dragged.
--font-maininherit:root8Font family for interface text; inherit follows the system font.
--font-mono"JetBrains Mono", monospace:root9Monospace font family for list info, key caps, git badges and status text.
--shadowalpha(black, 0.25):root3Drop-shadow color of cards, popover menus and the selected sidebar row.
--shadow-hoveralpha(black, 0.35):root1Drop-shadow color of a hovered card.

GTK and Adwaita colors

These follow the system light or dark preference and accent color, so prefer them over hard-coded colors. Uses counts the references in the built-in stylesheet.

Named colors

ColorRoleUses
@accent_bg_colorAccent background for selected and active elements.2
@accent_fg_colorText and icons drawn on top of @accent_bg_color.3
@accent_colorAccent for text, icons and borders on a plain background.1
@window_bg_colorWindow background.1
@window_fg_colorWindow text and icons.1
@theme_fg_colorDefault foreground; also the base for translucent text, borders and hover tints.3
@headerbar_bg_colorHeader bar background.1
@headerbar_fg_colorHeader bar text and icons.1
@popover_bg_colorPopover background; the quick panel uses it.2
@popover_fg_colorPopover text; also the base for translucent tints on popovers.4
@sidebar_bg_colorSidebar background.1
@card_bg_colorBackground of file cards.1

Palette colors

Fixed GNOME palette colors. Unlike the named colors above, they do not change with the theme.

ColorRoleUses
@red_3Errors and destructive actions: broken symlinks, delete buttons, window close.1
@orange_3Warnings and restricted items: locked files, cut items, git conflicts.1
@green_4Success: full level bars and git added.1
@blue_3Git modified and mixed.1
@purple_3Git staged.1
@dark_2Git untracked.1

Development

Building from Source

Flux links against GTK4, Libadwaita, and several C libraries. The build will fail with pkg-config errors if these dependencies are missing.

Arch Linux

sudo pacman -S base-devel git pkgconf rust \
  gtk4 libadwaita glib2 \
  poppler-glib fontconfig pango cairo gdk-pixbuf2 \
  vte4

Debian / Ubuntu

sudo apt install build-essential pkg-config git curl \
  libgtk-4-dev libadwaita-1-dev libglib2.0-dev libglib2.0-dev-bin \
  libpoppler-glib-dev libfontconfig1-dev \
  libpango1.0-dev libcairo2-dev \
  libgdk-pixbuf-2.0-dev \
  libvte-2.91-gtk4-dev

Fedora

sudo dnf install gcc gcc-c++ make pkgconf-pkg-config git \
  gtk4-devel libadwaita-devel glib2-devel \
  poppler-glib-devel fontconfig-devel pango-devel cairo-devel \
  gdk-pixbuf2-devel \
  vte291-gtk4-devel

Build

git clone https://github.com/killown/flux.git
cd flux
cargo build --release

The compiled binary is located at target/release/flux-fm.

Regenerating Documentation

Five pages under docs/ are generated from source: cli.md, config.md, css-variables.md, actions.md and search.md. The same command also writes completions/flux-fm.fish and data/fish/flux-integration.fish. Every other page is written by hand.

If you change CLI options, configuration fields, built-in actions, search syntax, or CSS variables, regenerate the documentation:

cargo run --bin gen-docs

CI runs cargo run --bin gen-docs and fails if git diff --exit-code docs/ detects changes. This keeps the documentation, --help output, and source code in sync.

Optional Dependencies

These tools are not required to build Flux, but they extend its preview and file-opening capabilities. Missing tools are logged to stderr at startup.

ToolEnables
ffmpeg, ffprobeVideo thumbnails and media duration
7z (p7zip-full / p7zip)AppImage icons and UDF ISO support
unar or unrarRAR archive browsing
exiftoolPrimary RAW camera thumbnail support
raw-thumbnailer / miniaturoFallback RAW camera thumbnail support
wrestool, icotool.exe icon extraction
mpvAudio playback in the preview panel
soffice / libreofficeOffice document previews
fc-cacheRefreshing the font cache after font installation

Development Profile

The local profile in Cargo.toml inherits from dev but uses opt-level = 2 for optimized builds.

Build using this profile:

cargo build --profile local

The helper script tools/local-build.sh builds with this profile, regenerates the documentation, and installs Flux:

./tools/local-build.sh

Where to go next

  • Architecture explains the crates, the message flow and the source layout.
  • Testing and CI explains tools/tests.sh and the documentation checks.
  • The Adding ... guides walk through the common extension points.

Architecture

A map of the codebase for contributors. Read it before the how-to guides: Adding a right panel, Adding an archive backend, Adding a menu action, Adding a search filter and Adding a keybinding.

Crates and entry points

Cargo.toml defines three targets:

TargetEntry pointPurpose
library fluxsrc/lib.rsArgument parsing, the doc generators, and the code that the integration tests link against.
binary flux-fmsrc/main.rs, which calls cli::run()The application.
binary gen-docssrc/bin/gen-docs.rsRegenerates the derived documentation.

src/main.rs declares its own mod model, mod services, mod ui and mod utils. The same files are therefore compiled twice, once into the library and once into the binary. This has consequences:

  • cargo fmt --check and cargo clippy report a problem once per crate.
  • A crate-level lint such as #![allow(clippy::result_large_err)] must appear in both lib.rs and main.rs.
  • A type that lives in model exists in both crates and the two are not interchangeable. flux::args::WindowOptions is the library's type, and cli/launch.rs copies it field by field into the binary's own.

Startup

cli::run resolves the command line with flux::args::resolve_startup_action and then picks one of these paths:

  1. Commands that need no display run first and exit: --selection, --toggle, --show and --hide. They are cheap enough to be called from every $F expansion in a shell.
  2. The IPC server for --cwd starts before any client can connect. If another window already owns the socket, --cwd acts as a client and exits.
  3. One-shot commands such as --theme, --set-icon and --reset-icon change config.toml and exit without a window.
  4. Everything else reaches cli/launch.rs, which builds an AppInit and runs the Relm4 component FluxApp.

Message flow

The UI follows Relm4's model, view, update pattern.

  • FluxApp in src/model/app.rs is the model. It owns the state and the widget references.
  • AppMsg in src/model/msg.rs is every event the app can receive. Each variant is doc-commented.
  • FluxApp::handle_update in src/ui/update/mod.rs is the single entry point for messages. It passes the message through a chain of routers in this order: navigation, loading, view, thumbnails, search, file_ops, dialogs, context_menu, network, git, window and sidebar.
  • Each router exposes handle(app, message, sender) -> Result<(), AppMsg>. Ok(()) means it consumed the message. Err(message) hands the message to the next router.
  • A message that no router claims is logged as [flux] unhandled AppMsg. A new AppMsg variant that you never routed shows up there, not as a compile error.

Routers stay thin. The real work lives in handle_* methods on FluxApp under src/ui/ops/ and src/ui/dialog/.

Threads

GTK objects belong to the main thread. Everything that can block runs elsewhere and reports back with an AppMsg.

  • Background threads reach the app through model::SENDER, a global relm4::Sender<AppMsg>. The --cwd server, the goto watcher and the selection server all use it.
  • Questions that need the UI are forwarded as a message with a reply channel. The selection server sends AppMsg::QuerySelection { reply } and waits up to two seconds for the main thread to answer.
  • Directory loading runs on a dedicated Rayon pool in src/services/loader/pool.rs. It has between 4 and 8 threads with 2 MiB stacks, so the first large folder does not commit the global pool's much larger stacks. Results reach the grid in batches of ui.loader_batch_size items.
  • Subprocesses, network work and transfers use Tokio, and short blocking jobs use relm4::spawn_blocking. A dispatch arm or message handler on the main thread should return in microseconds.

Persistence

  • config.toml is read through utils::load_config, which keeps a cached copy. save_config primes that cache and invalidate_config_cache drops it. A config watcher in src/cli/watcher.rs reloads the file while Flux runs. Migrations of renamed keys run when the file is loaded, for example ui.terminal_in_sidebar, and are listed in Configuration.
  • SQLite state lives behind StateManager in src/services/db/. It runs in WAL mode and holds per-folder view settings, location history and file tags, with one module per area: views, history, tags, icons and maintenance.
  • Shell bridge files under $XDG_CACHE_HOME/flux/ are described in Shell integration.

Source map

PathContents
src/args.rs, src/cli/Argument parsing and the non-GUI commands.
src/ipc.rsThe --cwd Unix socket server and client.
src/shell_functions.rsThe fish helper source, shared by the terminal snippet and gen-docs.
src/model/FluxApp, AppMsg, shared types, config/ structs and their defaults.
src/ui/update/The message routers.
src/ui/ops/The handle_* methods that do the work, grouped by area: files, paste, view, tags, tasks, undo, terminal.
src/ui/panels/Right-side panels: tag, search, diff, location, connect, preview, actions and terminal.
src/ui/dialog/, src/ui/properties/Dialogs and the properties window.
src/ui/menu/The context menu, the Ctrl+right-click menu and the menu editor.
src/ui/inputs/, src/ui/keymap/Keyboard handling and the configurable shortcut table.
src/ui/components/The file card and list row, its builder and its bind.
src/ui/settings/, src/ui/help/The settings window and the help window.
src/ui/startup/Building the window, sidebar, tabs and grid.
src/ui/debug/The debug window.
src/services/loader/Directory, archive, network and Recents loading.
src/services/archive/The ArchiveBackend trait and one backend per format.
src/services/search/Content search, the file indexer and the recursive walk.
src/services/thumbnails/, src/services/tasks/Thumbnail generation and the file-operation task queue.
src/services/terminal/The embedded terminal: PTY, emulator, drawing, and the shell bridge.
src/services/ (others)db, git, inspector, luks, mounts, network, trash.
src/utils/Config loading, path and glob helpers, media and icon helpers, search parsers and hwga.

Debug and performance tooling

  • hit!() adds a call counter to a function. hit!("name") gives it a stable name. In release builds both compile to nothing. The hwga module in src/utils/hwga/ collects the counts and flags calls on the main thread that take longer than 16 ms, one 60 Hz frame.
  • Tests can read the counters. tests/hwga_regression_test.rs uses hwga::count to assert that icon lookups do not rescan directories again and again. Such tests only exist in debug builds.
  • Debug builds time every call to handle_update and report slow messages.
  • Ctrl+Shift+F7, bound to shortcuts.open_debug_window, opens a debug window with process and memory diagnostics.

Build script

build.rs compiles the GResource bundle from data/resources.gresource.xml and exports the short git hash as FLUX_GIT_HASH. When you build with the local profile and set FLUX_SYNC_ASSETS, it also runs cargo fmt and make install with BIN_PATH pinned to target/local/flux-fm. It skips both when SOURCE_DATE_EPOCH is set, which is how packaging tools such as Arch's makepkg build.

Generated files

cargo run --bin gen-docs writes every file below. A test in the generating module fails when the committed copy is stale. See Testing and CI.

OutputGenerated from
docs/cli.mdsrc/doc_cli.rs
docs/config.mdsrc/model/doc_config.rs and the config structs
docs/css-variables.mdsrc/ui/doc_css_variables.rs and src/ui/style.css
docs/actions.mdThe BUILTINS table in src/ui/menu/editor/builtins.rs
docs/search.mdsrc/utils/search/doc.rs
completions/flux-fm.fishThe CLI_OPTIONS table in src/args.rs, through src/doc_fish.rs
data/fish/flux-integration.fishsrc/shell_functions.rs, through src/doc_fish.rs

Testing and CI

tools/tests.sh is the gate every change must pass. It is also the fastest way to find out why CI is red.

The gate

sh tools/tests.sh

The script stops at the first failing step (set -e):

StepCommandFails when
1cargo fmt --checkA file is not formatted. Fix it with cargo fmt.
2cargo clippy -- -D warningsAny lint fires. Warnings are errors.
3cargo test --verbose --tests -- --test-threads=1A test fails, or a test target does not compile.

Formatting is checked first, so a formatting failure hides lint and test failures until it is fixed. Run cargo fmt before you start.

Each crate reports its own problems, so the same formatting or lint message can appear twice. The reason is in Architecture.

Tests run on one thread because several of them set process-wide environment variables such as HOME and XDG_*, and because GTK may only be used from one thread.

Test layout

LocationContents
#[cfg(test)] modules in src/Unit tests that sit next to the code and can see private items. This includes the documentation sync tests.
tests/main_test.rsCommand-line parsing: resolve_startup_action and StartupAction.
tests/model_test.rs, tests/test_security.rsModel types and security checks.
tests/hwga_regression_test.rsCall-count regressions, in debug builds only.
tests/services.rs and tests/services/Loader, archives, database, search, task queue, thumbnails, terminal emulator and others.
tests/ui.rs and tests/ui/Components, dialogs, menus, keymap, settings, navigation and update routing.
tests/utils.rs and tests/utils/Config, search parsers, glob, helpers, media and xattr.

tests/services.rs, tests/ui.rs and tests/utils.rs are each one test target that lists its files with mod lines. A new file under those folders only runs after you add a mod line for it.

Integration tests link against the library crate, so they use the flux:: paths. They can only reach public items.

Running part of the suite

cargo test --lib                                   # unit tests only
cargo test --lib utils::search::                   # one module
cargo test --lib doc_is_in_sync                    # the documentation checks
cargo test --test services terminal_emulator       # one target, filtered by name
cargo test --test ui components_test -- --test-threads=1

Tests that build widgets call gtk::init(). Several skip themselves when it fails, but run the suite under a display, either a real session or a virtual one such as xvfb-run, so the widget tests actually run.

Documentation checks

cargo run --bin gen-docs regenerates the derived files. Each has a test that compares the committed copy with the generated text and fails with a message that names the file.

TestGuards
doc_cli::tests::doc_is_in_syncdocs/cli.md
model::doc_config::tests::doc_is_in_syncdocs/config.md
utils::search::tests::doc_is_in_syncdocs/search.md
ui::doc_css_variables::tests::doc_is_in_syncdocs/css-variables.md
the menu editor's sync testdocs/actions.md
doc_fish::tests::completions_are_in_synccompletions/flux-fm.fish
doc_fish::tests::integration_is_in_syncdata/fish/flux-integration.fish

CI runs cargo run --bin gen-docs and then git diff --exit-code docs/, so a stale file fails the build even without the tests. Never edit a generated file by hand.

Some changes that look unrelated touch generated files:

  • Using a custom property in src/ui/style.css changes its use count in docs/css-variables.md.
  • Declaring a new --property needs an entry in CUSTOM_PROPERTY_NOTES in src/ui/doc_css_variables.rs.
  • Using a new @color needs an entry in GTK_COLORS or PALETTE_COLORS there. A color that is not defined by GTK or libadwaita, such as an old palette name, is a bug in the stylesheet, not a missing table row.
  • Adding a command-line option changes docs/cli.md, the fish completions and the --help text together, and a test checks that every option has a completion.

Common failures

SymptomLikely cause
Diff in <file>A formatting problem. Run cargo fmt.
A clippy error with -D warningsFix the lint, or add a narrowly scoped #[allow(...)] with a comment explaining why.
could not compile ... (test "ui")A test no longer matches a refactored type, for example a struct that gained a field. Update the test.
<file> is stale. Run cargo run --bin gen-docsA source change altered generated output. Regenerate and commit the result.
A test passes alone and fails in the suiteIt depends on process state such as environment variables or a global cache. Keep it on one thread and reset what it changes.
A new test never runsThe file is missing from the mod list in tests/ui.rs, tests/services.rs or tests/utils.rs.

Cargo stops at the first target that fails to compile or fails a test, so fixing one failure can reveal the next. Rerun sh tools/tests.sh until it passes.

Fuzzing

Archive backends are fuzzed through the targets in fuzz/fuzz_targets/. See Adding an archive backend. The build script skips its git and asset steps when cargo-fuzz sets CARGO_CFG_FUZZING.

Adding a Right Panel

Every right-side panel in Flux, Tag, Search, Diff, Location, Connect, Preview, Actions, Terminal, is wired through the same set of files. The terminal panel (src/ui/panels/terminal.rs and the RightPanelType::Terminal arms in src/utils/helpers/sidebar.rs) is the smallest complete example, because it exercises every hook: config flag, message variants, lazy build, spawn-on-reveal, kill-on-hide, sidebar stash, focus skip. Read those two files before starting.

Checklist

Work through these in order. Each line names the file, the change, and why that file is on the list. Missing any of them is either a compile error or a silent bug.

  1. src/model/config/ui.rs - add the enable flag (bool with #[serde(default)]). If the panel has a width or height, add an i32 field defaulted via a helper in src/model/defaults.rs.

  2. src/utils/config/defaults.rs - mirror the field in the parse-failure UIConfig { ... } literal. Both branches must stay in sync or a corrupt config.toml silently drops the field.

  3. docs/config.md - run cargo run --bin gen-docs. Do not edit by hand.

  4. src/model/types.rs - add a variant to RightPanelType, grouped with the other panel variants.

  5. src/model/app.rs - add three fields minimum: Option<gtk::Revealer>, bool visible, bool initialized. If the panel hides the left sidebar (Terminal, Preview), add Option<bool> to stash the prior visibility. Doc-comment each field so the Debug derive stays readable.

  6. src/ui/startup/components.rs - initialize the new FluxApp fields in the struct literal. None / false unless the panel is open at startup.

  7. src/ui/view/component.rs - add a gtk::Revealer in the view! macro with #[name = "<panel>_panel_revealer"], SlideLeft, transition_duration: 200, reveal_child: false, visible: false. Match the neighboring panels.

  8. src/ui/view/init.rs - in store_widget_refs, store the revealer on the model. If the panel owns a subprocess (shell, mpv), add a root.connect_close_request that tears it down, mirroring the terminal block.

  9. src/utils/helpers/sidebar.rs - three edits inside toggle_sidebar_right_panel:

    • add the variant to hide_other_right_panels so opening another panel closes this one;
    • add a match arm in the (revealer, initialized, visible, is_search) tuple;
    • add a match arm in the panel-construction match that returns the gtk::Box from build_<panel>_panel(...).

    If the panel needs focus (Location, Connect, Search), leave it out of skip_focus. Otherwise add it to skip_focus so arrow keys keep driving the grid.

  10. src/ui/panels/mod.rs - pub mod <panel>; and pub use <panel>::build_<panel>_panel;.

  11. src/ui/panels/<panel>.rs - expose pub fn build_<panel>_panel(initial_width: i32, sender: AsyncComponentSender<FluxApp>) -> gtk::Box. Use header::panel_header, resize::resizable_panel, and spec::PanelSpec. Add const fn <panel>() to spec.rs with the min / max / default widths.

  12. src/model/msg.rs and src/ui/update/*.rs - if the panel has its own AppMsg variants, add them doc-commented and route them in the closest domain file.

  13. po/pt_BR.po - new strings, grouped under # src/ui/panels/<panel>.rs.

Lifecycle rules

Panels are lazy: the widget tree is built on first open and kept alive afterwards. These five rules are what keeps close/reopen correct.

  • Spawn on reveal, kill on hide. Subprocesses (shell, media player) are spawned in the now_visible branch and killed in the !now_visible branch of toggle_sidebar_right_panel. Do not reuse a process across toggles - the fd, pipe, or socket will be stale.

  • Do not grab focus by default. Preview, Actions and Terminal add themselves to skip_focus. Do the same unless the panel's purpose is text entry (Location, Connect, Search). Grabbing focus breaks arrow-key scrubbing in the grid.

  • Stash the left sidebar if the panel is wide. Terminal and Preview do this: store Some(self.sidebar_visible) on first reveal, restore on hide. Do not touch config.ui.sidebar_visible - that is a persisted preference, not session state.

  • Session-token every async load. Bump an Arc<AtomicU64> on open, capture its value, drop results whose token no longer matches. This is what prevents old contents from flashing into a newly opened panel on rapid switching.

  • Stop media on close. If the panel plays video or audio, call stop_media() (or equivalent) in the !now_visible branch. Otherwise the GStreamer pipeline and its fds keep running in the background.

Verifying the result

Once wired up, the panel should:

  • open and close on F<key> without touching any other panel's state;
  • survive a config reload (F10 on the settings window, then close it) with its flag preserved;
  • keep the file grid responsive to arrow keys while it is open;
  • leave no subprocess behind after the window closes (ps -ef | grep -E 'mvp|sh -l' should not show orphans);
  • not flash stale contents when re-opened after the underlying data changed on disk.

Adding an Archive Backend

Flux dispatches archive reads through the ArchiveBackend trait (src/services/archive/backend.rs). Every supported format is one implementation of that trait plus a small set of registrations scattered across the archive service. Adding a format means touching these files in order, missing any one is either a compile error or a format that lists but never extracts (or vice versa).

Read src/services/archive/backends/zip.rs first if the new format is seekable and single-file. Read backends/tar.rs first if the new format is stream-oriented or needs an external decompressor. Read backends/rar.rs first if the new format has to shell out to a CLI tool.

Checklist

  1. src/services/archive/backends/<fmt>.rs - new file implementing ArchiveBackend. The trait requires:

    • list_entries - returns immediate children of prefix, with name, is_dir, size, mtime, inner_path. Route through entry::collect_entry so synthesised directory nodes and child_count are populated correctly.
    • extract_entry_bytes - returns one entry's raw bytes.
    • extract_dir - extracts a subtree to a tempfile::TempDir and returns its root.

    Override the provided remove_entry, write_entry, create_directory only if the format supports in-place editing. The defaults return ArchiveError::Other(...), which is the correct behaviour for read-only formats.

  2. src/services/archive/backends/mod.rs - declare the module and re-export the struct:

     mod <fmt>;
     pub(in crate::services::archive) use self::<fmt>::<Fmt>Backend;
    

    Exports that other modules need (e.g. extract_dir_<fmt>_at for the fast path in extract_archive_to_dir) go here too.

  3. src/services/archive/backend.rs - two edits in get_backend:

    • Add an else if name_lc.ends_with(".<ext>") { return Box::new(<Fmt>Backend); } arm. Order matters: the .tar.gz family must stay grouped because every one of them begins with .tar.
    • Add a MIME fallback in the match mime.as_str() { ... } block for cases where the extension is absent or non-standard (e.g. .cbz which is a ZIP in disguise, or MIME types that arrive from a network share).
  4. src/services/archive/format.rs - two functions:

    • is_browsable_archive - add the extension to the matches_extension list so the grid treats the file as enterable.
    • is_supported_archive - add the extension to the ends_with chain so the CLI --preview and EnterArchive accept it.

    If only the MIME type is supported (no extension), extend the matches! on mime.as_str() in is_browsable_archive instead.

  5. src/utils/media/mime.rs - add the MIME type to guess_mime_from_extension's table if the extension is novel enough that GIO's content-type database doesn't already know it. Most common formats are already covered, skip this step for those.

  6. README.md - add a row to the supported-formats table (or the equivalent block in docs/development.md if it lives there).

  7. po/pt_BR.po - any user-facing string the backend emits (ArchiveError::Other(...) messages that reach a toast). Group under # src/services/archive/backends/<fmt>.rs.

Where the reference code lives

  • backends/zip.rs - the fullest example: streaming read, by_index_raw for raw copying, password handling, in-archive editing and the .flux-cache/ thumbnail writer.
  • backends/tar.rs - stream-oriented, one wrapper per decompressor (flate2, bzip2, xz2, zstd, lz4_flex). Use this when the format has no random access.
  • backends/rar.rs - external CLI wrapper (unar or unrar). Use this when the format has no pure-Rust library.
  • backends/sevenz.rs - pure-Rust random-access archive with AES256.
  • backends/iso.rs - hybrid: native reader for ISO 9660, 7z fallback for UDF. Use this if the format has more than one inner filesystem.
  • backends/single.rs - the "one virtual entry" shape (.gz, .bz2, .xz, .zst, .lz4 standalone). There is nothing to list list_entries returns a single synthesised entry named after the stripped extension.
  • backends/unsupported.rs - the fallback. Every get_backend chain ends here, do not delete it.

Behaviour rules

  • Never block the main thread. Backends run inside relm4::spawn_blocking or a Rayon pool, but a backend that itself blocks on a subprocess is fine because it is already off the GTK thread. The rule is about the caller, not the backend.
  • Never panic on malformed input. Every backend is fuzzed (fuzz/fuzz_targets/fuzz_archive_*.rs). Return ArchiveError::Other for truncated headers, corrupt central directories, out-of-range offsets, and any I/O error.
  • Reject path traversal. A ZipFile entry named ../../etc/passwd must be rejected in extract_entry_bytes (return Other("invalid path")) and silently skipped in extract_dir (as tar.rs does with has_traversal). The archive is untrusted input.
  • Return inner paths with forward slashes. Windows-built archives use \, normalise with .replace('\\', "/") before routing through collect_entry.
  • Preserve is_dir for synthesised nodes. An entry named foo/ with size 0 is a directory. An entry named foo/ with size 0 that has children is still a directory. Do not infer from size alone.
  • Forward the password argument, never cache it. The password lives on FluxApp::cached_archive_password and is forwarded by the caller. A backend that stores it in a field survives past a session and leaks it into the next archive that happens to use the same struct.
  • Respect ArchiveError::PasswordRequired vs WrongPassword. The UI uses these to decide whether to prompt or to show "incorrect password, try again". A generic Other message makes the encrypted archive prompt permanently stuck.

Verifying the result

Once wired up, the new format should:

  • appear in the --help supported list (is_supported_archive)
  • open with EnterArchive and --preview
  • list, navigate into a subfolder, and back out
  • extract a single file (Enter on a file, or Space for a preview)
  • extract the whole archive via the header button
  • return PasswordRequired on an encrypted archive with no password, and WrongPassword on a wrong password, if the format supports encryption
  • pass the corresponding fuzz target (cargo fuzz run fuzz_archive_<fmt> if you add one, or reuse fuzz_archive_tree by feeding it a sample).

Adding a Menu Action

Flux has two context-menu surfaces:

  • Primary right-click - built from ~/.config/flux/menu.rs and src/ui/menu/context.rs. Every entry is a CustomAction. Shell commands run via builtin:: dispatch, user commands go through handle_execute_command.
  • Secondary Ctrl+right-click - built from ~/.config/flux/menus/menu-<category>-<subtype>.rs and src/ui/menu/secondary/. Every entry is also a CustomAction, 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

  1. src/ui/menu/editor/builtins.rs - add a BuiltinDef to the BUILTINS table:

     BuiltinDef {
         name: "builtin::my_action",
         scope: "all",  // or "file", "directory", "image/all", ...
         desc: "One sentence describing what the action does.",
     },
    

    The desc field is rendered into the existing docs/actions.md table, and the doc_is_in_sync test in src/doc_cli.rs fails if the committed page drifts from the table. It is also shown verbatim in the "Pick Built-in" dialog in the menu editor.

  2. src/ui/menu/context.rs - add the dispatch arm. The match action.command.as_str() { ... } block has three shapes, pick the one that fits: - Stateless, no target - most actions. Use register_simple with a closure that dispatches an AppMsg:

                                                                   "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.
    
  3. src/model/msg.rs - add the AppMsg variant(s) the dispatch arm produces, doc-commented. If the action opens a dialog, the message name is usually Prompt<X>, if it runs work in the background, <X>Succeeded / <X>Failed pair.

  4. src/ui/update/*.rs - route the new AppMsg in the closest domain router (file_ops.rs, window.rs, search.rs, sidebar.rs, git.rs, network.rs, dialogs.rs). Everything below the router is a handle_* method on FluxApp, living in src/ui/ops/ or src/ui/dialog/.

  5. docs/actions.md - do not edit by hand. Run cargo run --bin gen-docs to regenerate it from BUILTINS. The gen-docs binary produces the file, and doc_is_in_sync in src/doc_cli.rs (plus the matching test in the menu editor module) fails CI if the committed file drifts from the generated output.

  6. po/pt_BR.po - any new user-facing string (toast, dialog title, error message). Group under # src/ui/menu/context.rs if 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 full builtin::install_fonts arm, which is the most complex selection-based case, and builtin::open_new_tab, which handles multi-tab activation.
  • src/ui/menu/editor/builtins.rs - the BUILTINS table and the render_actions_markdown() renderer that produces the existing docs/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 a builtin:: at all.
  • src/ui/menu/secondary/ - the Ctrl+right-click surface. Different procedure, every action here comes from a user template file, not from BUILTINS. Adding a builtin:: to a secondary template works because handle_execute_command handles 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_blocking or tokio::spawn. The dispatch arm in context.rs must return in microseconds.
  • Respect the toast contract. emit_toast fires whenever the menu.rs line 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_meta when the result depends on is_dir. get_selection returns only paths, if the action branches on file vs directory, resolve the meta once at dispatch time rather than doing a stat inside the handler (which races with concurrent renames).
  • Skip the action when its precondition fails. context.rs already does this for %l (line-number) actions and builtin::tagfile (which skips directories). Follow the same pattern: continue out 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_name field in CustomAction is what the popover's SimpleAction registers against. Two entries with the same name will collide in action_group and 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.md after cargo run --bin gen-docs, with the exact desc string from BUILTINS
  • pass cargo test --lib (the doc_is_in_sync test will catch any drift between BUILTINS and the committed docs/actions.md)
  • run from the primary right-click menu when an entry uses it
  • not appear in the primary menu when its mime_types requirement 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 in docs/actions.md.

Adding a Search Filter

The search bar dispatches on the query string in src/ui/search/filter.rs::handle_update_filter. Each query syntax is a parser that either matches and returns a modified view, or returns None and lets the next parser try. Order of evaluation is load-bearing and is not enforced by any test.

Read src/ui/search/filter.rs in full before starting.

Checklist

  1. src/utils/search/<syntax>.rs - new parser module, if the syntax is non-trivial. Follow the shape of size.rs or tag.rs: a parse_<syntax>(query: &str) -> Option<(Payload, String)> that returns the parsed payload and the remainder of the query. The parser is pure (no I/O, no allocator stress) and unit-tested in the same file under #[cfg(test)].

  2. src/utils/search/mod.rs - mod <syntax>; and pub use <syntax>::parse_<syntax>;.

  3. src/ui/search/filter.rs - insert the guard in handle_update_filter at the correct position in the chain. The current order is:

     empty → ':' (content search) → '*' or '?' (glob/walk)
         → # / :tag: / :t: (tag filter) → >/< / .. (size filter)
         → fuzzy match
    

    Insert your guard before the more permissive parsers:

    • If your syntax uses a prefix character that could also appear in a filename (e.g. date:), insert it before the fuzzy matcher but after any parser that uses a conflicting prefix.
    • If your syntax uses a prefix character that cannot appear in a filename (e.g. @), you may insert it earlier, but still after the : content-search guard, because users type : to switch into content search and any preceding parser that matches a bare : breaks it.
    • Never insert your parser after the fuzzy matcher. The fuzzy matcher returns true for almost everything, so anything after it is unreachable.
  4. src/utils/search/doc.rs - add the new syntax to render_search_markdown. The doc is regenerated into docs/search.md by cargo run --bin gen-docs, and crate::utils::search::tests::doc_is_in_sync fails CI if the committed file drifts. If the syntax accepts units or a fixed set of operators (like size.rs's ACCEPTED_SIZE_UNITS), expose the table as a pub const so the doc and the parser share one source of truth.

  5. docs/search.md - do not edit by hand. Run cargo run --bin gen-docs. CI regenerates it.

  6. po/pt_BR.po - any new user-facing message (e.g. "Invalid date filter") the parser surfaces through a toast.

Parser shape

A parser that is well-behaved under the dispatch chain:

  • is case-insensitive (query is already lowercased by the caller)
  • strips the prefix it consumed and returns the remainder untouched so the next parser can run on it
  • returns None for malformed input (e.g. >10 without a unit), so the fuzzy matcher can treat the whole string as a filename - never returns Some with a partial parse
  • never returns an error to the UI, if it cannot parse, it does not match and the next parser runs.

The size parser in src/utils/search/size.rs is the smallest complete example. The tag parser in tag.rs shows how to expose a table (TAG_PREFIXES) that both the parser and the doc renderer read.

Where the reference code lives

  • src/ui/search/filter.rs - the dispatch chain. Read every branch the wildcard/walk branch is the most subtle, because it kicks off a recursive walk (via AppMsg::StartAdvancedSearch) rather than filtering in place.
  • src/utils/search/size.rs - the smallest full parser. Good template if the syntax has a prefix and a numeric payload.
  • src/utils/search/tag.rs - the prefix-set parser. Good template if the syntax accepts one of several prefixes.
  • src/utils/search/content_query.rs - the multi-segment parser (:ext:term). Good template if the query has a second : in it.
  • src/utils/search/doc.rs - the doc renderer. Every pub const table referenced by a generated table in docs/search.md is defined here.

Behaviour rules

  • Case-insensitivity is the caller's job. Do not call to_lowercase() inside a parser. The caller passes a lowercased query, assume it.
  • Return the remainder, never re-parse it. If your syntax is foo:bar baz, return the payload bar and the remainder baz. Do not attempt to parse baz inside your parser - the dispatch chain will run it through the next parser in order.
  • Do not touch self.filter before returning. handle_update_filter assigns self.filter = query.clone() at the branch that handles your syntax. Doing it inside the parser would race with the outer branch.
  • Empty remainder is fine. >10MB alone should return Some(op, ""), and handle_update_filter's parse_size_filter branch handles the empty remainder (see the existing code - it filters the current view against the size alone).
  • A query that starts with your prefix but does not parse must return None. >10 (no unit) is not a size filter, it is a filename that happens to start with >. If your parser returns Some for a partial match, the fuzzy matcher never gets a chance and the file literally named >10 becomes unreachable.
  • The : prefix is reserved. parse_content_search_query claims anything that starts with : and does not start with :tag: or :t:. Do not add a syntax that starts with : without extending the content-query guard.
  • The # prefix is reserved. parse_tag_filter claims #. Do not add a syntax that starts with #.

Verifying the result

After wiring up the filter, it should:

  • trigger for its prefix (cargo test --lib utils::search:: covers the parser)
  • not trigger for a filename that happens to start with the same characters but does not parse (>10 must fall through to fuzzy)
  • preserve the remainder for the next parser (@foo bar with an @foo filter should leave bar to the fuzzy matcher)
  • appear in docs/search.md after cargo run --bin gen-docs
  • pass cargo test --lib utils::search::tests::doc_is_in_sync
  • not break :term content search, #tag filtering, >size filtering, or wildcard walks - run each of those queries manually before committing.

Adding a Keybinding

Flux has two shortcut systems that look identical from the user's perspective but are wired differently:

  • Configurable shortcuts - config.shortcuts.<name> in ~/.config/flux/config.toml. Resolved through KeyMap and used by the help window. Used for the actions a user might reasonably want to rebind (open, delete, refresh, toggle hidden, tab management).
  • Hardcoded shortcuts - F-keys, Ctrl+Shift+<letter> chords for panels, Ctrl+Alt+<letter> folder cycle. Registered directly in src/ui/inputs/capture.rs or src/ui/inputs/shortcuts.rs because they are structural (they toggle a panel, cycle the folder prefix, etc.) and rebinding them would break the mental model.

Pick the configurable system unless the shortcut is a fixed panel toggle or a cycle. The rest of this guide is the configurable path, the hardcoded path is a single match arm in capture.rs and has no other steps.

Checklist

  1. src/model/config/shortcuts.rs - add an Option<String> field to ShortcutsConfig, doc-commented with the default accelerator ("<Primary>k", "F7", etc.). If the default is a stable contract (like open = "Return"), spell it out in the comment.

  2. src/ui/keymap/constants.rs - add a pub const <NAME>: &str for the fallback accelerator. This is what KeyMap::new uses when config.shortcuts.<name> is None or Some("").

  3. src/ui/keymap/map.rs - add a pub <name>: gtk::ShortcutTrigger field to KeyMap and initialise it in KeyMap::new with parse_trigger(&config.<name>, constants::<NAME>). This is the only place the fallback chain lives.

  4. src/ui/inputs/shortcuts.rs - register the trigger with the controller. Most shortcuts go through the bind_msg!(keymap.<name>, AppMsg::<X>) macro at the top of setup_global_shortcuts. If the shortcut has a guard (skip when an entry is focused, yield to the terminal), follow the search pattern: a separate ShortcutController with a CallbackAction that checks root.focus() before dispatching.

  5. src/ui/help/page/*.rs - add a row(&tr("Description"), &sc(config, |s| s.<name>.clone(), "<default>")) to the relevant page. The sc helper formats the accelerator for the keycap label (Ctrl + K, not <Primary>k).

  6. po/pt_BR.po - the human-readable description and any dialog title. The accelerator itself is not translated.

When the shortcut needs a guard

A shortcut that should not fire while a text entry is focused needs the ShortcutController shape, not bind_msg!:

let sc = gtk::ShortcutController::new();
sc.set_scope(gtk::ShortcutScope::Managed);
sc.add_shortcut(gtk::Shortcut::new(
    Some(keymap.<name>.clone()),
    Some(gtk::CallbackAction::new(move |widget, _| {
        let rename_focused = widget
            .root()
            .and_then(|root| root.focus())
            .map(|focused| {
                focused
                    .css_classes()
                    .iter()
                    .any(|c| c.as_str() == "flux-rename-entry")
            })
            .unwrap_or(false);

        if rename_focused {
            return glib::Propagation::Proceed;
        }

        sender.input(AppMsg::<X>);
        glib::Propagation::Stop
    })),
));
window.add_controller(sc);

Scope::Managed is required. Scope::Global fires before the focused widget's own controller and breaks typing into entries. Use Managed for everything user-facing.

Where the reference code lives

  • src/ui/keymap/map.rs and src/ui/keymap/parse.rs - the fallback chain (user config → constant → "Escape"). Read these before adding a field a malformed user value must degrade to the constant, not panic.
  • src/ui/inputs/shortcuts.rs - the bind_msg! macro and the bind_trigger! macro for literal triggers. setup_settings_shortcut shows the Managed-scope-with-guard pattern.
  • src/ui/inputs/capture.rs - the hardcoded shortcuts. This file yields to the terminal and to editable widgets. If you add a chord here, you must yield in the same places (see terminal_focused and is_editable).
  • src/ui/help/page/navigation.rs - the sc(...) helper usage that renders a config value into a keycap label.

Behaviour rules

  • Managed scope, not Global. Global fires before the focused widget's controller and breaks typing. The only legitimate use of Global is a shortcut that must work no matter what - there should be none in this codebase.
  • Yield to the terminal. Both capture.rs and shortcuts.rs check flux-terminal-area on the focused widget. A shortcut that fires while the shell has focus breaks shell completion, Ctrl+C, Tab, and every other key the terminal owns.
  • Yield to editable widgets. If the focused widget is a gtk::Editable, Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+Z belong to the entry, not the app. Check is_editable before dispatching.
  • Fallback chain is config → constant → "Escape". A user-supplied accelerator that GTK refuses to parse must not leave the shortcut unbound. parse_trigger handles this, do not bypass it.
  • A rebindable shortcut needs a help-window row. If it is not in the help window, the user has no way to discover or verify the rebind. Hardcoded shortcuts go in system_view.rs, configurable ones go in their topic page.
  • Do not add a shortcut that shadows Space or Enter. Both are handled in handle_key_event with specific is_editable and terminal_focused guards that a new ShortcutController will not replicate.

Verifying the result

After wiring up the shortcut, it should:

  • fire when the grid has focus and nothing is typed
  • not fire when a rename entry is focused, a search entry is focused, or the terminal has focus
  • appear in the help window with the correct keycap label
  • survive a rebind through config.toml (edit the file, reload the window with F5, verify the old key no longer fires and the new one does)
  • fall back to the default when the user config contains garbage (e.g. shortcuts.<name> = "<NotAKey>" - the help window should show the default, and the default should fire)
  • not break Space, Enter, Tab, Escape, or the terminal.