A plain `go build` links no icon resource, so Explorer and the Start menu draw the generic executable glyph. Inno's SetupIconFile only skins setup.exe, so the fix has to happen at build time: `fyne package` turns cmd/haul/Icon.png into a .syso and links it in, and adds -H=windowsgui itself. Documents the installer build alongside it, since the two steps are only useful together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 |
| OpenGL 2.1 or newer | Fyne renders through OpenGL. Remote Desktop sessions, VMs without 3D acceleration, and machines running Microsoft Basic Display Adapter don't provide it — see Remote Desktop and VMs |
| 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
There are two prerequisites — the Go toolchain, and a C toolchain for cgo. Both are needed on every platform, macOS included.
1. Go 1.24 or newer. Verify with go version; if that says
command not found, install it:
# macOS
brew install go
# Debian / Ubuntu
sudo apt install golang-go
# Fedora
sudo dnf install golang
# Arch
sudo pacman -S go
Or download it from go.dev/dl. If your distro's package is older than 1.24, use the tarball from there instead.
2. A C toolchain and the platform's graphics headers, because Fyne renders through OpenGL:
# macOS — the Xcode command line tools
xcode-select --install
# 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
Windows needs a MinGW-w64 toolchain and has no make — it gets its own
section below.
On macOS the command line tools cover the C side entirely — there is no
Homebrew formula to add for graphics. That makes it easy to run
xcode-select --install, see "already installed", and assume you're ready when
Go itself is still missing. Both steps are required.
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
Building on Windows
Windows differs from the instructions above in three ways: cgo needs a
MinGW-w64 compiler, there is no make, and a GUI build needs an extra linker
flag to suppress the console window. All three are covered here.
1. Install Go. Either
winget install GoLang.Go
or the .msi from go.dev/dl, which puts Go on PATH for
you. Open a new terminal afterwards, then check with go version.
2. Install a C compiler. Go's cgo needs a GCC-style compiler —
MSVC/cl.exe will not work. MSYS2 is the usual
source:
winget install MSYS2.MSYS2
Then open the "MSYS2 UCRT64" shell from the Start menu and install GCC:
pacman -S mingw-w64-ucrt-x86_64-gcc
Add its bin directory to your Windows PATH so the normal Go toolchain can
find it — in PowerShell, as your user:
[Environment]::SetEnvironmentVariable("Path",
[Environment]::GetEnvironmentVariable("Path","User") + ";C:\msys64\ucrt64\bin",
"User")
Open a new terminal and confirm both are visible:
go version
gcc --version
3. Build. Windows has no make, so invoke the compiler directly from
PowerShell in the repo root:
go build -trimpath -ldflags "-s -w -H=windowsgui" -o bin\haul.exe .\cmd\haul
Then run .\bin\haul.exe.
Two details in that command matter:
-H=windowsguimarks the binary as a GUI application. Without it Windows opens a console window behind haul every time it launches. The Makefile does not pass this, because it targets Unix — so on Windows prefer the command above even if you do havemake.-o bin\haul.exenames the output explicitly. Windows needs the.exeextension to execute a file.
If you would rather use the Makefile, install make inside the MSYS2 shell
(pacman -S make) and build from there — but you will still want to add
-H=windowsgui yourself, for example
make build LDFLAGS="-s -w -H=windowsgui".
Unlike Linux, Windows needs no graphics development packages to build: OpenGL ships with the operating system. Getting a usable OpenGL at run time is a separate matter — see below.
Packaging on macOS
make build produces a plain Unix executable, and that is all you need if you
launch haul from a shell. Double-click it in Finder, though, and macOS opens a
Terminal window alongside the app. That is not a bug in haul and no build flag
fixes it — it is what Finder does with any bare executable: having no better
idea what a Mach-O file is, it hands it to Terminal.app to run, and the terminal
stays on screen for as long as the process does. macOS has no equivalent of
Windows' -H=windowsgui.
The fix is to ship an application bundle — a haul.app directory carrying an
Info.plist — which Finder launches directly, with a Dock icon and no terminal:
make package-darwin # -> ./haul.app
Then drag haul.app into /Applications. Run it on a Mac: it compiles with the
host cgo toolchain, and it builds for the host architecture, so an Intel Mac
gives you an amd64 bundle and Apple silicon an arm64 one.
Under the hood that target runs fyne package, which reads its metadata from
cmd/haul/FyneApp.toml, converts cmd/haul/Icon.png into the bundle's
.icns icon, and writes the Info.plist. Without make:
go run fyne.io/tools/cmd/fyne@v1.7.2 package --target darwin --src ./cmd/haul --release
Note that fyne package rewrites cmd/haul/FyneApp.toml in place, bumping
Build on every run and dropping any comments you add to it. That is by design;
commit the bump or discard it as you prefer.
The bundle is unsigned, so Gatekeeper blocks the first launch on any Mac that did not build it. Right-click the app and choose Open (once — after that it starts normally), or clear the quarantine flag:
xattr -dr com.apple.quarantine /Applications/haul.app
Signing and notarising properly needs a paid Apple Developer ID, after which
fyne release --target darwin --certificate <name> handles it.
Packaging on Windows
The go build command above produces a working
haul.exe, but one with no icon — Explorer, the taskbar and the Start menu
all draw the generic executable glyph. A Windows binary carries its icon as a
linked resource, and the Go compiler has nothing to link unless you give it a
.syso. fyne package builds one:
go run fyne.io/tools/cmd/fyne@v1.7.2 package --target windows --src .\cmd\haul --release
Move-Item .\cmd\haul\haul.exe .\bin\haul.exe
Or, from the MSYS2 shell with make installed, make package-windows.
That converts cmd/haul/Icon.png to an icon resource, links it in, and adds
-H=windowsgui itself — so unlike the manual build you don't pass any linker
flags. It writes the binary beside the source as cmd\haul\haul.exe, hence
the move: Inno-compile.iss expects it in bin\. As on macOS, fyne package
rewrites cmd/haul/FyneApp.toml in place and bumps Build.
The installer
Inno-compile.iss wraps that binary in a setup program. Install Inno
Setup (winget install JRSoftware.InnoSetup),
then from the repo root:
iscc Inno-compile.iss
That writes Output\haul-0.1.0-setup-x64.exe. Every Source: path in the
script is relative to the script itself, so it compiles from any clone — but it
packages whatever is in bin\ at that moment, and does not build anything
itself. Run the packaging step above first.
It installs per-user into %LOCALAPPDATA%\Programs\Haul, so it needs no
administrator rights and prompts for no UAC elevation.
The wizard offers two install types. Standard installs haul.exe alone.
Remote Desktop / virtual machine adds every DLL from bin\ as the optional
mesa component — the software OpenGL renderer described
below. That split is deliberate and
worth keeping: those DLLs override the system OpenGL for anything in the same
folder, so installing them by default would force software rendering on machines
with a perfectly good GPU. Populate bin\ with the Mesa DLLs only when you
intend to build an installer that offers them.
Note that cmd\haul\Icon.ico is used by Inno for setup.exe's own icon. The
icon inside haul.exe comes from Icon.png via fyne package — the two are
separate, and dropping the packaging step leaves you with a nicely-iconed
installer that installs an icon-less application.
Running over Remote Desktop (or in a VM)
A build that compiles perfectly will still fail to start in an RDP session with:
Fyne error: window creation error
Cause: APIUnavailable: WGL: The driver does not appear to support OpenGL
This is not a haul fault — any OpenGL application fails the same way. Fyne needs OpenGL 2.1+, and Remote Desktop's display driver doesn't provide it. The same applies to Hyper-V and to VMs without 3D acceleration, and to any machine using Microsoft Basic Display Adapter.
The fix is Mesa's software OpenGL renderer (llvmpipe), deployed just for haul:
- Download a
mesa3d-<version>-release-msvc.7zfrom mesa-dist-win releases. It's a.7z, so you'll need 7-Zip (winget install 7zip.7zip) to extract it. - Open the extracted
x64folder (notx86). - Copy every DLL in it — around 19 files — into the same folder as
haul.exe.
bin\
haul.exe
opengl32.dll
libgallium_wgl.dll
...and the rest of x64\
Verified working in an RDP session on 2026-07-30.
Three things worth knowing:
- Copy all of the DLLs, not just
opengl32.dll. In current Mesa builds that file is a thin front-end that loadslibgallium_wgl.dlland others alongside it. Taking it on its own produces a DLL that can't load its dependencies, and haul fails exactly as if you'd done nothing. - A local
opengl32.dllalways wins over the system one, because Windows searches the executable's own directory first. So these DLLs force software rendering unconditionally — including on a machine with a perfectly good GPU. If you use haul both locally and over RDP, keep two copies: one folder with the Mesa DLLs, one without. - Don't run
systemwidedeploy.cmdif you see it in the archive. That installs Mesa for every OpenGL application on the machine; copying files besidehaul.exekeeps the change contained to haul.
Software rendering is slower than a GPU, but haul draws two tables and some text
— it's entirely adequate. The headless Linux target does the same thing via
LIBGL_ALWAYS_SOFTWARE=1 in make run-xvfb.
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 package-darwin |
macOS .app bundle into ./haul.app (run on a Mac) |
make package-windows |
bin/haul.exe with an embedded icon (run on Windows) |
make clean |
Remove bin/, dist/, haul.app/ and Output/ |
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 — and on macOS the
binary alone is not the artefact you want to hand out, see Packaging on
macOS. 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.