loading...

Building a simple system tray app with Go

osuka42 profile image Oscar Mendoza ・4 min read

System tray in go
Hands on: Let's build a system tray application that shows us the current local time, with the possibility to choose some other countries.
We are going to use Getlantern's library systray.

package main

import (
    "fmt"
    "io/ioutil"

    "github.com/getlantern/systray"
)

func main() {
    systray.Run(onReady, onExit)
}

func onReady() {
    systray.SetIcon(getIcon("assets/clock.ico"))
    systray.SetTitle("I'm alive!")
    systray.SetTooltip("Look at me, I'm a tooltip!")
}

func onExit() {
    // Cleaning stuff here.
}

func getIcon(s string) []byte {
    b, err := ioutil.ReadFile(s)
    if err != nil {
        fmt.Print(err)
    }
    return b
}

You can try this code right away.
The only thing that do is to show a simple message with an icon in the system tray; that's all.
Let's split up this a bit. In out main() the only thing that we do is to call a new instance of the library systray with the function Run, this function receives a couple a parameters where we are defining what to do onReady and when the application exits.
SetIcon, SetTitle and SetTooltip are pretty straightforward. The only thing we should have in consideration is that SetIcon takes as parameter the icon we want to display as a []byte. We use the function getIcon to read the .ico file and return the []byte.

Hint: A good place to find free icons is IconArchive.

Now, we don't want to have I'm alive! as title, so we are going to update each second with the local value of the timer; let's update our onReady() function and add a couple more that help us to get the time value:

func onReady() {
    systray.SetIcon(getIcon("assets/clock.ico"))
    for {
        systray.SetTitle(getTime())
        systray.SetTooltip("Look at me, I'm a tooltip!")
        time.Sleep(1 * time.Second)
    }
}

func getTime() string {
    t := time.Now()
    hour, min, sec := t.Clock()
    return ItoaTwoDigits(hour) + ":" + ItoaTwoDigits(min) + ":" + ItoaTwoDigits(sec)
}

// ItoaTwoDigits time.Clock returns one digit on values, so we make sure to convert to two digits
func ItoaTwoDigits(i int) string {
    b := "0" + strconv.Itoa(i)
    return b[len(b)-2:]
}

If we run, we'll see our clock being updated each second; but this is not the best way to implement an autoupdater. Implementing time.Sleep() will let us pause current instance, but we don't want to block our main process. In this case we are going to use a Go routine.

go func() {
    for {
    systray.SetTitle(getTime())
    systray.SetTooltip("Look at me, I'm a tooltip!")
        time.Sleep(1 * time.Second)
    }
}()

Now it's time to add some menu items. We can easily do it with systray.AddMenuItem("Title", "Tooltip"). This function returns us a channel when pressed. So, I'm going to add some items like this.

localTime := systray.AddMenuItem("Local time", "Local time")
hcmcTime := systray.AddMenuItem("Ho Chi Minh time", "Asia/Ho_Chi_Minh")
sydTime := systray.AddMenuItem("Sydney time", "Australia/Sydney")
gdlTime := systray.AddMenuItem("Guadalajara time", "America/Mexico_City")
sfTime := systray.AddMenuItem("San Fransisco time", "America/Los_Angeles")
systray.AddSeparator()
mQuit := systray.AddMenuItem("Quit", "Quits this app")

Also, in the top of the code, let's add a way to track the timezone in use:

var (
    timezone string
)

And replace systray.SetTitle(getTime()) for systray.SetTitle(getClockTime(timezone)).
Now, a way to fetch when a menu item is pressed.

go func() {
    for {
        select {
    case <-localTime.ClickedCh:
            timezone = "Local"
        case <-hcmcTime.ClickedCh:
            timezone = "Asia/Ho_Chi_Minh"
        case <-sydTime.ClickedCh:
            timezone = "Australia/Sydney"
        case <-gdlTime.ClickedCh:
            timezone = "America/Mexico_City"
        case <-sfTime.ClickedCh:
            timezone = "America/Los_Angeles"
        case <-mQuit.ClickedCh:
            systray.Quit()
            return
        }
    }
}()

Aaaand, let's modify our getTime function in order to be a bit more compatible with our timezone selector.

