Files
haul/internal/ui/ui.go
T
bsncubed 6b52e83bdc Initial commit: haul — desktop SFTP client
A two-pane local/remote file browser over SSH, split out of dial so file
transfer lives in its own tool. The two share ~/.ssh/config as their base.

- internal/sshconf: ~/.ssh/config parsing, for the saved-host list.
- internal/sshx: dialling and auth — agent, keys, password, host-key
  verification, ProxyJump.
- internal/vfs: the FS interface both sides are driven through, implemented
  over the local disk and over SFTP.
- internal/xfer: recursive copy engine with progress reporting, cancellation,
  conflict policy, and resume of partial transfers.
- internal/ui: the Fyne GUI — connect window (saved hosts plus quick connect)
  and the two-pane browser session.

Build with `make build`; the Makefile locates the Go toolchain at ~/.local/go,
which is deliberately kept off PATH. Cross-compiling needs a per-target C
toolchain because Fyne is cgo, so `make dist` builds the native binary only —
the alternatives are documented in the Makefile.

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

98 lines
3.2 KiB
Go

// Package ui is haul's desktop GUI: a connection window backed by ~/.ssh/config
// (plus ad-hoc quick connect), and a two-pane local/remote file browser per
// connected host.
//
// The rule the whole package follows: Fyne widgets are only ever touched from
// the UI thread. Every SFTP call — listing a directory, transferring, deleting
// — runs on its own goroutine and comes back through fyne.Do. A GUI that blocks
// its event loop on a network round-trip is exactly the thing that makes an
// SFTP client feel slow, and remote round-trips are the common case here.
package ui
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/theme"
)
// Run starts the GUI and blocks until the user quits.
func Run() error {
a := app.NewWithID("space.apointless.haul")
a.Settings().SetTheme(&haulTheme{base: theme.DefaultTheme()})
newConnectWindow(a).showAndRun()
return nil
}
// haulTheme keeps the high-contrast, low-light-friendly look haul had as a TUI:
// a near-black background, bright text, and a clearly visible selection. It also
// tightens padding, because a file manager lives or dies on how many rows fit on
// screen.
type haulTheme struct{ base fyne.Theme }
var (
bgDark = color.NRGBA{R: 0x11, G: 0x13, B: 0x18, A: 0xff}
panelDark = color.NRGBA{R: 0x1a, G: 0x1d, B: 0x24, A: 0xff}
inputDark = color.NRGBA{R: 0x0b, G: 0x0d, B: 0x11, A: 0xff}
fgDark = color.NRGBA{R: 0xf0, G: 0xf3, B: 0xf8, A: 0xff}
mutedDark = color.NRGBA{R: 0x8b, G: 0x93, B: 0xa1, A: 0xff}
separatorDark = color.NRGBA{R: 0x2c, G: 0x31, B: 0x3a, A: 0xff}
accent = color.NRGBA{R: 0x4c, G: 0x9a, B: 0xff, A: 0xff}
selectionDark = color.NRGBA{R: 0x1e, G: 0x3f, B: 0x66, A: 0xff}
hoverDark = color.NRGBA{R: 0x25, G: 0x2a, B: 0x33, A: 0xff}
)
func (t *haulTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
if v == theme.VariantLight {
// In light mode only the accent is overridden; the stock light palette
// already has the contrast this theme is trying to buy back in the dark.
if n == theme.ColorNamePrimary {
return accent
}
return t.base.Color(n, v)
}
switch n {
case theme.ColorNameBackground:
return bgDark
case theme.ColorNameForeground:
return fgDark
case theme.ColorNameButton, theme.ColorNameMenuBackground, theme.ColorNameOverlayBackground:
return panelDark
case theme.ColorNameHeaderBackground:
return panelDark
case theme.ColorNameInputBackground:
return inputDark
case theme.ColorNamePrimary:
return accent
case theme.ColorNameSelection:
return selectionDark
case theme.ColorNameHover:
return hoverDark
case theme.ColorNameSeparator:
return separatorDark
case theme.ColorNamePlaceHolder, theme.ColorNameDisabled:
return mutedDark
case theme.ColorNameScrollBar:
return separatorDark
}
return t.base.Color(n, v)
}
func (t *haulTheme) Font(s fyne.TextStyle) fyne.Resource { return t.base.Font(s) }
func (t *haulTheme) Icon(n fyne.ThemeIconName) fyne.Resource { return t.base.Icon(n) }
func (t *haulTheme) Size(n fyne.ThemeSizeName) float32 {
switch n {
case theme.SizeNamePadding:
return 3 // stock is 4; buys roughly one extra row per screenful
case theme.SizeNameInnerPadding:
return 6
case theme.SizeNameSeparatorThickness:
return 1
}
return t.base.Size(n)
}