Files
haul/README.md
T
bsncubed b533d35f95 docs: document the OpenGL requirement and the RDP workaround
haul compiles cleanly on Windows and then fails at startup inside an RDP
session, because Remote Desktop's display driver provides no usable OpenGL and
Fyne needs 2.1+. Same for Hyper-V, VMs without 3D, and Microsoft Basic Display
Adapter.

Documents deploying Mesa's llvmpipe renderer beside haul.exe, and two things
that are easy to get wrong: all ~19 DLLs from x64 are needed rather than just
opengl32.dll, since that file is now only a front-end for the others; and a
local opengl32.dll shadows the system one, so it forces software rendering even
on machines with a working GPU.

Also adds OpenGL to the runtime requirements table, which only mentioned
needing a display server.

Verified in an RDP session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 21:58:02 +10:00

389 lines
15 KiB
Markdown

# 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](https://fyne.io), 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` (`Include` directives followed), and connecting resolves the
effective config through `ssh -G`, so `ProxyJump` chains, `IdentityFile`,
`User`, and `Port` behave exactly as the real `ssh` client 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`](https://gitea.apointless.space/bsncubed/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](#running-over-remote-desktop-or-in-a-vm) |
| An SFTP-capable server | Transfers use the SFTP subsystem — see [Scope](#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:
```sh
# 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](https://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:
```sh
# 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](#building-on-windows).
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
```sh
make build # -> bin/haul
```
That's the whole thing. Run it with `./bin/haul`, or install it onto your
`PATH`:
```sh
make install # -> $GOBIN, or $GOPATH/bin (usually ~/go/bin)
```
Make sure that directory is on `PATH`:
```sh
echo 'export PATH="$(go env GOPATH)/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
```
Or just copy `bin/haul` wherever you like:
```sh
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:
```sh
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
```powershell
winget install GoLang.Go
```
or the `.msi` from [go.dev/dl](https://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](https://www.msys2.org/) is the usual
source:
```powershell
winget install MSYS2.MSYS2
```
Then open the **"MSYS2 UCRT64"** shell from the Start menu and install GCC:
```sh
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:
```powershell
[Environment]::SetEnvironmentVariable("Path",
[Environment]::GetEnvironmentVariable("Path","User") + ";C:\msys64\ucrt64\bin",
"User")
```
Open a new terminal and confirm both are visible:
```powershell
go version
gcc --version
```
**3. Build.** Windows has no `make`, so invoke the compiler directly from
PowerShell in the repo root:
```powershell
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=windowsgui`** marks 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 have `make`.
- **`-o bin\haul.exe`** names the output explicitly. Windows needs the `.exe`
extension 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.
### 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:
1. Download a `mesa3d-<version>-release-msvc.7z` from
[mesa-dist-win releases](https://github.com/pal1000/mesa-dist-win/releases).
It's a `.7z`, so you'll need 7-Zip (`winget install 7zip.7zip`) to extract it.
2. Open the extracted **`x64`** folder (not `x86`).
3. 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 loads `libgallium_wgl.dll` and 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.dll` always 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.cmd`** if you see it in the archive. That
installs Mesa for every OpenGL application on the machine; copying files beside
`haul.exe` keeps 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 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](https://github.com/tpoechtrager/osxcross) |
For that reason `make dist` builds only the native binary. The practical route
to the others is [`fyne-cross`](https://github.com/fyne-io/fyne-cross), which
does each build inside a Docker image carrying the right toolchain:
```sh
go install github.com/fyne-io/fyne-cross@latest
fyne-cross windows -arch=amd64
```
## Usage
```sh
haul
```
### Connecting
The connect window has two tabs:
- **Saved** — every `Host` block from `~/.ssh/config`, with a search box to
filter them. Connecting resolves the alias through `ssh -G`, so it honours
everything your config says about that host, `ProxyJump` chains 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.