func getClockTime(tz string) string {
    t := time.Now()
    utc, _ := time.LoadLocation(tz)

    hour, min, sec := t.In(utc).Clock()
    return ItoaTwoDigits(hour) + ":" + ItoaTwoDigits(min) + ":" + ItoaTwoDigits(sec)
}

Hint: the timezone identifier is known as IANA time zone identifier; google it like that. You can easly get the identifier from the city you need in time.is.

So, all our code should look like:

package main

import (
    "fmt"
    "io/ioutil"
    "strconv"
    "time"

    "github.com/getlantern/systray"
)

var (
    timezone string
)

func main() {
    systray.Run(onReady, onExit)
}

func onReady() {
    timezone = "Local"

    systray.SetIcon(getIcon("assets/clock.ico"))

    localTime := systray.AddMenuItem("Local time", "Local time")
    hcmcTime := systray.AddMenuItem("Ho Chi Minh time", "Asia/Ho_Chi_Minh")
    sydTime := systray.AddMenuItem("Sydney time", "Australia/Sydney")
    gdlTime := systray.AddMenuItem("Guadalajara time", "America/Mexico_City")
    sfTime := systray.AddMenuItem("San Fransisco time", "America/Los_Angeles")
    systray.AddSeparator()
    mQuit := systray.AddMenuItem("Quit", "Quits this app")

    go func() {
        for {
            systray.SetTitle(getClockTime(timezone))
            systray.SetTooltip(timezone + " timezone")
            time.Sleep(1 * time.Second)
        }
    }()

    go func() {
        for {
            select {
            case <-localTime.ClickedCh:
                timezone = "Local"
            case <-hcmcTime.ClickedCh:
                timezone = "Asia/Ho_Chi_Minh"
            case <-sydTime.ClickedCh:
                timezone = "Australia/Sydney"
            case <-gdlTime.ClickedCh:
                timezone = "America/Mexico_City"
            case <-sfTime.ClickedCh:
                timezone = "America/Los_Angeles"
            case <-mQuit.ClickedCh:
                systray.Quit()
                return
            }
        }
    }()
}

func onExit() {
    // Cleaning stuff here.
}

func getClockTime(tz string) string {
    t := time.Now()
    utc, _ := time.LoadLocation(tz)

    hour, min, sec := t.In(utc).Clock()
    return ItoaTwoDigits(hour) + ":" + ItoaTwoDigits(min) + ":" + ItoaTwoDigits(sec)
}

// ItoaTwoDigits time.Clock returns one digit on values, so we make sure to convert to two digits
func ItoaTwoDigits(i int) string {
    b := "0" + strconv.Itoa(i)
    return b[len(b)-2:]
}

func getIcon(s string) []byte {
    b, err := ioutil.ReadFile(s)
    if err != nil {
        fmt.Print(err)
    }
    return b
}

🎉

Do you know a different/better way to implement any of this features?
Seems that we are hardcoding the timezone selection/channels, can you find a way optimize this part?

You can find all the code ready to run in github.com/Osuka42g/simple-clock-systray. Hack on! 🍻

Posted on by:

osuka42 profile

Oscar Mendoza

@osuka42

Turns out that JS wasn't so bad at the end...

Discussion

markdown guide
 

I'm learning Go now and I haven't done a .exe since ages, now it would be a good time to make some developer tools.
:( Tons of ideas - shortage of time.

 

Good article Oscar, I was implementing a sys tray icon for Iris using this library some months ago but it's not really cross-platform by-default, it requires from user to install some dependencies, i.e some linux packages (on windows systems it works out-of-the-box), so be careful when using this library, your users will not be able to run your package with just go get!

proof-of-concept: github.com/kataras/iris/tree/witht...

 

Thank you @kataras ! I will run this on different OS and update the requirements :)

 

Did you ever try to use this systray package together with some GUI golang package?
I tried to use GTK gotk3 to build some windows and run it together with github.com/getlantern/systray/ and i was not success.
This systray package blocks access to xwindows system from GTK package.
Do you have such experience?

 

Huh, that's really cool. I didn't know that library existed. Thanks for sharing Oscar!