From 87f82e2c4747105cb09840af587bba4136cb261b Mon Sep 17 00:00:00 2001 From: TZGyn Date: Fri, 30 Oct 2026 22:21:58 +0800 Subject: [PATCH] Add the Fyne desktop app (M5) A cross-platform desktop UI over the same client service: status, routes with add/remove/copy/open, live logs, settings, official-profile import, and start/stop. It uses the existing JSON config and offers to stop a running CLI daemon so the two do not fight over routes. The app can autostart the service, refreshes status/config every few seconds, and builds as a macOS .app via scripts/build-desktop-app.sh. --- .gitignore | 5 + AGENTS.md | 6 +- README.md | 16 ++- assets/icon.png | Bin 0 -> 54354 bytes cmd/tunnel-desktop/main.go | 6 +- internal/desktop/app.go | 159 +++++++++++++++++----------- internal/desktop/process_unix.go | 17 +++ internal/desktop/process_windows.go | 15 +++ scripts/build-desktop-app.sh | 31 ++++++ 9 files changed, 192 insertions(+), 63 deletions(-) create mode 100644 assets/icon.png create mode 100644 internal/desktop/process_unix.go create mode 100644 internal/desktop/process_windows.go create mode 100755 scripts/build-desktop-app.sh diff --git a/.gitignore b/.gitignore index 54c4961..19a03f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ /tunneld +/tunnel +/tunnel-desktop +/Tunnel.app cmd/tunneld/tunneld +cmd/tunnel/tunnel +cmd/tunnel-desktop/tunnel-desktop .local/ .DS_Store web/node_modules/ diff --git a/AGENTS.md b/AGENTS.md index 3a494bb..531fcd4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,7 +75,11 @@ what we take vs. skip). Never vendor it into the build. - Desktop app: `go build -o .local/tunnel-desktop ./cmd/tunnel-desktop` (Fyne; needs CGO). It runs the same `internal/client` service in-process and uses `~/.config/tunnel/config.json`. If the CLI service is already running, stop - it first — the app shows a banner and offers to stop it. + it first — the app shows a banner and offers to stop it. `--autostart` + connects on launch. On macOS, `scripts/build-desktop-app.sh ` builds + and ad-hoc signs `Tunnel.app`; the installed copy is + `~/Applications/Tunnel.app`. Cross-OS builds need that OS's CGO toolchain; + `CGO_ENABLED=0` does not work for Fyne. - Dev loop: `go build -o .local/tunneld ./cmd/tunneld && .local/tunneld -domain localhost`, then `OPENTUNNEL_API=http://localhost:8080 opentunnel route add 4096 --name api` with the official CLI. `go build` drops a binary next to the source — it is diff --git a/README.md b/README.md index 736c180..d2631b1 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,16 @@ client service: ```bash go build -o .local/tunnel-desktop ./cmd/tunnel-desktop -.local/tunnel-desktop +.local/tunnel-desktop # click Start +.local/tunnel-desktop --autostart +``` + +On macOS, build and ad-hoc sign a launchable bundle: + +```bash +./scripts/build-desktop-app.sh 0.2.0 +ditto Tunnel.app "$HOME/Applications/Tunnel.app" +open "$HOME/Applications/Tunnel.app" --args --autostart ``` It uses `~/.config/tunnel/config.json` like the CLI, shows tunnel status and @@ -107,6 +116,11 @@ and profile import, and has a start/stop button that runs the service in-process. If the CLI service is already running, a banner offers to stop it first. +Fyne needs CGO: the macOS build is tested; Linux and Windows must be built on +those platforms (or with a native CGO cross toolchain), not with +`CGO_ENABLED=0`. Distribution of the macOS app still needs Developer ID signing +and notarization (the local script only ad-hoc signs). + ## Configuration Every flag also reads an environment variable (`tunneld -h`): diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ea7bdffb46236caab7344934362f07209564a30c GIT binary patch literal 54354 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 0 && processAlive(pid) { - _ = syscall.Kill(pid, syscall.SIGTERM) + terminateProcess(pid) } a.banner.Hide() }) - a.banner = container.NewHBox(bannerLabel, stopCLI) + a.banner = container.NewBorder(nil, nil, nil, stopCLI, bannerLabel) a.banner.Hide() routeHeader := container.NewHBox( @@ -157,42 +169,11 @@ func (a *App) buildUI() { layout.NewSpacer(), widget.NewButtonWithIcon("Add route", theme.ContentAddIcon(), a.addRouteDialog), ) - a.routeList = widget.NewList( - func() int { return len(a.routeNames()) }, - func() fyne.CanvasObject { - return container.NewHBox( - widget.NewLabel("name"), - widget.NewLabel("target"), - widget.NewHyperlink("url", nil), - layout.NewSpacer(), - widget.NewButtonWithIcon("", theme.ContentCopyIcon(), nil), - widget.NewButtonWithIcon("", theme.DeleteIcon(), nil), - ) - }, - func(i widget.ListItemID, item fyne.CanvasObject) { - names := a.routeNames() - name := names[i] - route := a.cfg.Routes[name] - linkURL := a.cfg.RouteURL(name) - box := item.(*fyne.Container) - box.Objects[0].(*widget.Label).SetText(name) - box.Objects[1].(*widget.Label).SetText(route.Target) - link := box.Objects[2].(*widget.Hyperlink) - link.SetText(linkURL) - if parsed, err := url.Parse(linkURL); err == nil { - link.SetURL(parsed) - } - box.Objects[4].(*widget.Button).OnTapped = func() { a.app.Clipboard().SetContent(linkURL) } - box.Objects[5].(*widget.Button).OnTapped = func() { a.removeRoute(name) } - }, - ) - - a.logList = widget.NewList( - func() int { return a.logWriter.count() }, - func() fyne.CanvasObject { return widget.NewLabel("") }, - func(i widget.ListItemID, item fyne.CanvasObject) { item.(*widget.Label).SetText(a.logWriter.line(i)) }, - ) - a.logWriter.onAppend = func() { fyne.Do(a.logList.Refresh) } + a.routeBox = container.NewVBox() + a.logText = widget.NewTextGrid() + logScroll := container.NewScroll(a.logText) + logScroll.SetMinSize(fyne.NewSize(0, 220)) + a.logWriter.onAppend = func() { fyne.Do(a.refreshLog) } content := container.NewVBox( container.NewHBox( @@ -208,9 +189,9 @@ func (a *App) buildUI() { statusCard, a.banner, routeHeader, - a.routeList, + a.routeBox, widget.NewLabelWithStyle("Log", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), - a.logList, + logScroll, ) a.window.SetContent(container.NewPadded(container.NewScroll(content))) } @@ -235,20 +216,64 @@ func (a *App) refresh() { return } a.cfg = cfg - a.svc.SetConfig(cfg) + if a.svc != nil { + a.svc.SetConfig(cfg) + } a.apiLabel.SetText(valueOr(cfg.API, "(not set)")) a.tunnelLabel.SetText(valueOr(cfg.Hostname, "(no tunnel yet)")) - a.certLabel.SetText(valueOr(cfg.CertificateExpiry, "(no certificate)")) + a.certLabel.SetText(formatCert(cfg.CertificateExpiry)) if pid := readCLIpid(a.paths.PID); pid > 0 && processAlive(pid) { a.banner.Show() } else { a.banner.Hide() } - a.routeList.Refresh() + a.refreshRoutes() a.updateStatus() } +func (a *App) refreshRoutes() { + a.routeBox.Objects = nil + for _, name := range a.routeNames() { + route := a.cfg.Routes[name] + linkURL := a.cfg.RouteURL(name) + link := widget.NewHyperlink(linkURL, nil) + if parsed, err := url.Parse(linkURL); err == nil { + link.SetURL(parsed) + } + copyButton := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { + a.app.Clipboard().SetContent(linkURL) + }) + deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() { + a.removeRoute(name) + }) + row := container.NewHBox( + container.NewGridWrap(fyne.NewSize(130, 32), widget.NewLabel(name)), + container.NewGridWrap(fyne.NewSize(170, 32), widget.NewLabel(route.Target)), + link, + layout.NewSpacer(), + copyButton, + deleteButton, + ) + a.routeBox.Add(row) + } + a.routeBox.Refresh() +} + +func (a *App) refreshLog() { + a.logText.SetText(a.logWriter.text(300)) +} + +func formatCert(value string) string { + if value == "" { + return "(no certificate)" + } + if parsed, err := time.Parse(time.RFC3339, value); err == nil { + return parsed.Local().Format("2006-01-02 15:04 MST") + } + return value +} + func (a *App) updateStatus() { status := a.svc.CurrentStatus() running := a.isRunning() @@ -280,7 +305,8 @@ func (a *App) updateStatus() { a.lastError.Hide() } a.dot.Refresh() - a.routeList.Refresh() + a.startButton.Enable() + a.refreshRoutes() } func (a *App) isRunning() bool { @@ -312,7 +338,6 @@ func (a *App) start() { svc := a.svc a.runMu.Unlock() - a.startButton.Disable() a.stateLabel.SetText("Provisioning…") a.dot.Refresh() go func() { @@ -476,9 +501,13 @@ func (a *App) readExistingLog() { if err != nil { return } + onAppend := a.logWriter.onAppend + a.logWriter.onAppend = nil for _, line := range strings.Split(strings.TrimRight(string(data), "\n"), "\n") { - a.logWriter.append(line) + a.logWriter.remember(line) } + a.logWriter.onAppend = onAppend + a.refreshLog() } func randomRouteName() string { @@ -502,11 +531,6 @@ func readCLIpid(path string) int { return pid } -func processAlive(pid int) bool { - err := syscall.Kill(pid, 0) - return err == nil || errors.Is(err, syscall.EPERM) -} - // uiLogWriter mirrors service logs into a bounded in-memory list and a file. type uiLogWriter struct { mu sync.Mutex @@ -532,6 +556,14 @@ func (w *uiLogWriter) Write(p []byte) (int, error) { } func (w *uiLogWriter) append(line string) { + w.remember(line) + if w.file != nil { + _, _ = io.WriteString(w.file, line+"\n") + } +} + +// remember adds a line to the in-memory list without writing it to the file. +func (w *uiLogWriter) remember(line string) { if line == "" { return } @@ -542,9 +574,6 @@ func (w *uiLogWriter) append(line string) { } onAppend := w.onAppend w.mu.Unlock() - if w.file != nil { - _, _ = io.WriteString(w.file, line+"\n") - } if onAppend != nil { onAppend() } @@ -565,6 +594,16 @@ func (w *uiLogWriter) line(i int) string { return w.lines[i] } +func (w *uiLogWriter) text(limit int) string { + w.mu.Lock() + defer w.mu.Unlock() + lines := w.lines + if len(lines) > limit { + lines = lines[len(lines)-limit:] + } + return strings.Join(lines, "\n") +} + func valueOr(value, fallback string) string { if value == "" { return fallback diff --git a/internal/desktop/process_unix.go b/internal/desktop/process_unix.go new file mode 100644 index 0000000..b19c140 --- /dev/null +++ b/internal/desktop/process_unix.go @@ -0,0 +1,17 @@ +//go:build !windows + +package desktop + +import ( + "errors" + "syscall" +) + +func processAlive(pid int) bool { + err := syscall.Kill(pid, 0) + return err == nil || errors.Is(err, syscall.EPERM) +} + +func terminateProcess(pid int) { + _ = syscall.Kill(pid, syscall.SIGTERM) +} diff --git a/internal/desktop/process_windows.go b/internal/desktop/process_windows.go new file mode 100644 index 0000000..91c2dbf --- /dev/null +++ b/internal/desktop/process_windows.go @@ -0,0 +1,15 @@ +//go:build windows + +package desktop + +import "os" + +// The CLI background daemon is not supported on Windows; the desktop app +// runs the service in-process, so there is no CLI pid to show or stop. +func processAlive(int) bool { return false } + +func terminateProcess(pid int) { + if process, err := os.FindProcess(pid); err == nil { + _ = process.Kill() + } +} diff --git a/scripts/build-desktop-app.sh b/scripts/build-desktop-app.sh new file mode 100755 index 0000000..2ca47e7 --- /dev/null +++ b/scripts/build-desktop-app.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Build and ad-hoc sign a local macOS Tunnel.app. +# Distribution releases still need Developer ID signing + notarization. +set -euo pipefail +cd "$(dirname "$0")/.." + +VERSION="${1:-0.2.0}" +BUILD="${BUILD:-1}" +FYNE="$(command -v fyne || true)" +if [ -z "$FYNE" ]; then + FYNE="$(go env GOPATH)/bin/fyne" +fi +if [ ! -x "$FYNE" ]; then + echo "installing fyne CLI..." + go install fyne.io/tools/cmd/fyne@latest + FYNE="$(go env GOPATH)/bin/fyne" +fi + +mkdir -p .local +go build -o .local/Tunnel -ldflags "-X main.version=$VERSION" ./cmd/tunnel-desktop +rm -rf Tunnel.app +"$FYNE" package -os darwin \ + -executable .local/Tunnel \ + -name Tunnel -app-id dev.tzgyn.tunnel \ + -app-version "$VERSION" -app-build "$BUILD" \ + -icon assets/icon.png -release + +codesign --force --deep --sign - Tunnel.app +echo "Built and ad-hoc signed Tunnel.app (version $VERSION)." +echo "Install locally with:" +echo " rm -rf \"\$HOME/Applications/Tunnel.app\" && ditto Tunnel.app \"\$HOME/Applications/Tunnel.app\""