docs: add README
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>
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
# 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
|
||||
|
||||
**Go 1.24 or newer** — check with `go version`.
|
||||
|
||||
Then the C toolchain and system headers Fyne needs:
|
||||
|
||||
```sh
|
||||
# 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](https://www.msys2.org/)) so cgo has a C compiler.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### 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.
|
||||
Reference in New Issue
Block a user