Skip to content

Repository files navigation

FileCTRL

CI License: MIT

FileCTRL is a light, opinionated, responsive, theme-able, and simple Text User Interface (TUI) file manager for Linux and macOS

42KM theme

Features

Installation

Download a pre-built binary. Each release publishes filectrl_{system}_{arch}.tar.gz and a .sha256 checksum for linux_x86_64, linux_arm64, and darwin_arm64 (macOS is Apple Silicon only).

curl -sL https://github.com/andornaut/filectrl/releases/latest/download/filectrl_linux_x86_64.tar.gz | tar -xz filectrl
sudo mv filectrl /usr/local/bin/

The archives also contain LICENSE and README.md; tar -xz filectrl extracts only the binary.

On macOS, allow the unsigned binary to run:

xattr -d com.apple.quarantine filectrl

Building

cargo build --release && sudo cp target/release/filectrl /usr/local/bin/

Usage

filectrl [OPTIONS] [DIRECTORY]
Option Description
-c, --config <PATH> Read the config from PATH, or write it there when combined with a --write-default-* flag
-i, --include <PATH> Merge a TOML file on top of the config. Repeatable; later files take precedence
--no-truecolor Use the 256-color theme instead of detecting truecolor support
--force Replace an existing file when writing defaults, which fails without it
--print-keybindings Print the keybindings, then exit
--write-default-config Write the default config, then exit
--write-default-themes Write the default theme as theme.toml beside the config, then exit
-V, --version Print the version, then exit
-h, --help Print usage, then exit

The four flags that act and exit are mutually exclusive, and each accepts only the arguments that change what it does:

Flag Also accepts
--print-keybindings --config, --include
--write-default-config --config, --force
--write-default-themes --config, --force
--version nothing

Anything else is reported rather than ignored. Both write flags print the path they wrote, which follows $XDG_CONFIG_HOME and so is not always under ~/.config.

Bookmarks

Bookmarks are symlinks to folders, stored in a bookmarks/ directory beside the config file (e.g. ~/.config/filectrl/bookmarks/).

Key Action
B Bookmark the current directory. The prompt defaults to the directory's name
' or ` Show all bookmarks in the table
Enter Navigate to the linked folder
r, d Rename or delete the bookmark

Names must be unique, cannot be empty, and cannot contain a path separator.

Copy / paste

Copying or cutting puts ${operation} ${path} on the system clipboard, where operation is cp or mv. Pasting in another FileCTRL window performs the equivalent of ${operation} ${path} ${current_directory}, e.g. cp filectrl.desktop ~/.local/share/applications/.

Without a system clipboard (e.g. over SSH or on a bare console), copy and paste still work within a single window. Pasting with nothing to paste and no system clipboard to read shows a warning, since an entry copied in another window would be unreachable.

When the destination already contains an entry with the same name, the paste stops and asks:

Key Action
s Skip this entry
S Skip this entry and every later collision in the same paste
o Replace the existing entry
O Replace this and every later collision in the same paste
Esc Abandon the rest of the paste
  • An existing directory is never replaced, so only the skip choices are offered for one. Modifier chords are not choices: Ctrl+o abandons the paste.
  • S and O also cover copies already running: if another program takes a name inside a directory being copied, the standing answer settles it without stopping the copy. Only S settles a directory. Anything left unsettled is reported when the copy finishes.
  • A cut that skipped an entry keeps its original: the skipped entry is not at the destination, so removing the source would take the only copy of it.
  • Whatever is not pasted (collisions you abandon, entries that failed) stays on the clipboard, so pasting again retries exactly those. Entries you skip deliberately do not. If nothing was pasted at all, the clipboard is unchanged.

Multi-select

Mark entries to apply chmod, copy, cut, or delete to several at once.

Key Action
v/Space Toggle a mark on the current row
V Enter range mode: the current row becomes the anchor. Press again to exit, keeping the marks
Esc Clear all marks and exit range mode

In range mode, moving the cursor or clicking extends the marked range from the anchor to the cursor. Outside range mode, clicking only moves the cursor. Marking clears the clipboard.

Marks name entries but are stored as row positions, so what becomes of them depends on why the listing changed:

Change Marks
Sorting, filtering, toggling hidden files Cleared
Reload (Ctrl+r or a watcher refresh) Kept, re-found by path. An entry that is gone loses its mark
A search finishing or being cancelled Kept
Navigating to another directory Cleared
chmod, copy, cut, delete Consumed by the operation

Filtering

The filter (f/\) is a case-insensitive substring match against the Name column, so it matches what is on screen: the entry's own name in a normal listing, the path relative to the search root while searching, and the bookmark name in the bookmarks view.

Directories carry a trailing / outside the bookmarks view, so / filters a listing down to directories, and docs/ matches both the docs directory and, in search results, everything under it.

Searching

Search (/) walks the current directory recursively, matching a case-insensitive substring against each entry's name. Symlinked directories are not descended into. search_max_depth and search_max_results in [file_system] bound the walk; on reaching either, FileCTRL keeps the results it has and says so.

Results appear as the walk finds them and settle into the sort order once it ends, whether it finished or was cancelled.

Sorting

n/m/s sort by name, modified time, or size; clicking a column header does the same. Sorting by the same column again reverses it. Each column starts in the direction it is usually reached for:

Column Default direction
Name A-Z
Modified Newest first
Size Largest first

The Name column orders by the text it displays (while searching, the path relative to the search root), ignoring case and a leading dot on each path segment, so a dot file sorts next to its neighbours the way ls -a does. sort_directories_first in the [ui] section groups directories first, for the Name column only.

Default keybindings

All keybindings can be customized.

Normal mode

Actions Keys
Select next, previous row /j, /k
Select first, middle, last row Home/g/^, z, End/G (Uppercase)/$
Select top, middle, bottom visible row H (Uppercase), M (Uppercase), L (Uppercase)
Page down, up PgDn/Ctrl+d/Ctrl+f, PgUp/Ctrl+u/Ctrl+b
Go to parent dir /h/b/Backspace
Go to previous dir -
Go to home dir ~
Go to path :/Tab
Open /l/Enter
Open current directory t
Open new window w
Open with... o
Mark/unmark item v/Space
Range mark V (Uppercase)
Copy, Cut, Paste y/Ctrl+c, x/Ctrl+x, p/Ctrl+v
Rename r/F2
Chmod (octal) P (Uppercase)
Create directory c
Delete d/Delete
Filter f/\
Search /
Add bookmark B (Uppercase)
Show bookmarks '/`
Refresh Ctrl+r/F5
Sort by name, modified, size n, m, s
Toggle show hidden files .
Cancel file or search operations K (Uppercase)
Clear alerts, progress Ctrl+a, Ctrl+p
Clear clipboard/filter/marks/search, exit bookmarks view Esc
Toggle help ?
Quit q

Prompt mode

Actions Keys
Submit Enter
Cancel Esc
Reset to initial value Ctrl+u/Ctrl+z
Select all Ctrl+a
Copy, Cut, Paste text Ctrl+c, Ctrl+x, Ctrl+v
Move cursor /
Move cursor by word Ctrl+/
Move cursor to start, end Home, Ctrl+e/End
Select text Shift+/
Select to line start, end Shift+Home, Shift+End
Select by word Ctrl+Shift+/
Delete before, after cursor Backspace, Delete
Accept path suggestion (cursor at end of input) Tab
Cycle path suggestions (cursor at end of input) /

Note

Ctrl+Shift keybindings require a terminal that supports the kitty keyboard protocol (e.g. Alacritty). tmux users must also add the following to ~/.tmux.conf:

set -g extended-keys on
set -ga terminal-features ",*:extkeys"

Configuration

The configuration is drawn from the first of:

  1. The path given by --config
  2. ~/.config/filectrl/config.toml, if it exists
  3. The built-in default configuration

filectrl --write-default-config writes the defaults to that path. It writes the configuration keys only; the theme keys are a separate file written by --write-default-themes.

Override only what you want to change:

# ~/.config/filectrl/config.toml
[openers.linux]
open_directory = "alacritty --working-directory %s"
open_filectrl_window = "alacritty --command filectrl %s"

Validation is strict: an unrecognized key (a misspelled setting or theme property), an unknown modifier name, or an invalid value (such as buffer_min_bytes exceeding buffer_max_bytes) makes FileCTRL exit with an error rather than ignore it.

Opening in other applications

