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>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user