Covers what haul is, how to build it, and how to use it. The build section is deliberately detailed about cgo: haul needs a C compiler and the platform graphics headers, unlike dial, and cross-compiling needs a per-target toolchain rather than just GOOS/GOARCH. Documents fyne-cross as the practical route to Windows and macOS builds. Claims are limited to what the code actually does — SFTP only, resume within a session only, and ~/.ssh/config treated as read-only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.6 KiB
haul
A desktop SFTP client — a two-pane local/remote file browser over SSH, in the
spirit of WinSCP but quite a bit simpler. haul reads your ~/.ssh/config for
its host list, or connects ad-hoc to anything that isn't in it.
- Native desktop GUI, not a terminal app — built on Fyne, so it's a single compiled binary with no runtime.
- Two panes: local disk on the left, the remote host on the right, with the same navigation, marking, and file operations on both sides.
- Your existing SSH setup just works — saved hosts come from
~/.ssh/config(Includedirectives followed), and connecting resolves the effective config throughssh -G, soProxyJumpchains,IdentityFile,User, andPortbehave exactly as the realsshclient would. - Auth the usual ways:
ssh-agent, identity files (with passphrase prompt), password, and keyboard-interactive. - Host keys are verified, not blindly accepted. An unknown key is shown
with its SHA256 fingerprint for confirmation before it's recorded in
known_hosts. - Recursive transfers in both directions with aggregate progress, a stop button, per-file conflict prompts (overwrite / skip / rename, with "apply to all"), and resume of partial files.
- Never blocks on the network — every SFTP call runs off the UI thread, so listing a slow directory doesn't freeze the window.
- High-contrast dark theme with tightened padding, so more rows fit on screen.
haul was split out of dial
(an interactive SSH host picker) so that file transfer lives in its own tool.
The two share ~/.ssh/config as their common base and are otherwise
independent.
Requirements
To run haul:
| Needs | Why |
|---|---|
The system ssh client |
Saved-host connections call ssh -G <alias> to resolve the effective config, so ssh_config semantics are never re-implemented |
| A graphical desktop | haul is a GUI application; it needs a display server |
| An SFTP-capable server | Transfers use the SFTP subsystem — see Scope |
Building
haul is written in Go and uses cgo (Fyne renders through OpenGL), so a C compiler and the platform's graphics/windowing headers are needed as well as the Go toolchain.
Prerequisites
Go 1.24 or newer — check with go version.
Then the C toolchain and system headers Fyne needs:
# Debian / Ubuntu
sudo apt install gcc libgl1-mesa-dev xorg-dev
# Fedora
sudo dnf install gcc libX11-devel libXcursor-devel libXrandr-devel \
libXinerama-devel mesa-libGL-devel libXi-devel libXxf86vm-devel
# Arch
sudo pacman -S gcc libx11 libxcursor libxrandr libxinerama mesa libxi
# macOS — the Xcode command line tools
xcode-select --install
On Windows, use a MinGW-w64 toolchain (for example via MSYS2) so cgo has a C compiler.
Build it
make build # -> bin/haul
That's the whole thing. Run it with ./bin/haul, or install it onto your
PATH:
make install # -> $GOBIN, or $GOPATH/bin (usually ~/go/bin)
Make sure that directory is on PATH:
echo 'export PATH="$(go env GOPATH)/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
Or just copy bin/haul wherever you like:
install -D -m 0755 bin/haul ~/.local/bin/haul
Expect the first build to take a few minutes. Compiling Fyne's cgo dependencies from scratch is the slow part; afterwards Go's build cache makes rebuilds close to instant.
Without make, the equivalent is:
go build -trimpath -ldflags "-s -w" -o bin/haul ./cmd/haul
Make targets
| Target | Does |
|---|---|
make build |
Native binary into ./bin/haul |
make install |
Build and install into $GOBIN / $GOPATH/bin |
make test |
Run the test suite |
make vet |
go vet ./... |
make check |
Typecheck every package without needing the cgo toolchain |
make run |
Build, then run (needs a DISPLAY) |
make run-xvfb |
Build, then run against a virtual X display |
make dist |
Release binary into ./dist |
make clean |
Remove bin/ and dist/ |
The Makefile picks up a Go toolchain at ~/.local/go/bin/go if one is there,
falling back to whatever go is on PATH — so it works on machines where Go
is deliberately kept off PATH. Override it explicitly with make GO=/path/to/go build.
make check is the useful one when the C dependencies aren't installed: it
builds for GOOS=js GOARCH=wasm, which typechecks the entire tree including all
Fyne API usage without any cgo toolchain. It proves nothing about layout or
runtime behaviour, but it catches every compile error.
make run-xvfb is for headless machines — it starts an Xvfb virtual display,
runs haul against it with software GL, and tears the display down again when
you quit (including on Ctrl-C). Requires xvfb installed.
Cross-compiling
This does not work the way it does for dial. dial is pure Go and
cross-compiles to five platforms with nothing but GOOS/GOARCH. haul is cgo,
so GOOS=windows go build will fail — each target needs a matching C
toolchain:
| Target | Needs |
|---|---|
linux/arm64 |
gcc-aarch64-linux-gnu |
windows/amd64 |
gcc-mingw-w64 |
darwin/* |
a Mac, or osxcross |
For that reason make dist builds only the native binary. The practical route
to the others is fyne-cross, which
does each build inside a Docker image carrying the right toolchain:
go install github.com/fyne-io/fyne-cross@latest
fyne-cross windows -arch=amd64
Usage
haul
Connecting
The connect window has two tabs:
- Saved — every
Hostblock from~/.ssh/config, with a search box to filter them. Connecting resolves the alias throughssh -G, so it honours everything your config says about that host,ProxyJumpchains included. - Quick connect — host, port, user, and optionally a password or a private
key file, for anything not in your config. It doesn't touch
~/.ssh/config.
If the server's host key isn't in known_hosts, haul shows the SHA256
fingerprint and asks before recording it. Passphrase-protected keys and
password prompts appear as dialogs during the connection attempt.
Browsing
Each pane has a path bar (type a path and press Enter to jump straight there), up / home / refresh buttons, and New folder, Rename, and Delete actions. In the file table:
| Click | Action |
|---|---|
| the left column | mark / unmark a file for transfer |
| a directory name | descend into it |
.. |
go up one level |
| anywhere else | move the highlight |
Marks apply to the directory you made them in and are cleared when you navigate away, since they refer to names in the directory you left.
Transferring
Upload → copies from the local pane to the remote one, ← Download the other way. Either takes whatever is marked in the source pane, falling back to the highlighted row if nothing is marked, and writes into the other pane's current directory. Directories are copied recursively.
While a transfer runs, the progress bar shows aggregate bytes and file counts, and Stop cancels it. Completed files stay put and the interrupted file is left on disk — running the same transfer again resumes it from where it stopped, for as long as the session stays open.
If a destination file already existed beforehand, haul asks whether to overwrite, skip, or write under a non-colliding name, with an "apply to all" option so you're asked once rather than per file.
Closing the window or disconnecting mid-transfer asks for confirmation first.
Keyboard shortcuts
| Key | Action |
|---|---|
Ctrl+R |
reload the active pane |
Ctrl+Up |
go up a directory in the active pane |
Ctrl+T |
transfer the active pane's selection to the other side |
Ctrl+T uploads or downloads depending on which pane you last touched, so one
key covers both directions.
Scope
haul speaks SFTP only. There is no SCP or TFTP support: SCP is a
copy-stream protocol with no directory listing, stat, rename, or delete, and
TFTP has neither listing nor authentication — neither can back a file browser
without falling back to parsing shell command output. Servers whose sshd has no
sftp-server subsystem are therefore out of reach.
Resume works within a session, because the engine tracks which destination files it created; restarting haul starts those files over.
~/.ssh/config is treated as read-only — haul never writes to it. The only
file it writes outside a transfer is known_hosts, when you confirm a new host
key.