Key Opens with
l openers.open_file
t openers.open_directory, for the current directory
w openers.open_filectrl_window, a new filectrl window
o A picker of the applications that can open the selection
# Use [openers.linux] on Linux, or [openers.macos] on macOS.
# %s is replaced at runtime: the current directory, the selected entry, or a
# new window's directory. In run_in_terminal alone it is a command line
# rather than a path (see "Open with..." below).
[openers.linux]
open_directory = "alacritty --working-directory %s"
open_file = "pcmanfm %s"
open_filectrl_window = "alacritty --command filectrl %s"
run_in_terminal = "alacritty --command %s"

[openers.macos]
open_directory = "open %s"
open_file = "open %s"
open_filectrl_window = "open -a Terminal %s"
run_in_terminal = "" # Linux only, ignored here

Open with…

o replaces the file table with the applications that can open the selection, leaving the breadcrumbs and status bar visible. The default application is listed first and marked (default).

Key Action
/j, /k Move between applications
/l/Enter Open with the selected application
1 to 9 Open with that numbered application
o Close the picker
Esc Close the picker and reset the view (clears the clipboard, filter and marks, and leaves search or bookmarks)

Only the first nine rows have a number; scroll to reach the rest. Applications that share a name are collapsed to the best ranked one.

The list is built per platform:

  • Linux: the MIME type is resolved through the shared MIME database, including its parent types, so a .rs file also offers plain text editors. It is then matched against mimeapps.list and the .desktop files under $XDG_DATA_DIRS/applications, per the mime-apps spec. The application directories are indexed once per run, so an application installed while FileCTRL is open is not offered until the next start.
  • macOS: Launch Services, which requires macOS 12 or newer. The chosen application is launched with open -a.

Two openers settings shape the list, and setting either to "" drops its effect:

  • Applications that need a terminal (Terminal=true) run inside openers.run_in_terminal, whose %s is a command line: xterm -e %s becomes xterm -e vim '/some file.txt'.
  • openers.open_file (or openers.open_directory for a directory) is offered last, showing its command template beside the setting name, so the picker still works with no application database. Without it, a path that matches nothing shows "No applications found".

Theming

[theme] applies to truecolor terminals and [theme256] to 256-color terminals. FileCTRL detects truecolor support via $COLORTERM; --no-truecolor selects the 256-color theme regardless. There is no flag for the other direction: a terminal that supports truecolor but does not set $COLORTERM (common under tmux, and under some SSH and sudo sessions) gets the 256-color theme, so set the variable yourself with COLORTERM=truecolor filectrl.

Style properties

Each theme entry is a style. All three properties are optional; set fg or bg to "" to inherit the parent widget's color.

Property Format Default
fg Color string Inherited
bg Color string Inherited
modifiers Array of strings []
  • Truecolor ([theme]): hex strings like "#FF0000", or named colors like "Red"
  • 256 color ([theme256]): decimal indexes "0" through "255"
  • Modifiers: "bold", "dim", "italic", "underlined", "blink", "rapid_blink", "reversed", "crossed_out"
[theme.table.selected]
bg = "#1A1A2E"

[theme.file_type.directory]
fg = "#E94560"
modifiers = ["bold"]

Theme sections

Section Description
[theme] / [theme256] Base foreground, background, and modifiers
alert Alert bar (base, error, info, warn)
breadcrumbs Path breadcrumbs (base, ancestor, basename, separator)
clipboard Clipboard status indicators (copy, cut, delete)
file_modified_date Date column by age (less_than_minute, less_than_hour, less_than_day, less_than_month, less_than_year, greater_than_year)
file_size Size column by magnitude (bytes, kib, mib, gib, tib, pib)
file_type Row colors by file type (directory, executable, symlink, regular_file, etc.)
help Help panel (base, header, actions, shortcuts)
notice Notice bar (filter, progress)
open_with Open with... picker (base, detail, selected, shortcut)
prompt Input prompt (cursor, input, label, selected)
scrollbar Scrollbar (ends, thumb, track, plus show_ends boolean)
status Status bar (detail, label)
table File table (body, header, header_sorted, selected, marked, delete, bookmark)

LS_COLORS integration

With ls_colors_take_precedence, colors from $LS_COLORS are applied on top of the configured file type colors, including extension patterns such as *.tar=01;31.

