// 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) }