Hoja (OH-hah) is a file manager for Linux. It runs on Wayland and renders via GPUI, the GPU-accelerated framework from the Zed editor. It features recursive pane layouts, background folder size calculation, optimized file transfers, undoable deletes, live directory updates, and archive browsing that feels like working with ordinary folders.
Warning
Hoja is experimental software. Use at your own risk.
- Recursive pane layout. Split the window into panes. Each pane shows one directory, and panes can be split recursively into any layout.
- Fast directory listings. Rows are virtualized, so even very large directories stay smooth while scrolling.
- Independent navigation. Every pane has its own history. Go back, forward, up, or home, or click the path to jump to another location.
- Recursive folder sizes. Folder sizes are calculated in the background and appear only when complete, so values never count upward in the listing. Sorting by size happens once, after every folder has a final value.
- Pane footer. Each pane summarizes exactly what it contains: total items and size, selected files, permissions and ownership for a single file, or live search progress.
- Clear active pane. The active pane keeps full emphasis while inactive panes dim their names, icons, paths, and selections. Git status colours are preserved.
- Per-pane view settings. Hidden files, folder grouping, and sort order are configured independently for each pane. Hidden files are off by default.
- Archives as folders. Browse
.zip,.tar,.tar.gz,.tar.bz2,.tar.xz, and.tar.zstarchives like directories. Tar archives stream while loading so files appear immediately, and archive folders report their recursive sizes instantly. Copy files out using the keyboard, context menu, or drag and drop. Archives are always read-only. - Live listings. Hoja watches every directory it displays. If another program changes the contents, the pane refreshes without losing your selection. If the directory disappears, Hoja moves to the nearest existing parent.
- Optimized transfers. Hoja automatically chooses the fastest correct
method for every transfer:
rename()for moves on the same filesystem, reflinks on supported filesystems, andcopy_file_range()elsewhere with fallbacks. Sparse files, permissions, timestamps, extended attributes, symlinks, and hardlinks are preserved. - Undo. Press
ctrl-zto take back the last thing you did, whether that was a delete or a transfer.deletemoves files to the freedesktop trash, which is instant on the same filesystem and compatible with other Linux file managers. Undoing a transfer removes what it copied and restores anything it replaced. It checks first: a file edited since the transfer is left alone and reported, and what it removes goes to the trash rather than being deleted, so an undo can itself be undone. - Drag and drop. Drag files between panes, onto folders, or into other applications. Moves stay on one filesystem, copies cross filesystems, and modifier keys override the default behaviour.
- Clipboard integration. Copy and paste files between Hoja and other file managers using the standard GNOME clipboard format.
- Transfer progress. Progress reports bytes copied, transfer speed, time remaining, and file counts from the start of the operation. Fixed-width columns keep everything from shifting while values change.
- Transfer errors. Failed transfers stay in the progress list with a warning. Open the details to see every failed file and its error.
- Pause and resume. Every transfer has a pause control, and
ctrl-shift-spacestops or starts them all. A transfer pauses between files, so one already in flight finishes first; the row sayspausing…until it has actually stopped. - Interrupted transfers. If Hoja is killed partway through a transfer, the next start clears up the half-written files it left and offers to finish what it had not done.
- Desktop notifications. Long-running transfers notify you when they finish, and failures always generate a notification using the standard freedesktop notification service.
- Recursive search. Press
ctrl-fto search every directory below the current one. Results appear as they are found. The first result is ready immediately: use the arrow keys to move through them andenterto open one, all without leaving the search field. It works inside an archive too, where there is nothing to walk: the member list the pane already read answers immediately, and results keep arriving if the archive is still being read. Each result is labelled by where it sits, and copying one out keeps the structure below the folder you searched. - Places. Press
ctrl-pto jump to your home directory, bookmarks, or attached drives. Hoja reads the same bookmarks used by GTK file dialogs and can mount or eject removable drives. - Command palette. Press
ctrl-shift-pto search available commands. Frequently used commands naturally rise to the top.
-
Human-friendly dates. The Modified column shows relative times such as
just now,3 hours ago, or2 months ago. -
Columns you choose. A pane shows the size, the kind and the modified time. It can also show the permissions, the owner and the group. Use the Columns list in the view menu to show a column or to hide one. Click a header to sort by that column, and drag a header sideways to move the column. A new pane from a split shows the same columns as the pane it came from.
You can also set the order in the settings file with a list. A column you hide keeps its place until you close Hoja. After that it comes back in the usual place, because a column that is not shown has no position to record.
-
Git status. File names are coloured by Git status, including folders, using Git's own status information so Hoja always matches the command line.
-
Themes. Hoja reads Zed theme files from
~/.config/hoja/themes/and reloads them automatically. Rosé Pine themes are included. -
Icons. File icons follow the Zed icon system and inherit colours from the active theme.
-
Settings. Configure Hoja with
~/.config/hoja/settings.json. Hoja never rewrites the file, preserves your comments, and reloads changes automatically.
On Arch Linux, build the package from packaging/:
cd packaging && makepkg -siIt builds from the current main, since there are no releases yet.
hoja [DIRECTORY] [--theme NAME] [--list-themes]The HOJA_THEME environment variable also sets the theme.
Write ~/.config/hoja/settings.json. Every field is optional, comments are
allowed, and Hoja applies a change as soon as you save.
--theme on the command line, then $HOJA_THEME, then this file.
Hoja keeps what you change through the interface (the sort order, hidden
files, the columns you show, and the column widths you drag) in
~/.local/state/hoja/state.json,
and reads it back at the next start. It writes that file and you write the
other one, so neither can overwrite the other. When both have an answer, the
more recent one applies: what you last toggled survives a restart, and editing
the settings file takes effect over it.
Two Hoja windows share that file safely. Each one writes only the settings you changed in it, so a change made in one window is not undone by the other.
Keys work on the pane that has focus.
| Keys | Action |
|---|---|
| ↑ / ↓ | Move the selection one row |
| Page Up / Page Down | Move the selection one screen |
| Home / End | Move to the first or the last entry |
| Enter | Open the selected entry. A folder or a .zip opens in the pane. |
| Type a name | Jump to the first match. Repeat one letter to cycle the matches. |
| Alt + ← / Alt + → | Go back and forward in the history |
| Alt + ↑ / Backspace | Go to the parent directory |
| Alt + Home | Go to the home directory |
| Ctrl + L | Edit the path |
| Ctrl + F | Search this folder and everything below it |
An outline marks the entry that the keys act on. It is usually also selected. Use Ctrl with the movement keys to move the outline alone, and Ctrl + Space to add or remove that one entry. This builds a selection of entries that are not next to each other.
| Keys | Action |
|---|---|
| Shift + ↑ / Shift + ↓ | Extend the selection one row |
| Shift + Page Up / Shift + Page Down | Extend the selection one screen |
| Shift + Home / Shift + End | Extend the selection to the first or the last entry |
| Ctrl + ↑ / Ctrl + ↓ | Move the outline and keep the selection |
| Ctrl + Space | Add or remove the outlined entry |
| Ctrl + A | Select all |
| Escape | Stop searching, or clear the selection |
| Keys | Action |
|---|---|
| F2 | Rename the selected entry |
| Delete | Delete the selection |
| Ctrl + Z | Undo the last delete or transfer |
| Ctrl + C / Ctrl + X / Ctrl + V | Copy, cut, paste |
To split in another direction, or to move to the pane above or to the left, open the command palette and type the name. These commands are not on a key because they are rare.
| Keys | Action |
|---|---|
| Tab / Shift + Tab | Move to the next or the previous pane |
| F3 | Split the active pane |
| Ctrl + W | Close the active pane |
| Ctrl + H | Show or hide hidden files |
| Ctrl + Shift + D | Dismiss finished transfers |
| Ctrl + Shift + Space | Pause or resume every transfer |
| Ctrl + Shift + P | Open the command palette |
| Ctrl + P | Go to a place: home, a bookmark, or a drive |
| Ctrl + E | In that list, eject the highlighted drive |
| Mouse | Action |
|---|---|
| Click | Select the row |
| Ctrl + Click | Add or remove one row |
| Shift + Click | Select a range |
| Double-click | Open the entry |
| Right-click | Open the context menu |
| Click a column header | Sort. Click again to reverse the order. |
| Drag a header divider | Resize the column |
| Click the magnifier | Start or stop a search |
| Drag rows | Move them. Across filesystems, Hoja copies them. |
| Ctrl + Drag / Shift + Drag | Always copy / always move |
| Drop on a folder row | Put the files in that folder |
| Back and forward buttons | Go back and forward in the history |
| Keys | Action |
|---|---|
| ← / → / Ctrl + ← / Ctrl + → | Move one character or one word |
| Home / End | Move to the start or the end |
| add Shift | Extend the selection instead |
| Ctrl + Backspace / Ctrl + Delete | Delete one word |
| Enter / Escape | Go to the path, or cancel |
| Keys | Action |
|---|---|
| ↑ / ↓ | Move the highlight |
| Enter / Escape | Choose, or close |
- Install Rust 1.95 or later. The Zed source sets this minimum.
- On Arch Linux, install these packages:
clang cmake pkgconf fontconfig freetype2 wayland wayland-protocols libxkbcommon libxkbcommon-x11 vulkan-icd-loader alsa-lib openssl zstd - Run
cargo build --release.
Note: Cargo compiles GPUI with your local toolchain. The toolchain file in the Zed repository does not apply to git dependencies.
CI runs on every push and pull request, and the same three commands run locally:
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspaceClippy runs again in release, which is the one profile where dead code shows up differently: a method reached only from a debug assertion is live in one and not the other.
scripts/sway-harness.sh runs Hoja in a compositor of its own, so a test of
the interface does not touch the desktop you are using. It takes a directory to
open and a script of things to do, and prints where the screenshots went:
cargo build
scripts/sway-harness.sh ~/some/dir my-test.shIt needs sway, grim, wtype and wlrctl, and it exercises the Wayland
backend Hoja actually ships on. The test script can click rows, send keys, and
capture the window or just its footer; nothing takes your focus, because every
one of those tools is scoped to the nested display rather than to whatever the
real session has focused. HOJA_TEST_WIDTH and HOJA_TEST_HEIGHT size the
window, which is how docs/screenshot.png is made. HOJA_TEST_HEADLESS=1 hides
it, at the cost of keyboard input, which does not reach the app there.
Nested, its window presents to the host as class wlroots, so float it at a
fixed size or a tiling compositor will stretch it and move the rows a test
clicks.
A wait_for that times out ends the run. That is a safety property rather
than a tidiness one: once a wait has failed the script no longer knows where
the window is, and everything after it is keystrokes sent somewhere unverified.
An earlier version carried on, walked a pane up to the root of the filesystem
and pressed paste there; nothing was written only because root is not writable.
There are thirteen suites in scripts/tests/. listing.sh runs against ~/Mock;
archive.sh and tar.sh need fixtures, which scripts/tests/setup-archives.sh
builds and prints the path of:
scripts/sway-harness.sh "$(scripts/tests/setup-archives.sh)" scripts/tests/archive.sh
scripts/sway-harness.sh "$(scripts/tests/setup-archives.sh)" scripts/tests/tar.shRebuilt for each, not shared between them: archive.sh copies a folder out of
an archive and leaves it in the fixture directory, which is the tenth row
tar.sh does not expect. setup-archives.sh clears the directory before it
builds, so calling it twice is the whole fix.
transfer.sh needs setup-transfer.sh, and pauses a transfer by stopping it
on a conflict first: four thousand files copy in about 170ms here, so a test
that raced one would lose.
XFER=$(scripts/tests/setup-transfer.sh)
scripts/sway-harness.sh "$XFER" scripts/tests/transfer.shcontext-menu.sh needs setup-context-menu.sh, whose fixture is a PNG rather
than a text file on purpose: "Open With" is a submenu because the flat list was
capped at eight applications, and proving the cap is gone needs a type that
draws more than eight handlers. Images do; plain text does not.
scripts/sway-harness.sh "$(scripts/tests/setup-context-menu.sh)" scripts/tests/context-menu.shcolumns.sh needs setup-columns.sh, which sets the modes it asserts with
chmod rather than inheriting whatever umask gives it. The suite writes
settings.json while Hoja runs and waits for the header to follow, so it covers
the whole path a hand-written line takes without a coordinate in it.
scripts/sway-harness.sh "$(scripts/tests/setup-columns.sh)" scripts/tests/columns.shsearch-click.sh uses the same fixture. It covers what a search does when it
is used with the mouse and the arrows rather than only with enter.
scripts/sway-harness.sh "$(scripts/tests/setup-columns.sh)" scripts/tests/search-click.shcrash-phase1.sh and crash-phase2.sh are one test in two runs against one
state directory. The harness kills the app when a script returns, which is the
crash; HOJA_TEST_KEEP_STATE=1 stops the second run wiping what the first
left behind.
OUT=$(mktemp -d)
scripts/sway-harness.sh "$XFER" scripts/tests/crash-phase1.sh "$OUT"
HOJA_TEST_KEEP_STATE=1 \
scripts/sway-harness.sh "$XFER" scripts/tests/crash-phase2.sh "$OUT"resort-while-reading.sh, interrupt-archive-read.sh,
search-while-reading.sh and select-while-reading.sh all act while an
archive is still being read, so they
need a fixture slow enough to act in. setup-slow-archive.sh builds it, and
is kept separate so a ~15 MB member genuinely slow to read (bzip2, real
pseudo-random content) does not change the row counts the other suites assert
on:
SLOW=$(scripts/tests/setup-slow-archive.sh)
scripts/sway-harness.sh "$SLOW" scripts/tests/resort-while-reading.sh
scripts/sway-harness.sh "$SLOW" scripts/tests/interrupt-archive-read.sh
scripts/sway-harness.sh "$SLOW" scripts/tests/search-while-reading.sh
scripts/sway-harness.sh "$SLOW" scripts/tests/select-while-reading.shTwo things it cannot do. It cannot synthesise a drag: wlrctl only clicks, and
swaymsg seat - cursor moves the pointer without gpui starting a drag from it,
so column resizing and drag-and-drop need a person. And it has no second
application to drag from or paste into, so inbound drops and clipboard interop
have to be tried in a real session.
One thing to know if the nested window is sent to another workspace by a rule:
give it render_unfocused too. A window nobody is looking at gets no frame
callbacks, so the nested compositor paints nothing, hoja never renders past
its first frame, and every assertion times out against a listing that stays
empty.
scripts/x11-harness.sh is kept but does not currently run: Xvfb offers no
DRI3, gpui never gets a GPU context, and the window never opens. Testing the
X11 backend needs a real X server.
The hoja-transfer crate contains the transfer engine. It is a standard
Rust library with no UI dependencies. One worker thread does each job. The UI
reads progress from atomic counters.
Each job also records what it changed, in the form that reverses it, and hands that back when it finishes. The record stays small because a directory stands for its contents: copying into a name that held nothing records the directory and nothing beneath it, and a move within one filesystem is a single rename however large the tree. Per-file records appear only where a copy merged into a directory that already existed. Undoing a transfer is itself a job, so it gets the same progress bar, cancel button and error report as the transfer it is taking back.
To run the engine tests:
cargo test -p hoja-transferThe reflink test needs a btrfs filesystem. To prepare one:
./scripts/btrfs-loop.sh up
HOJA_TEST_BTRFS=/tmp/hoja-btrfs/mnt cargo test -p hoja-transfer -- --ignoredPlanned and not complete:
- Writing to an archive. Everything here reads; rename, delete and paste inside one are refusals rather than half-features.
- Tabs, previews and thumbnails.
- Query history in the pickers, and modal geometry that persists.
- Explicit sync between two directories, chunked so that repeating it moves only what changed.
- Sync between two machines, Hoja to Hoja.
Planned once, and now in doubt:
- Parallel copy for many small files. Measured against
xcp: 4.7x faster on tmpfs, and slower than a single thread on real devices. The win was the RAM disk rather than the parallelism, so this needs a benchmark that is not a tmpfs before it is worth building. What the same measurements did point at is the atomic temp-file rename, which was half the time of an exFAT copy. - io_uring underneath it, which was always conditional on that benchmark.
The license of Hoja is GPL-3.0-or-later. See the LICENSE file.
GPUI has the Apache-2.0 license. The Zed theme and file_icons crates have
the GPL-3.0-or-later license. This is why Hoja uses the GPL.
The included assets have their own licenses:
- The icons Hoja's own interface uses are Lucide, under the ISC license. The
file-type icons are Zed's set, which includes brand and language marks
belonging to their own projects. See
assets/icons/LICENSES. - The Rosé Pine themes have the MIT license. See
assets/themes/rose-pine/LICENSE.

{ // A theme in ~/.config/hoja/themes/, or a bundled Rosé Pine variant. "theme": "Rosé Pine Moon", // What a new pane shows. "view": { "sort": { "key": "name", "direction": "ascending" }, "show_hidden": false, "folders_first": true, // Only the columns you name change. The others keep their usual state. "columns": { "permissions": true } // Or give a list, which sets the order as well as the set. A list shows // the columns it names and hides every other one. // "columns": ["permissions", "size", "modified"] } }