[theme.file_type]
ls_colors_take_precedence = true

External theme files

include_files merges other TOML files on top of the config:

include_files = ["theme.toml"]
  • Relative paths resolve from the directory containing the config file; absolute paths are used as-is
  • Files merge in order, later ones taking precedence over the base config and over earlier files
  • The value must be an array of strings, and every listed file must exist and parse, or FileCTRL exits with an error

Export the defaults, then copy and edit:

filectrl --write-default-themes  # writes ~/.config/filectrl/theme.toml
cp ~/.config/filectrl/theme.toml ~/.config/filectrl/solarized.toml

--include/-i applies a theme without editing the config. It is repeatable and merges in order, later ones taking precedence. Unlike include_files, relative paths resolve against the current directory:

filectrl -i ~/.config/filectrl/solarized.toml -i overrides.toml

Bundled themes

Theme Inspired by Screenshot
IBM1970 (default) vscode-ibm1970-theme IBM1970
42KM vscode-42km-theme 42KM
filectrl --include themes/42km.toml

Customizing keybindings

Keybindings live in the [keybindings] section of config.toml. A value is a single key string or an array of them.

[keybindings]
# Normal mode
quit = "q"
toggle_help = "?"
...
# Prompt mode
prompt_submit = "Enter"
prompt_reset = ["Ctrl+u", "Ctrl+z"]
...

Key strings support:

Form Examples
Single characters "q", "/", "~", "^", "$"
Uppercase (implies Shift) "G", "V", "N"
Named keys "Enter", "Esc", "Backspace", "Delete", "Space", "Tab", "BackTab", "Up", "Down", "Left", "Right", "Home", "End", "PgUp", "PgDn"
Function keys "F2", "F5"
Modifier prefixes "Ctrl+c", "Shift+Left", "Ctrl+Shift+a"

"Shift+g" is equivalent to "G", and "Shift+Tab" to "BackTab".

Binding one key to two actions in the same mode prevents startup, including a collision between a key you configured and a default you did not override. Assigning the same key to one action more than once is allowed.

Some keys are hardcoded and always work alongside any configured keys, scoped to their mode:

Mode Hardcoded
Normal Arrow keys, Home/End, PageUp/PageDown, Esc
Prompt Esc (cancel), Tab (accept suggestion), / (cycle suggestions)

Because the scoping is per mode, Tab is still configurable in normal mode, where the default goto binding uses it. Binding a hardcoded key to a different action in the same mode prevents startup; binding it to its own action is allowed.

The help view (?) reflects the configured keybindings.

Desktop entry

To make filectrl the default application for opening directories:

cp filectrl.desktop ~/.local/share/applications/
xdg-mime default filectrl.desktop inode/directory
update-desktop-database ~/.local/share/applications/

Developing

# Run against a directory, logging to ./err
RUST_LOG=debug,notify=info cargo run -- fixtures/ 2>err

# Typecheck the macOS-only code without a Mac
rustup target add aarch64-apple-darwin
cargo check --target aarch64-apple-darwin

fixtures/ is a committed file tree for manual UI testing. Navigate into it with cargo run to exercise rendering edge cases:

Path Covers
file_types/ Named pipe, symlinks, executable, and directory permission variants (other-writable, sticky)
no_delete/ Delete and rename permission errors. Needs chmod 555 fixtures/no_delete first; git does not track the read-only bit
scrolling/ 48 entries with long filenames interspersed, for scrolling and multi-row truncation
Elsewhere Executables, symlinks, hidden files, Unicode names, special characters, long filenames

Date-color and size-color buckets need fixtures git cannot store (mtimes, sparse files); create them locally with touch -t and truncate.

Git hooks

Changing cargo-husky configuration:

  1. Edit the [dev-dependencies.cargo-husky] section of Cargo.toml
  2. rm .git/hooks/pre-commit (or other hook file)
  3. cargo clean
  4. cargo test
  5. Verify that the changes have been applied to .git/hooks/pre-commit

Releasing

Push a semantic version tag from an up-to-date main. The release workflow builds the binaries and creates the GitHub Release.

git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0

Pushes to main rebuild the rolling dev release. The workflow manages that tag; do not push it manually.

About

A light, opinionated, responsive, theme-able, and simple Text User Interface (TUI) file manager for Linux and macOS

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages