Skip to content

Repository files navigation

Hoja

Platform: Linux Built with GPUI

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.

Features

Panes

  • 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.zst archives 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.

File operations

  • Optimized transfers. Hoja automatically chooses the fastest correct method for every transfer: rename() for moves on the same filesystem, reflinks on supported filesystems, and copy_file_range() elsewhere with fallbacks. Sparse files, permissions, timestamps, extended attributes, symlinks, and hardlinks are preserved.
  • Undo. Press ctrl-z to take back the last thing you did, whether that was a delete or a transfer. delete moves 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-space stops or starts them all. A transfer pauses between files, so one already in flight finishes first; the row says pausing… 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.

Navigation & search

  • Recursive search. Press ctrl-f to 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 and enter to 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-p to 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-p to search available commands. Frequently used commands naturally rise to the top.

Appearance

  • Human-friendly dates. The Modified column shows relative times such as just now, 3 hours ago, or 2 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.

Install

On Arch Linux, build the package from packaging/:

cd packaging && makepkg -si

It builds from the current main, since there are no releases yet.

Start

hoja [DIRECTORY] [--theme NAME] [--list-themes]

The HOJA_THEME environment variable also sets the theme.

Settings

Write ~/.config/hoja/settings.json. Every field is optional, comments are allowed, and Hoja applies a change as soon as you save.

{
  // 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"]
  }
}

--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

Keys work on the pane that has focus.

Move

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

Select

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

Change files

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

Panes and view

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

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

While you edit a path

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

In a menu, a dialog, or the command palette

Keys Action
/ Move the highlight
Enter / Escape Choose, or close

Build

  1. Install Rust 1.95 or later. The Zed source sets this minimum.
  2. On Arch Linux, install these packages: clang cmake pkgconf fontconfig freetype2 wayland wayland-protocols libxkbcommon libxkbcommon-x11 vulkan-icd-loader alsa-lib openssl zstd
  3. 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.

Checks

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 --workspace

Clippy 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.

Testing the interface

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.sh

It 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.sh

Rebuilt 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.sh

context-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.sh

columns.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.sh

search-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.sh

crash-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.sh

Two 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.

Transfer engine

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-transfer

The 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 -- --ignored

Roadmap

Planned 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.

License

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.

About

GPU-accelerated file manager for Linux, built with GPUI

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages