DEV Community

y-yagi
y-yagi

Posted on

CLI for Windows with Go

Recently, I created a CLI for windows. I learned about some TIPS. Let's describe that in this article. Sample code exists in https://github.com/y-yagi/cli-for-windows

"GUI binary" or "console binary"

By default, Go build a binary as "console binary". This means a window console is kept showing when a binary is executed. It is good for applications that will finish immediately. But that's not good for some kind of applications(e.g. GUI application, HTTP server).

In the case of such applications, we can build binary as "GUI binary". In "GUI binary", a window console doesn't show when a binary is executed.

In order to build with "GUI binary", you need to specify "-H windowsgui" to ldflags.
On Windows, -H windowsgui writes a "GUI binary" instead of a "console binary."

$ go build -ldflags "-H windowsgui"
Enter fullscreen mode Exit fullscreen mode

See also: https://github.com/golang/go/blob/3b2a578166bdedd94110698c971ba8990771eb89/src/cmd/link/doc.go#L28

Embed icon to a binary file

Maybe you want to include an icon in your CLI. Go build binary as a Portable Executable format. Portable Executable format has a section for resources(rsrc section). We need to add an icon to that section.

When embed data to Go binary, need to prepare syso file. There is a tool called rsrc that will generate syso file for embedding in the rsrc section, so let's use that.

$ go get github.com/akavel/rsrc
$ rsrc -ico your-icon.ico -arch=amd64
Enter fullscreen mode Exit fullscreen mode

You need to prepare syso files per architecture. syso file will link automatically when run go build. So we don't need to specify options or files to go build.

See also: GcToolchainTricks ยท golang/go Wiki

Notification Area

If you choose "GUI binary" applications, maybe we need to consider how the exit of applications.
In the case of "console binary", if want to exit, simplify close the console. But in the case of "GUI binary", we can't do that.
Of course, we can exit applications via the Task Manager. But maybe it difficult for people who unfamiliar with Windows.

So let's consider to use "Notification Area"(or called "System Tray").

The notification area is located at the right end of the taskbar, it can be operated to applications by GUI.

Of course, already exists that library for using Notification Area.
getlantern/systray

Here's an example code with systray.

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

func onReady() {
    systray.SetIcon(icon) // (2)
    systray.SetTitle("My CLI")
    systray.SetTooltip("My CLI")

    mQuit := systray.AddMenuItem("Quit", "Quit") // (3)
    <-mQuit.ClickedCh
    systray.Quit()
}

func onExit() {
  // clean up
}
Enter fullscreen mode Exit fullscreen mode

(1) systray.Run initializes GUI and starts the event loop. Also, you can pass two functions that run when applications starting and exiting.
(2) systray.SetIcon accept to array bytes of icon data. This means you need to prepare array bytes of an icon. 2goarray can use in this case. 2goarray can encode files into array bytes.

$ go get github.com/cratonica/2goarray
$ 2goarray icon main < icon.png > icon.go
Enter fullscreen mode Exit fullscreen mode

(3) systray.AddMenuItem add a menu item to an application. In this example, add a menu item that quit an application.

This example shows as follows on Windows.

Alt Text

CI

Of course, we need to consider the CI. Before, we have little choice for CI. But now we have some choices. For example, AppVeyor, CircleCI, and GitHub Actions.

In my personal opinion, if you are using GitHub, I recommend GitHub Actions. Because it can use easily and don't' need to create an extra account.

An example of GitHub Actions is the following. It needs to add a file to .github/workflows/ directory for use(e.g. .github/workflows/ci.yml).

name: CI
on: [push]
jobs:

  build:
    name: Build
    runs-on: windows-latest
    steps:

    - name: Set up Go # (1)
      uses: actions/setup-go@v2
      with:
        go-version: 1.15

    - name: Check out code # (2)
      uses: actions/checkout@v2

    - name: Run test (3)
      run: |
        go test ./...
      env:
        GO111MODULE: on
Enter fullscreen mode Exit fullscreen mode

(1) Set up Go environment by setup-go action.
(2) Checks-out my repository by checkout action.
(3) Run tests.

That's it!

If you want to run golangci-lint in CI, you can use golangci-lint-action same as other actions.

Release

Finally, let's consider the management of release binaries. If you are using GitHub, I think GitHub Releases is good for that. It can manage source and binaries in the same place.

GitHub Releases are based on Git tags. So I wanted to upload binaries to GitHub Releases when push gi tags. We can use GitHub Actions also this case. Here is an example of a setting.

name: Release
on:
  push:
    tags:
      - 'v*' # (1)

jobs:
  windows:
    runs-on: windows-latest
    steps:
      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: 1.15

      - name: Check out code into the Go module directory
        uses: actions/checkout@v2

      - name: Run release # (2)
        uses: goreleaser/goreleaser-action@v2
        with:
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

(1) Restrict that only run when git tag started with v(e.g. v1.0.0).
(2) Use GoReleaser action for upload binaries to GitHub Releases.

GoReleaser settings as follows.

builds:
  - binary: "MY CLI"
    goos:
      - windows
    ldflags: -H windowsgui
    goarch:
      - amd64
      - 386
archives:
  -
    format: zip
release:
  github:
    owner: y-yagi
    name: cli-for-windows
Enter fullscreen mode Exit fullscreen mode

You need to specify github section under the release section` for upload binaries to GitHub Releases.

Discussion (0)