Building a simple system tray app with Go
Tags: go, systray
Published: 2017-10-29 06:26:17

---
title: Building a simple system tray app with Go
published: true
description: We are going to review step by step how to build a system tray application with Golang 1.9, and what are main points that we should take in consideration.
tags: golang, systray
---
![System tray in go](https://thepracticaldev.s3.amazonaws.com/i/v0s37zp20nm6ik5l5itb.png)
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](https://getlantern.org)'s library [systray](https://github.com/getlantern/).

```go
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](http://www.iconarchive.com/).

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:
```go
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](https://gobyexample.com/goroutines).
```go
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.
```go
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:
```go
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
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.
```go
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](https://time.is/).

So, all our code should look like:
```go
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](http://github.com/Osuka42g/simple-clock-systray). Hack on! ðŸ»