Sign in
Log inSign up
WILT: Golang systray and fyne

WILT: Golang systray and fyne

How to create a systray app in Golang that can pop up fyne windows

Colin Morris's photo
Colin Morris
·Sep 15, 2021·

2 min read

Continuing to experiment with Golang and extend my capabilities in the compiled app space. I've played with making GUI to improve my efficiency at work with Sharepoint search and auto-pdf generators written in Fyne. Fyne being a very decent cross platform GUI framework for Golang. A colleague showed me a neat thing they'd written using systray - so you could make a wee menu icon to show status and pop up a menu. So I tried to combine the two.

This resulted in hours of frustration - both of them wanted to be the last thing in the Main window to terminate and run; especially Fyne. It took me a long time to find[^1] the systray's Register function that would wait until the main window was looping and then trigger. So now I've managed to get an app that runs into the systray and does not appear in the Windows taskbar. And when I click a menu item in the systray it pops up a full window that does appear in the taskbar and you can close it and not lose the app. Useful.

Unfortunately, there's a known bug in the Mac implementation where both systray and Fyne compete to be the sole owner of the main thread, so to do this in the Mac environment you need to use systray and a different gui library.

Systray and Window

package main

import (
    "fmt"
    "time"

    "fyne.io/fyne/v2"
    app "fyne.io/fyne/v2/app"
    container "fyne.io/fyne/v2/container"
    layout "fyne.io/fyne/v2/layout"
    widget "fyne.io/fyne/v2/widget"
    "github.com/getlantern/systray"
    "github.com/skratchdot/open-golang/open"
)

var thisApp fyne.App
var mainWindow fyne.Window
var sabWindow fyne.Window

func main() {
    // Load Config
    // Load Auth tokens
    // Register systray's starting and exiting functions
    systray.Register(onReady, onExit)
    // Set up base GUI
    thisApp = app.NewWithID("Test Application")
    thisApp.SetIcon(resourceIconPng)
    mainWindow = thisApp.NewWindow("Hidden Main Window")
    mainWindow.Resize(fyne.NewSize(800, 800))
    mainWindow.SetMaster()
    mainWindow.Hide()
    mainWindow.SetCloseIntercept(func() {
        mainWindow.Hide()
    })
    sabWindow = thisApp.NewWindow("SAB Window")
    sabWindow.Resize(fyne.NewSize(640, 480))
    sabWindow.Hide()
    sabWindow.SetCloseIntercept(func() {
        sabWindow.Hide()
    })
    thisApp.Run()
}

func onReady() {
    // Refresh any expired tokens
    // Set up menu
    systray.SetTemplateIcon(icon.Data, icon.Data)
    title := "GU"
    systray.SetTitle(title)
    systray.SetTooltip(title)
    mGSM := systray.AddMenuItem("SAB", "SAB")
    systray.AddSeparator()
    mAbout := systray.AddMenuItem("About", "About this app")
    mPrefs := systray.AddMenuItem("Preferences", "Preferences")
    mQuit := systray.AddMenuItem("Quit", "Quit this")
    // Display Menu
    go func() {
        for {
            select {
            case <-mGSM.ClickedCh:
                sabWindow.Show()
            case <-mPrefs.ClickedCh:
                open.Run("vonexplaino.com")
            case <-mAbout.ClickedCh:
                open.Run("vonexplaino.com")
            case <-mQuit.ClickedCh:
                systray.Quit()
                return
            }
        }
    }()
}

[^1]: It was in an example file where I finally found the register function. Thank heavens for people who write examples in their source code