Files
haul/README.md
T
bsncubed 47ad62bb84 docs: add a proper Windows build section
Windows previously got one line pointing vaguely at MSYS2, while every other
platform got real commands — and the README's main instruction is `make build`,
which stock Windows cannot run at all.

Covers the three ways Windows actually differs: cgo needs a GCC-style compiler
(MSVC will not do), there is no make, and a GUI build needs -H=windowsgui or
Windows opens a console window behind the app on every launch. The Makefile
does not pass that flag since it targets Unix, so the section gives the direct
go build command and names the .exe output explicitly.

Also notes that Windows, unlike Linux, needs no graphics dev packages because
OpenGL ships with the OS.

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

335 lines
12 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 |
| 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: OpenGL is part of
the operating system.
### 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.