diff --git a/README.md b/README.md new file mode 100644 index 0000000..169cac0 --- /dev/null +++ b/README.md @@ -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 ` 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.