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
- CLI Reference – How to launch Flux and manage custom icons.
- Configuration – Tweak
~/.config/flux/config.tomlto your liking. - Custom Actions – Add your own right-click menu entries.
- Search Syntax – Master the live filter, recursive search, and content search.
- Shell Integration – Use the selection and current folder from your shell.
- CSS Variables – Theme Flux to match your desktop.
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
| Time | Feature shown |
|---|---|
| 0:00 | Grid view with thumbnails |
| 0:04 | Embedded terminal (F4), ls command run |
| 0:09 | Close terminal, return to grid view |
| 0:11 | Peek-preview panel on an image (Space) |
| 0:12 | Arrow through image files in peek-preview |
| 0:18 | Return to grid view |
| 0:22 | Navigate into "Videos" folder |
| 0:24 | Peek-preview on video files, navigating through them |
| 0:33 | Return to Videos folder grid view |
| 0:35 | Return to main grid view |
| 0:37 | Search panel / fuzzy search filtering (Ctrl+F) |
| 0:38 | Close 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
PATH | Result |
|---|---|
| none | Opens the home directory. |
| a directory | Opens it. |
| a supported archive file | Opens the archive explorer on it. |
#name, :tag:name, :t:name or tags://name | Opens the home directory filtered by the tag name. |
Options
| Option | Description |
|---|---|
-h, --help | Print this help message |
-v, --version | Print version information |
--no-sidebar | Disable sidebar for this session |
--no-header | Disable header bar for this session |
--no-statusbar | Disable statusbar for this session |
--terminal | Start with the terminal open in the configured position |
--file-properties PATH | Open the file properties window for PATH |
--set-icon TARGET IMAGE | Set a custom icon/image for TARGET |
--cwd PATH | Navigate the running window to PATH (starts a new window if none is running) |
--set-icons-stdin | Read TARGET<TAB>IMAGE pairs from standard input |
--reset-icon TARGET | Reset custom icon for TARGET back to default |
--reset-icons-stdin | Read TARGET paths from standard input to reset |
--quick-list PATH... | Open with quick-panel pre-populated with PATHs |
--quick-list-stdin | Open with quick-panel populated from standard input |
--preview PATH | Open folder PATH with the preview panel in wide mode |
--selection | Print the paths selected in the running window, one per line (-0: NUL-separated) |
--toggle | Show or hide the running window |
--show | Bring the running window to the front |
--hide | Hide the running window |
--theme NAME | Set the theme to NAME, or 'default' to remove it |
--menu-editor | To 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
| Status | Meaning |
|---|---|
0 | Success. Missing values for --file-properties, --set-icon, --reset-icon, --theme and --quick-list print the help text and also exit with 0. |
1 | Unrecognized 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
| Key | Default | Description |
|---|---|---|
default_list_mode | false | Open folders in list mode instead of grid mode. |
network_bookmarks | [] | Saved network locations; see Array tables. |
sidebar | [] | Custom sidebar entries; see Array tables. |
[shortcuts]
| Key | Default | Description |
|---|---|---|
back | unset | Go back in navigation history. |
change_icon | unset | Open the folder icon picker. |
close_tab | unset | Close the current tab. |
copy_path | unset | Copy the absolute paths of the selected items. |
create_hardlink | unset | Create hardlinks from clipboard paths. Directories are not supported. |
create_symlink | unset | Create symlinks from clipboard paths, auto-renaming conflicts. |
cycle_sort | unset | Rotate through the sort fields. |
delete | unset | Move the selected items to the trash. |
forward | unset | Go forward in navigation history. |
home | unset | Go to the home directory. |
menu_editor | unset | Open the menu editor. |
new_tab | unset | Open a new tab. |
next_tab | unset | Switch to the next tab. |
open | unset | Open the selected file or enter the directory. |
open_properties | unset | Open the properties window. |
prev_tab | unset | Switch to the previous tab. |
quit | unset | Quit the application. |
refresh | unset | Reload the current directory. |
rename | unset | Start an inline rename. |
reset_icon | unset | Reset a folder icon to the default. |
root | unset | Go to the root directory. |
search | unset | Focus the search interface. |
settings | unset | Open the settings window. |
toggle_folders_first | unset | Toggle folder grouping for the active folder. |
toggle_header | unset | Show or hide the header bar. |
toggle_hidden | unset | Show or hide hidden files. |
toggle_sort_order | unset | Toggle between ascending and descending order. |
[ui]
| Key | Default | Description |
|---|---|---|
actions_panel_width | 380 | Width in pixels of the right-side contextual-actions panel. |
archive_thumbnail_cache | false | Store thumbnails of files inside zip archives in a hidden .flux-cache/ folder in the zip itself. |
ascending | true | Sort listings in ascending order. |
auto_generate_mime_icons | true | Generate and cache an SVG document icon when the icon theme has none. |
auto_list_threshold | 300 | Folders 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_size | 13.0 | Base font size of auto-generated document icons. |
auto_show_diff | true | Open the diff panel when a modified git file is selected. |
autoplay_video_previews | true | Play a muted loop preview when a single video card is selected. |
bg_alpha_sidebar_left | 0.65 | Tint opacity (0.0 to 1.0) over the left sidebar background image. |
bg_alpha_sidebar_right | 0.65 | Tint opacity (0.0 to 1.0) over the right panel background image. |
bg_alpha_window | 0.65 | Tint opacity (0.0 to 1.0) over the main window background image. |
breadcrumb_count | 3 | Maximum number of path segments shown as breadcrumbs in the header bar; only the last ones are kept. |
breadcrumb_max_chars | 20 | Characters shown in a breadcrumb label before truncation. |
content_search_max_file_mb | 128 | Skip 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_size | 0 | Default 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_width | 420 | Width in pixels of the right-side git diff panel. |
disable_drag_and_drop | false | Disable drag and drop of files and folders. |
enable_file_indexing | false | Enable the FTS5 filename indexer. |
expand_labels | false | Wrap filenames over multiple lines instead of truncating them. |
ffmpeg_auto_rotate | false | Let FFmpeg rotate portrait video thumbnails. |
ffmpeg_seek_seconds | 5.0 | Seek position in seconds when grabbing a video thumbnail frame. |
ffmpeg_threads | 1 | Threads per FFmpeg process when extracting video frames. |
file_icons | {} | Per-file custom icon image, keyed by absolute path. Managed by --set-icon. |
fish_theme | false | Generate a fish conf.d snippet so the embedded shell's colors and prompt follow the GTK theme. Off by default. |
folder_cache_capacity | 3 | Number of recently visited directory snapshots kept in memory. |
folder_grid_thumbnails | true | Show 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_first | true | Group directories above files. |
grid_spacing | 10 | Pixel spacing between items in the file grid. |
header_show_filter | true | Show the filter button in the header bar. Ctrl+Shift+F toggles the filter bar while it is hidden. |
header_show_preview | true | Show the preview-panel toggle in the header bar. |
header_show_sort | true | Show the sort indicator in the header bar. |
header_show_view | true | Show the list/grid toggle in the header bar. Ctrl+Shift+M toggles the mode while it is hidden. |
header_visible | true | Show the header bar. Persisted across sessions. |
hidden_extensions | [] | Extensions to hide from grid labels, for example ["desktop", "AppImage"]. |
lazy_thumbnails | false | Generate thumbnails only for items scrolled into view. |
list_icon_size | 24 | Pixel size of icons in list mode. |
loader_batch_size | 10 | Items dispatched per chunk into the grid while a folder loads. |
max_content_search_results | 0 | Maximum results returned by content search. |
max_history | 100 | Back and forward history entries kept. |
max_search_results | 100 | Maximum matches during recursive filename and extension search. |
max_width_chars | 20 | Characters shown in a file label before truncation. |
panel_menu_requires_ctrl | false | Open left-sidebar, preview-panel and quick-list context menus only on Ctrl+right-click; a plain right-click does nothing. |
preview_panel_width | 400 | Width in pixels of the right-side preview panel. |
preview_syntax_highlighting | true | Syntax-highlight source files in the preview panel. |
recents_row | 0 | Zero-based position of the Recents row among the [[sidebar]] entries. No effect when show_recents is false. |
scale_font_with_icons | false | Scale label font size together with the grid icons. |
search_panel_width | 350 | Width in pixels of the right-side search panel. |
show_csd | false | Draw client-side window decorations. |
show_empty_dir_emblem | false | Show an emblem on folders that contain no visible items. |
show_hidden_by_default | false | Show hidden files at startup. |
show_recents | true | Show the Recents location in the sidebar. |
show_symlink_emblem | true | Show a badge and target hint on symbolic links. |
show_thumbnails | true | Master switch for thumbnail generation; overrides thumbnail_types. |
show_xdg_dirs | false | Show the XDG user directories, such as Documents and Downloads, in the sidebar. |
sidebar_menu_requires_ctrl | false | Open sidebar context menus only on Ctrl+right-click; a plain right-click does nothing. |
sidebar_visible | true | Show the sidebar. Persisted across sessions. |
sidebar_width | 0 | Width in pixels of the left sidebar. |
single_click | false | Activate items with a single click. |
start_maximized | false | Open the window maximized. |
startup_window_height | 0 | Initial window height in pixels. |
startup_window_width | 0 | Initial window width in pixels. |
tag_panel_width | 350 | Width in pixels of the right-side tag panel. |
theme | unset | Name of the theme to apply, such as default, os-dark or os-light. |
thumbnail_size | 256 | Target size in pixels of generated thumbnails. |
thumbnail_threads | 4 | Maximum concurrent thumbnail workers. |
ui_scale | 1.0 | Global UI scale multiplier. |
wide_preview | false | Open the preview panel in wide mode, with the sidebar hidden. |
window_controls_left | false | Put window controls on the left instead of the right. |
[ui.terminal]
| Key | Default | Description |
|---|---|---|
bg_color | "#1A1A1A" | Terminal background color. |
fg_color | "#E5E5E5" | Terminal foreground color. |
font | "JetBrains Mono 13" | Terminal font as a Pango font description. |
height | 30 | Terminal pane height. |
position | "bottom" | Where F4 opens the terminal: bottom pane, right sidebar panel, or fullscreen window. |
shell | unset | Shell started in the terminal pane; the user's login shell when unset. |
[ui.thumbnail_types]
| Key | Default | Description |
|---|---|---|
audio | true | Thumbnails and embedded artwork for audio files. |
executables | false | Thumbnails for Windows .exe files. |
fonts | true | Thumbnails for font files. |
images | true | Thumbnails for image files. |
pdfs | true | Thumbnails for PDF documents. |
videos | true | Thumbnails 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 key | Replaced by | Since | Note |
|---|---|---|---|
ui.terminal_in_sidebar | ui.terminal.position | 0.6.0 | Replaced 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.
| Placeholder | One target | Several targets |
|---|---|---|
%p | Absolute path of the item | All the paths, shell-quoted and separated by spaces |
%d | Parent directory of the item | The current directory |
%f | File name of the item | Not substituted |
%l | Line number of the content-search hit, 0 when the item is not a hit | 0 |
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.
| Action | Scope | Description |
|---|---|---|
builtin::copy | all | Copies the selected files or folders to the clipboard. |
builtin::cut | all | Cuts the selected files or folders to the clipboard. |
builtin::paste | directory | Pastes items from the clipboard into the current directory. |
builtin::open_new_tab | directory | Opens the selected folder or current location in a new tab. |
builtin::rename | all | Triggers inline filename renaming on the selected item. |
builtin::delete | all | Moves the selected files or folders to the system trash. |
builtin::new_folder | directory | Opens the new folder creation prompt. |
builtin::new_file | directory | Opens the new file creation prompt. |
builtin::toggle_pin | directory | Pins or unpins the target folder in the sidebar. |
builtin::add_to_quick_list | all | Adds the selected folder or current directory to the Quick List. |
builtin::quick_list_transfer | all | Opens a submenu to move or copy items directly to Quick List slots. |
builtin::tagfile | file | Opens the file tag editor popover. |
builtin::inspect_dir | directory | Opens the Directory Inspector with disk usage and largest files. |
builtin::install_fonts | font/all | Installs the selected font files into ~/.local/share/fonts. |
builtin::open_with | file | Populates a dynamic submenu of registered applications for the MIME type. |
builtin::open_with_dialog | file | Opens the full system application chooser dialog. |
builtin::select_folder_icon | directory | Opens the folder icon picker to assign a theme icon. |
builtin::set_custom_icon | all | Opens a file chooser to assign a custom image as file/folder icon. |
builtin::reset_custom_icon | all | Restores the default icon for the selected file or folder. |
builtin::set_extension_icon | file | Sets a custom image icon for the target file's extension globally. |
builtin::reset_extension_icon | file | Resets the custom icon for the target extension back to theme default. |
builtin::set_bg_window | image/all | Sets the selected image as the main window background. |
builtin::set_bg_sidebar_left | image/all | Sets the selected image as the left sidebar background. |
builtin::set_bg_sidebar_right | image/all | Sets the selected image as the right panel background. |
builtin::clear_backgrounds | all | Clears 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 -
:termor:.ext:termin 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.
| Unit | Byte multiplier |
|---|---|
k | 1024 |
m | 1048576 |
g | 1073741824 |
t | 1099511627776 |
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.
| Prefix | Example |
|---|---|
: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"
Content search
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.
| Syntax | Meaning |
|---|---|
:TERM | Search all files for TERM |
:.EXT:TERM | Search 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_mbis skipped.0removes 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.
| Field | Behavior |
|---|---|
| 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 match | The term, lowercased, with no wildcards added. |
| Regular expression | A case-insensitive regex matched against the file name. Makes the search recursive. An invalid expression shows an error and nothing runs. |
| Inside files | Content 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 subfolders | On by default. When off, the search only sets an extension filter on the current folder. |
| Only search folders | Makes the search recursive and matches directory names. |
| Include hidden files | Adds 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. |
| Modified | Any time, last hour, last 24 hours (Today), last 7, 30 or 365 days. Applied by the recursive walk. |
| File size | Larger 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
| Command | Result |
|---|---|
flux-fm --selection | Prints the paths selected in the active tab, one per line. -0 ends each path with NUL instead. |
flux-fm --cwd PATH | Navigates the running window to PATH. Starts a window if none is running. |
flux-fm --toggle, --show, --hide | Show 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
| Helper | Description |
|---|---|
flux_selection | Prints 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_cwd | Prints 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. |
$F | Abbreviation that expands to (flux_selection) as you type, in any position on the line. |
$FN | Expands to (flux_selection --names). |
$FD | Expands to (flux_selection --dir). |
$FCWD | Environment variable set before every command to the folder shown in the grid. |
$FLUX_SELECTION | Environment 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.
| Path | Purpose |
|---|---|
$XDG_CACHE_HOME/flux/cwd | The folder shown in the window that navigated last. XDG_CACHE_HOME defaults to ~/.cache. |
$XDG_CACHE_HOME/flux/pid | The process id of that window. |
$XDG_CACHE_HOME/flux/goto | A 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>.sock | A 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.sock | Serves --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
| Symptom | Cause and fix |
|---|---|
$F expands to nothing | No 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 terminal | Turn on ui.fish_theme, or install flux-integration.fish. |
| Another window answers | With 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 window | The 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 withvar(--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.
| Variable | Default | Declared in | Uses | Purpose |
|---|---|---|---|---|
--border-color | alpha(var(--color-fg), 0.1) | :root | 16 | Hairline borders on the sidebar, list rows, inputs, tooltips and toasts. |
--color-accent | @accent_bg_color | :root | 24 | Primary accent background for selected and active elements. |
--color-accent-fg | @accent_fg_color | :root | 34 | Text and icons drawn on top of --color-accent. |
--color-accent-text | @accent_color | :root | 47 | Accent color for text, icons and borders on a plain background. |
--color-card-bg | @card_bg_color | :root | 1 | Background color of file cards in the grid view. |
--color-danger | @red_3 | :root | 22 | Errors and destructive actions. |
--color-fg | @theme_fg_color | :root | 60 | Default foreground color used across the UI. |
--color-git-mod | @blue_3 | :root | 2 | Git modified and mixed file states. |
--color-git-staged | @purple_3 | :root | 2 | Git staged file states. |
--color-git-untracked | @dark_2 | :root | 2 | Git untracked file states. |
--color-headerbar-bg | @headerbar_bg_color | :root | 4 | Background color of the header bar and status bar. |
--color-headerbar-fg | @headerbar_fg_color | :root | 9 | Foreground text and icons in the header bar. |
--color-popover-bg | @popover_bg_color | :root | 1 | Background color of popovers and context menus. |
--color-popover-fg | @popover_fg_color | :root | 12 | Foreground text and icons in popovers. |
--color-sidebar-bg | @sidebar_bg_color | :root | 0 | Background color of the left sidebar. |
--color-success | @green_4 | :root | 4 | Success states and git additions. |
--color-warn | @orange_3 | :root | 8 | Warnings, restricted items and git conflicts. |
--color-window-bg | @window_bg_color | :root | 6 | Main window background color. |
--color-window-fg | @window_fg_color | :root | 14 | Main window foreground text and icons. |
--disabled-opacity | 0.4 | :root | 4 | Opacity of disabled controls and of items being dragged. |
--font-main | inherit | :root | 8 | Font family for interface text; inherit follows the system font. |
--font-mono | "JetBrains Mono", monospace | :root | 9 | Monospace font family for list info, key caps, git badges and status text. |
--shadow | alpha(black, 0.25) | :root | 3 | Drop-shadow color of cards, popover menus and the selected sidebar row. |
--shadow-hover | alpha(black, 0.35) | :root | 1 | Drop-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
| Color | Role | Uses |
|---|---|---|
@accent_bg_color | Accent background for selected and active elements. | 2 |
@accent_fg_color | Text and icons drawn on top of @accent_bg_color. | 3 |
@accent_color | Accent for text, icons and borders on a plain background. | 1 |
@window_bg_color | Window background. | 1 |
@window_fg_color | Window text and icons. | 1 |
@theme_fg_color | Default foreground; also the base for translucent text, borders and hover tints. | 3 |
@headerbar_bg_color | Header bar background. | 1 |
@headerbar_fg_color | Header bar text and icons. | 1 |
@popover_bg_color | Popover background; the quick panel uses it. | 2 |
@popover_fg_color | Popover text; also the base for translucent tints on popovers. | 4 |
@sidebar_bg_color | Sidebar background. | 1 |
@card_bg_color | Background of file cards. | 1 |
Palette colors
Fixed GNOME palette colors. Unlike the named colors above, they do not change with the theme.
| Color | Role | Uses |
|---|---|---|
@red_3 | Errors and destructive actions: broken symlinks, delete buttons, window close. | 1 |
@orange_3 | Warnings and restricted items: locked files, cut items, git conflicts. | 1 |
@green_4 | Success: full level bars and git added. | 1 |
@blue_3 | Git modified and mixed. | 1 |
@purple_3 | Git staged. | 1 |
@dark_2 | Git 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.
| Tool | Enables |
|---|---|
ffmpeg, ffprobe | Video thumbnails and media duration |
7z (p7zip-full / p7zip) | AppImage icons and UDF ISO support |
unar or unrar | RAR archive browsing |
exiftool | Primary RAW camera thumbnail support |
raw-thumbnailer / miniaturo | Fallback RAW camera thumbnail support |
wrestool, icotool | .exe icon extraction |
mpv | Audio playback in the preview panel |
soffice / libreoffice | Office document previews |
fc-cache | Refreshing 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.shand 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:
| Target | Entry point | Purpose |
|---|---|---|
library flux | src/lib.rs | Argument parsing, the doc generators, and the code that the integration tests link against. |
binary flux-fm | src/main.rs, which calls cli::run() | The application. |
binary gen-docs | src/bin/gen-docs.rs | Regenerates 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 --checkandcargo clippyreport a problem once per crate.- A crate-level lint such as
#![allow(clippy::result_large_err)]must appear in bothlib.rsandmain.rs. - A type that lives in
modelexists in both crates and the two are not interchangeable.flux::args::WindowOptionsis the library's type, andcli/launch.rscopies 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:
- Commands that need no display run first and exit:
--selection,--toggle,--showand--hide. They are cheap enough to be called from every$Fexpansion in a shell. - The IPC server for
--cwdstarts before any client can connect. If another window already owns the socket,--cwdacts as a client and exits. - One-shot commands such as
--theme,--set-iconand--reset-iconchangeconfig.tomland exit without a window. - Everything else reaches
cli/launch.rs, which builds anAppInitand runs the Relm4 componentFluxApp.
Message flow
The UI follows Relm4's model, view, update pattern.
FluxAppinsrc/model/app.rsis the model. It owns the state and the widget references.AppMsginsrc/model/msg.rsis every event the app can receive. Each variant is doc-commented.FluxApp::handle_updateinsrc/ui/update/mod.rsis 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,windowandsidebar.- 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 newAppMsgvariant 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 globalrelm4::Sender<AppMsg>. The--cwdserver, thegotowatcher 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 ofui.loader_batch_sizeitems. - 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.tomlis read throughutils::load_config, which keeps a cached copy.save_configprimes that cache andinvalidate_config_cachedrops it. A config watcher insrc/cli/watcher.rsreloads the file while Flux runs. Migrations of renamed keys run when the file is loaded, for exampleui.terminal_in_sidebar, and are listed in Configuration.- SQLite state lives behind
StateManagerinsrc/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,iconsandmaintenance. - Shell bridge files under
$XDG_CACHE_HOME/flux/are described in Shell integration.
Source map
| Path | Contents |
|---|---|
src/args.rs, src/cli/ | Argument parsing and the non-GUI commands. |
src/ipc.rs | The --cwd Unix socket server and client. |
src/shell_functions.rs | The 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. Thehwgamodule insrc/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.rsuseshwga::countto 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_updateand report slow messages. Ctrl+Shift+F7, bound toshortcuts.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.
| Output | Generated from |
|---|---|
docs/cli.md | src/doc_cli.rs |
docs/config.md | src/model/doc_config.rs and the config structs |
docs/css-variables.md | src/ui/doc_css_variables.rs and src/ui/style.css |
docs/actions.md | The BUILTINS table in src/ui/menu/editor/builtins.rs |
docs/search.md | src/utils/search/doc.rs |
completions/flux-fm.fish | The CLI_OPTIONS table in src/args.rs, through src/doc_fish.rs |
data/fish/flux-integration.fish | src/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):
| Step | Command | Fails when |
|---|---|---|
| 1 | cargo fmt --check | A file is not formatted. Fix it with cargo fmt. |
| 2 | cargo clippy -- -D warnings | Any lint fires. Warnings are errors. |
| 3 | cargo test --verbose --tests -- --test-threads=1 | A 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
| Location | Contents |
|---|---|
#[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.rs | Command-line parsing: resolve_startup_action and StartupAction. |
tests/model_test.rs, tests/test_security.rs | Model types and security checks. |
tests/hwga_regression_test.rs | Call-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.
| Test | Guards |
|---|---|
doc_cli::tests::doc_is_in_sync | docs/cli.md |
model::doc_config::tests::doc_is_in_sync | docs/config.md |
utils::search::tests::doc_is_in_sync | docs/search.md |
ui::doc_css_variables::tests::doc_is_in_sync | docs/css-variables.md |
| the menu editor's sync test | docs/actions.md |
doc_fish::tests::completions_are_in_sync | completions/flux-fm.fish |
doc_fish::tests::integration_is_in_sync | data/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.csschanges its use count indocs/css-variables.md. - Declaring a new
--propertyneeds an entry inCUSTOM_PROPERTY_NOTESinsrc/ui/doc_css_variables.rs. - Using a new
@colorneeds an entry inGTK_COLORSorPALETTE_COLORSthere. 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--helptext together, and a test checks that every option has a completion.
Common failures
| Symptom | Likely cause |
|---|---|
Diff in <file> | A formatting problem. Run cargo fmt. |
A clippy error with -D warnings | Fix 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-docs | A source change altered generated output. Regenerate and commit the result. |
| A test passes alone and fails in the suite | It 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 runs | The 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.
-
src/model/config/ui.rs- add the enable flag (boolwith#[serde(default)]). If the panel has a width or height, add ani32field defaulted via a helper insrc/model/defaults.rs. -
src/utils/config/defaults.rs- mirror the field in the parse-failureUIConfig { ... }literal. Both branches must stay in sync or a corruptconfig.tomlsilently drops the field. -
docs/config.md- runcargo run --bin gen-docs. Do not edit by hand. -
src/model/types.rs- add a variant toRightPanelType, grouped with the other panel variants. -
src/model/app.rs- add three fields minimum:Option<gtk::Revealer>,bool visible,bool initialized. If the panel hides the left sidebar (Terminal, Preview), addOption<bool>to stash the prior visibility. Doc-comment each field so theDebugderive stays readable. -
src/ui/startup/components.rs- initialize the newFluxAppfields in the struct literal.None/falseunless the panel is open at startup. -
src/ui/view/component.rs- add agtk::Revealerin theview!macro with#[name = "<panel>_panel_revealer"],SlideLeft,transition_duration: 200,reveal_child: false,visible: false. Match the neighboring panels. -
src/ui/view/init.rs- instore_widget_refs, store the revealer on the model. If the panel owns a subprocess (shell, mpv), add aroot.connect_close_requestthat tears it down, mirroring the terminal block. -
src/utils/helpers/sidebar.rs- three edits insidetoggle_sidebar_right_panel:- add the variant to
hide_other_right_panelsso 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
matchthat returns thegtk::Boxfrombuild_<panel>_panel(...).
If the panel needs focus (Location, Connect, Search), leave it out of
skip_focus. Otherwise add it toskip_focusso arrow keys keep driving the grid. - add the variant to
-
src/ui/panels/mod.rs-pub mod <panel>;andpub use <panel>::build_<panel>_panel;. -
src/ui/panels/<panel>.rs- exposepub fn build_<panel>_panel(initial_width: i32, sender: AsyncComponentSender<FluxApp>) -> gtk::Box. Useheader::panel_header,resize::resizable_panel, andspec::PanelSpec. Addconst fn <panel>()tospec.rswith the min / max / default widths. -
src/model/msg.rsandsrc/ui/update/*.rs- if the panel has its ownAppMsgvariants, add them doc-commented and route them in the closest domain file. -
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_visiblebranch and killed in the!now_visiblebranch oftoggle_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 touchconfig.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_visiblebranch. 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 (
F10on 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
-
src/services/archive/backends/<fmt>.rs- new file implementingArchiveBackend. The trait requires:list_entries- returns immediate children ofprefix, withname,is_dir,size,mtime,inner_path. Route throughentry::collect_entryso synthesised directory nodes andchild_countare populated correctly.extract_entry_bytes- returns one entry's raw bytes.extract_dir- extracts a subtree to atempfile::TempDirand returns its root.
Override the provided
remove_entry,write_entry,create_directoryonly if the format supports in-place editing. The defaults returnArchiveError::Other(...), which is the correct behaviour for read-only formats. -
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>_atfor the fast path inextract_archive_to_dir) go here too. -
src/services/archive/backend.rs- two edits inget_backend:- Add an
else if name_lc.ends_with(".<ext>") { return Box::new(<Fmt>Backend); }arm. Order matters: the.tar.gzfamily 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..cbzwhich is a ZIP in disguise, or MIME types that arrive from a network share).
- Add an
-
src/services/archive/format.rs- two functions:is_browsable_archive- add the extension to thematches_extensionlist so the grid treats the file as enterable.is_supported_archive- add the extension to theends_withchain so the CLI--previewandEnterArchiveaccept it.
If only the MIME type is supported (no extension), extend the
matches!onmime.as_str()inis_browsable_archiveinstead. -
src/utils/media/mime.rs- add the MIME type toguess_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. -
README.md- add a row to the supported-formats table (or the equivalent block indocs/development.mdif it lives there). -
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_rawfor 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 (unarorunrar). 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,7zfallback 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,.lz4standalone). There is nothing to listlist_entriesreturns a single synthesised entry named after the stripped extension.backends/unsupported.rs- the fallback. Everyget_backendchain ends here, do not delete it.
Behaviour rules
- Never block the main thread. Backends run inside
relm4::spawn_blockingor 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). ReturnArchiveError::Otherfor truncated headers, corrupt central directories, out-of-range offsets, and any I/O error. - Reject path traversal. A
ZipFileentry named../../etc/passwdmust be rejected inextract_entry_bytes(returnOther("invalid path")) and silently skipped inextract_dir(astar.rsdoes withhas_traversal). The archive is untrusted input. - Return inner paths with forward slashes. Windows-built archives
use
\, normalise with.replace('\\', "/")before routing throughcollect_entry. - Preserve
is_dirfor synthesised nodes. An entry namedfoo/with size 0 is a directory. An entry namedfoo/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_passwordand 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::PasswordRequiredvsWrongPassword. The UI uses these to decide whether to prompt or to show "incorrect password, try again". A genericOthermessage makes the encrypted archive prompt permanently stuck.
Verifying the result
Once wired up, the new format should:
- appear in the
--helpsupported list (is_supported_archive) - open with
EnterArchiveand--preview - list, navigate into a subfolder, and back out
- extract a single file (
Enteron a file, orSpacefor a preview) - extract the whole archive via the header button
- return
PasswordRequiredon an encrypted archive with no password, andWrongPasswordon a wrong password, if the format supports encryption - pass the corresponding fuzz target (
cargo fuzz run fuzz_archive_<fmt>if you add one, or reusefuzz_archive_treeby feeding it a sample).
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.
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
-
src/utils/search/<syntax>.rs- new parser module, if the syntax is non-trivial. Follow the shape ofsize.rsortag.rs: aparse_<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)]. -
src/utils/search/mod.rs-mod <syntax>;andpub use <syntax>::parse_<syntax>;. -
src/ui/search/filter.rs- insert the guard inhandle_update_filterat the correct position in the chain. The current order is:empty → ':' (content search) → '*' or '?' (glob/walk) → # / :tag: / :t: (tag filter) → >/< / .. (size filter) → fuzzy matchInsert 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
truefor almost everything, so anything after it is unreachable.
- If your syntax uses a prefix character that could also appear in a
filename (e.g.
-
src/utils/search/doc.rs- add the new syntax torender_search_markdown. The doc is regenerated intodocs/search.mdbycargo run --bin gen-docs, andcrate::utils::search::tests::doc_is_in_syncfails CI if the committed file drifts. If the syntax accepts units or a fixed set of operators (likesize.rs'sACCEPTED_SIZE_UNITS), expose the table as apub constso the doc and the parser share one source of truth. -
docs/search.md- do not edit by hand. Runcargo run --bin gen-docs. CI regenerates it. -
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
Nonefor malformed input (e.g.>10without a unit), so the fuzzy matcher can treat the whole string as a filename - never returnsSomewith 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 (viaAppMsg::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. Everypub consttable referenced by a generated table indocs/search.mdis 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 payloadbarand the remainderbaz. Do not attempt to parsebazinside your parser - the dispatch chain will run it through the next parser in order. - Do not touch
self.filterbefore returning.handle_update_filterassignsself.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.
>10MBalone should returnSome(op, ""), andhandle_update_filter'sparse_size_filterbranch 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 returnsSomefor a partial match, the fuzzy matcher never gets a chance and the file literally named>10becomes unreachable. - The
:prefix is reserved.parse_content_search_queryclaims 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_filterclaims#. 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 (
>10must fall through to fuzzy) - preserve the remainder for the next parser (
@foo barwith an@foofilter should leavebarto the fuzzy matcher) - appear in
docs/search.mdaftercargo run --bin gen-docs - pass
cargo test --lib utils::search::tests::doc_is_in_sync - not break
:termcontent search,#tagfiltering,>sizefiltering, 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 throughKeyMapand 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 insrc/ui/inputs/capture.rsorsrc/ui/inputs/shortcuts.rsbecause 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
-
src/model/config/shortcuts.rs- add anOption<String>field toShortcutsConfig, doc-commented with the default accelerator ("<Primary>k","F7", etc.). If the default is a stable contract (likeopen = "Return"), spell it out in the comment. -
src/ui/keymap/constants.rs- add apub const <NAME>: &strfor the fallback accelerator. This is whatKeyMap::newuses whenconfig.shortcuts.<name>isNoneorSome(""). -
src/ui/keymap/map.rs- add apub <name>: gtk::ShortcutTriggerfield toKeyMapand initialise it inKeyMap::newwithparse_trigger(&config.<name>, constants::<NAME>). This is the only place the fallback chain lives. -
src/ui/inputs/shortcuts.rs- register the trigger with the controller. Most shortcuts go through thebind_msg!(keymap.<name>, AppMsg::<X>)macro at the top ofsetup_global_shortcuts. If the shortcut has a guard (skip when an entry is focused, yield to the terminal), follow thesearchpattern: a separateShortcutControllerwith aCallbackActionthat checksroot.focus()before dispatching. -
src/ui/help/page/*.rs- add arow(&tr("Description"), &sc(config, |s| s.<name>.clone(), "<default>"))to the relevant page. Theschelper formats the accelerator for the keycap label (Ctrl + K, not<Primary>k). -
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.rsandsrc/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- thebind_msg!macro and thebind_trigger!macro for literal triggers.setup_settings_shortcutshows theManaged-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 (seeterminal_focusedandis_editable).src/ui/help/page/navigation.rs- thesc(...)helper usage that renders a config value into a keycap label.
Behaviour rules
Managedscope, notGlobal. Global fires before the focused widget's controller and breaks typing. The only legitimate use ofGlobalis a shortcut that must work no matter what - there should be none in this codebase.- Yield to the terminal. Both
capture.rsandshortcuts.rscheckflux-terminal-areaon 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+Zbelong to the entry, not the app. Checkis_editablebefore dispatching. - Fallback chain is
config → constant → "Escape". A user-supplied accelerator that GTK refuses to parse must not leave the shortcut unbound.parse_triggerhandles 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
SpaceorEnter. Both are handled inhandle_key_eventwith specificis_editableandterminal_focusedguards that a newShortcutControllerwill 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 withF5, 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.