From 3bf8a195a8c38424cadbd560ae1b863146646754 Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Fri, 21 Aug 2026 10:46:42 +0200 Subject: [PATCH 1/3] feat(vizd): survive console-close under the Windows tray launcher Add a Windows-only SetConsoleCtrlHandler that returns TRUE on CTRL_CLOSE_EVENT so the tray launcher can hide the console window instead of vizd being terminated when the user closes it. CTRL_C_EVENT still falls through to the appbase SIGINT handler for a clean shutdown. --- programs/vizd/main.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/programs/vizd/main.cpp b/programs/vizd/main.cpp index 7187bbf77c..396f04dcb2 100644 --- a/programs/vizd/main.cpp +++ b/programs/vizd/main.cpp @@ -31,6 +31,21 @@ #include +#ifdef _WIN32 +#include + +/// Tray-launcher support: the launcher owns the console window lifecycle. +/// Surviving CTRL_CLOSE_EVENT lets the launcher hide the window when it is +/// closed instead of the process being terminated. All other events +/// (CTRL_C_EVENT in particular) fall through so the appbase SIGINT handler +/// performs a clean shutdown. +static BOOL WINAPI vizd_tray_console_ctrl_handler(DWORD ctrl_type) { + if (ctrl_type == CTRL_CLOSE_EVENT) + return TRUE; + return FALSE; +} +#endif + using graphene::protocol::version; @@ -121,6 +136,12 @@ int main( int argc, char** argv ) { wlog("Error parsing logging config"); } +#ifdef _WIN32 + // Let the tray launcher manage the console window: closing it (X) + // must hide vizd, not stop it. See vizd_tray_console_ctrl_handler. + SetConsoleCtrlHandler(vizd_tray_console_ctrl_handler, TRUE); +#endif + appbase::app().startup(); appbase::app().exec(); ilog("exited cleanly"); From 88994a7c684dd1570868b36e2ec7201cb50f1f1f Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Fri, 21 Aug 2026 10:53:38 +0200 Subject: [PATCH 2/3] feat(windows): add vizd-tray system-tray launcher A tiny static Go launcher (GUI subsystem) that runs vizd.exe sharing its console so vizd keeps its colored log output, and adds a tray icon with Open window / Close vizd / Enable autostart / Quit. Closing the console hides it to the tray instead of stopping the node. --- contrib/windows/vizd-tray/README.md | 31 ++++ contrib/windows/vizd-tray/go.mod | 10 ++ contrib/windows/vizd-tray/go.sum | 6 + contrib/windows/vizd-tray/icon.ico | Bin 0 -> 4286 bytes contrib/windows/vizd-tray/main.go | 260 ++++++++++++++++++++++++++++ 5 files changed, 307 insertions(+) create mode 100644 contrib/windows/vizd-tray/README.md create mode 100644 contrib/windows/vizd-tray/go.mod create mode 100644 contrib/windows/vizd-tray/go.sum create mode 100644 contrib/windows/vizd-tray/icon.ico create mode 100644 contrib/windows/vizd-tray/main.go diff --git a/contrib/windows/vizd-tray/README.md b/contrib/windows/vizd-tray/README.md new file mode 100644 index 0000000000..54da220602 --- /dev/null +++ b/contrib/windows/vizd-tray/README.md @@ -0,0 +1,31 @@ +# vizd-tray + +Tiny system-tray launcher for the Windows `vizd.exe` node (Go, static, GUI subsystem). + +## What it does + +- Runs `vizd.exe -d data` sharing this process's console, so vizd's colored log + output (Windows `SetConsoleTextAttribute`) keeps working. +- Shows a tray icon with a context menu: + - **Open window** — bring the node console back. + - **Close vizd** — stop the node cleanly (Ctrl+C → appbase SIGINT), then exit. + - **Enable autostart** — toggle a `HKCU\...\Run` entry (checked when enabled). + - **Quit** — same as Close vizd. +- Closing the console window (X) hides it to the tray instead of stopping vizd — + see the `SetConsoleCtrlHandler(CTRL_CLOSE_EVENT)` handler in `programs/vizd/main.cpp`. + +## Build + +```sh +cd contrib/windows/vizd-tray +CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath \ + -ldflags "-s -w -H windowsgui" -o vizd-tray.exe . +``` + +The release workflow (`windows-release.yml`) builds it with the same flags and +ships `vizd-tray.exe` next to `vizd.exe`. + +## Layout + +- `main.go` — tray, console management, vizd lifecycle, autostart. +- `icon.ico` — placeholder tray icon (replace with a proper VIZ icon). diff --git a/contrib/windows/vizd-tray/go.mod b/contrib/windows/vizd-tray/go.mod new file mode 100644 index 0000000000..f8abe3dd7f --- /dev/null +++ b/contrib/windows/vizd-tray/go.mod @@ -0,0 +1,10 @@ +module vizd-tray + +go 1.22 + +require ( + fyne.io/systray v1.12.2 + golang.org/x/sys v0.15.0 +) + +require github.com/godbus/dbus/v5 v5.1.0 // indirect diff --git a/contrib/windows/vizd-tray/go.sum b/contrib/windows/vizd-tray/go.sum new file mode 100644 index 0000000000..57d4478021 --- /dev/null +++ b/contrib/windows/vizd-tray/go.sum @@ -0,0 +1,6 @@ +fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA= +fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/contrib/windows/vizd-tray/icon.ico b/contrib/windows/vizd-tray/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cd019ab3897605beb0ec33dcd8e82f77f4160c14 GIT binary patch literal 4286 zcmeH@OAf*?5JQI>uwvaM$KV{CkVA2ol7J#PZa%gUqKgKFCNrMr$wZveZ&@P$$KxDv ziHK|3DHOM~bNszh?pWVVV4bY*=>6)6?IztPd6^B^40vv}x@Yyg$J*LGeNEMF%xd|K zVqCAeepFmL@)_lrqx8TvBfgSj-mC}ID||(R%#lCp6t1H|cCQE2IBZ3W?3O>~^I2g! z8s>8kn0L627CjFCdIC5bXQqx8J!OpyXnF(t1*_m-m%e>+<`f^Q+tHhWy^5h)XDt@Tpdgd#4W|TYn;JM`at9bP^ X%yRENt{eZCFZVg?gZ0OK-xGKN$8qi@ literal 0 HcmV?d00001 diff --git a/contrib/windows/vizd-tray/main.go b/contrib/windows/vizd-tray/main.go new file mode 100644 index 0000000000..a4398cea30 --- /dev/null +++ b/contrib/windows/vizd-tray/main.go @@ -0,0 +1,260 @@ +//go:build windows + +// vizd-tray is a tiny system-tray launcher for the Windows vizd node. It runs +// vizd.exe sharing this process's console (so vizd's colored log output keeps +// working), shows a tray icon, and: +// - hides the console window to the tray when it is closed (X) instead of +// stopping the node; +// - "Open window" brings the console back; +// - "Close vizd" sends Ctrl+C so vizd shuts down cleanly (appbase SIGINT), +// then the launcher exits; +// - "Enable autostart" (checkbox) writes/removes a HKCU Run entry. +package main + +import ( + _ "embed" + "fmt" + "os" + "os/exec" + "os/signal" + "path/filepath" + "syscall" + "time" + "unsafe" + + "fyne.io/systray" + "golang.org/x/sys/windows/registry" +) + +//go:embed icon.ico +var trayIcon []byte + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + user32 = syscall.NewLazyDLL("user32.dll") + + procAllocConsole = kernel32.NewProc("AllocConsole") + procGetConsoleWindow = kernel32.NewProc("GetConsoleWindow") + procSetConsoleCtrlHandler = kernel32.NewProc("SetConsoleCtrlHandler") + procGenerateConsoleCtrlEvent = kernel32.NewProc("GenerateConsoleCtrlEvent") + + procShowWindow = user32.NewProc("ShowWindow") + procSetForegroundWindow = user32.NewProc("SetForegroundWindow") + procMessageBox = user32.NewProc("MessageBoxW") +) + +const ( + ctrlCEvent = 0 + ctrlBreakEvent = 1 + ctrlCloseEvent = 2 + + swHide = 0 + swRestore = 9 + + runKeyPath = `Software\Microsoft\Windows\CurrentVersion\Run` + runValue = `vizd-tray` + + stopGrace = 10 * time.Second +) + +// --- console window management ------------------------------------------- + +func allocConsole() { procAllocConsole.Call() } +func consoleWindow() uintptr { + hwnd, _, _ := procGetConsoleWindow.Call() + return hwnd +} +func showConsole() { + if hwnd := consoleWindow(); hwnd != 0 { + procShowWindow.Call(hwnd, swRestore) + procSetForegroundWindow.Call(hwnd) + } +} +func hideConsole() { + if hwnd := consoleWindow(); hwnd != 0 { + procShowWindow.Call(hwnd, swHide) + } +} + +// consoleCtrlHandler is installed via SetConsoleCtrlHandler. The launcher owns +// the console window: closing it (X) hides it to the tray instead of exiting, +// and the launcher survives the Ctrl+C it sends to stop vizd (vizd receives its +// own copy and shuts down cleanly). +func consoleCtrlHandler(ctrlType uintptr) uintptr { + switch ctrlType { + case ctrlCloseEvent: + hideConsole() + return 1 // handled + case ctrlCEvent: + return 1 // swallow so the launcher survives + } + return 0 +} + +func installCtrlHandler() { + cb := syscall.NewCallback(consoleCtrlHandler) + procSetConsoleCtrlHandler.Call(cb, 1) +} + +// --- vizd lifecycle ------------------------------------------------------- + +func bundleDir() (string, error) { + self, err := os.Executable() + if err != nil { + return "", err + } + return filepath.Dir(self), nil +} + +func spawnVizd() (*exec.Cmd, error) { + dir, err := bundleDir() + if err != nil { + return nil, err + } + vizdExe := filepath.Join(dir, "vizd.exe") + if _, err := os.Stat(vizdExe); err != nil { + return nil, fmt.Errorf("vizd.exe not found next to the launcher: %v", err) + } + cmd := exec.Command(vizdExe, "-d", "data") + cmd.Dir = dir + // No CREATE_NEW_CONSOLE / CREATE_NO_WINDOW: vizd inherits this process's + // console, so its SetConsoleTextAttribute coloring works and Ctrl+C can be + // delivered to the shared console group. + if err := cmd.Start(); err != nil { + return nil, err + } + return cmd, nil +} + +func stopVizd(cmd *exec.Cmd) { + if cmd == nil || cmd.Process == nil { + return + } + // Ctrl+C to the whole shared console group (launcher + vizd). + procGenerateConsoleCtrlEvent.Call(ctrlCEvent, 0) + + done := make(chan struct{}) + go func() { + _, _ = cmd.Process.Wait() + close(done) + }() + select { + case <-done: + case <-time.After(stopGrace): + _ = cmd.Process.Kill() + } +} + +// --- autostart (HKCU Run) ------------------------------------------------- + +func autostartEnabled() bool { + k, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.QUERY_VALUE) + if err != nil { + return false + } + defer k.Close() + _, _, err = k.GetStringValue(runValue) + return err == nil +} + +func setAutostart(enabled bool) error { + exe, err := os.Executable() + if err != nil { + return err + } + if enabled { + k, _, err := registry.CreateKey(registry.CURRENT_USER, runKeyPath, registry.SET_VALUE) + if err != nil { + return err + } + defer k.Close() + return k.SetStringValue(runValue, fmt.Sprintf(`"%s"`, exe)) + } + k, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.SET_VALUE) + if err != nil { + return nil // key absent: nothing to remove + } + defer k.Close() + _ = k.DeleteValue(runValue) + return nil +} + +func errBox(title, text string) { + t, _ := syscall.UTF16PtrFromString(text) + c, _ := syscall.UTF16PtrFromString(title) + procMessageBox.Call(0, uintptr(unsafe.Pointer(t)), uintptr(unsafe.Pointer(c)), 0x10) +} + +func toggleAutostart(item *systray.MenuItem) { + enable := !item.Checked() + if err := setAutostart(enable); err != nil { + errBox("vizd-tray", "Could not update autostart: "+err.Error()) + return + } + if enable { + item.Check() + } else { + item.Uncheck() + } +} + +// --- entry point ---------------------------------------------------------- + +func main() { + // Allocate the console vizd will share, then install our ctrl handler so + // closing that window (X) hides it to the tray instead of killing vizd. + allocConsole() + installCtrlHandler() + signal.Ignore(os.Interrupt) // defensive: survive the Ctrl+C we send + + vizd, err := spawnVizd() + if err != nil { + errBox("vizd-tray", err.Error()) + } + + ready := make(chan struct{}) + + // Follow vizd: when it exits (Close vizd, Ctrl+C in the window, crash) the + // launcher has nothing left to do and exits as well. + if vizd != nil { + go func() { + _ = vizd.Wait() + <-ready + systray.Quit() + }() + } + + onReady := func() { + systray.SetIcon(trayIcon) + systray.SetTitle("VIZ node") + systray.SetTooltip("VIZ node (vizd)") + + mOpen := systray.AddMenuItem("Open window", "Show the node console") + mClose := systray.AddMenuItem("Close vizd", "Stop the node") + mAuto := systray.AddMenuItemCheckbox("Enable autostart", "Start the node at Windows login", autostartEnabled()) + systray.AddSeparator() + mQuit := systray.AddMenuItem("Quit", "Stop vizd and exit") + + close(ready) + + go func() { + for { + select { + case <-mOpen.ClickedCh: + showConsole() + case <-mClose.ClickedCh: + stopVizd(vizd) + systray.Quit() + case <-mAuto.ClickedCh: + toggleAutostart(mAuto) + case <-mQuit.ClickedCh: + stopVizd(vizd) + systray.Quit() + } + } + }() + } + onExit := func() {} + + systray.Run(onReady, onExit) +} From 8b6a3f29347583b062f4e9d3dc6b11b973e342fa Mon Sep 17 00:00:00 2001 From: Anatoly Piskunov Date: Fri, 21 Aug 2026 10:53:40 +0200 Subject: [PATCH 3/3] ci(windows): build and ship vizd-tray.exe Add a setup-go + go build step (static, -H windowsgui) and attach the resulting vizd-tray.exe to the Windows release next to vizd.exe. --- .github/workflows/windows-release.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index d5adc617be..ae464a17c0 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -187,12 +187,31 @@ jobs: cd $GITHUB_WORKSPACE/build mingw32-make -j$(nproc) cli_wallet # ------------------------------------------------------- + # 10b. Build the tray launcher (Go, static, GUI subsystem) + # ------------------------------------------------------- + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + cache: true + cache-dependency-path: contrib/windows/vizd-tray/go.sum + + - name: Build vizd-tray + shell: pwsh + run: | + cd $env:GITHUB_WORKSPACE/contrib/windows/vizd-tray + $env:CGO_ENABLED = "0" + $env:GOOS = "windows" + $env:GOARCH = "amd64" + go build -trimpath -ldflags "-s -w -H windowsgui" -o $env:GITHUB_WORKSPACE/build/vizd-tray.exe . + # ------------------------------------------------------- # 11. Verify binaries # ------------------------------------------------------- - name: Verify binaries run: | ls -lh $GITHUB_WORKSPACE/build/programs/vizd/vizd.exe ls -lh $GITHUB_WORKSPACE/build/programs/cli_wallet/cli_wallet.exe + ls -lh $GITHUB_WORKSPACE/build/vizd-tray.exe # ------------------------------------------------------- # 12. Derive release metadata # Tag: win- e.g. win-a1b2c3d @@ -226,5 +245,6 @@ jobs: files: | build/programs/vizd/vizd.exe build/programs/cli_wallet/cli_wallet.exe + build/vizd-tray.exe env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file