47ad62bb84
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>
335 lines
12 KiB
Markdown
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.
|