Colin Tester

Helping to build a more robust and inclusive web

Desktop App Development with Go and Wails

Published:
By:

Build backwards compatible apps with Go and Wails.

Wails was created to make it easier for developers to create desktop apps using Go and web technologies.

A main feature of Wails is that it uses the OS native rendering engine, e.g. WebKit WebView on Mac OS, in order to present HTML views and run client-side JavaScript. An approach different from, say, Electron, which embeds Chromium and Node.js directly into the final application binary. Wails' approach, therefore, results in a much smaller file size for the app binary.

Wails uses Vite – a fast build tool for web technologies – to bundle and watch client-side JavaScript as it is developed. Vite's default operating mode is to parse client code to run on browsers that support ECMAScript 2015+.

However, I needed to be sure that the apps I intend to share would run on both current and older desktop platforms. So, my approach in this article, was to configure Vite to produce client code that will be backwards compatible.

Ultimately, I wished to explore how the experience of developing with Wails could be tailored, restructuring the folder and files Wails creates automatically; in order to accommodate my operational preferences.

I primarily use an Apple Mac and the following article outlines the steps I took to provide a coherent working environment for developing desktop apps with Go and Wails.

An example project to support this article is available on Github

Wails Installation

Wails requires two dependencies, Go 1.18+ and NPM (Node15+), so make sure they are installed before continuing.

One should also view the Wails installation document for platform specific guidance.

  1. To install Wails, type the following into the CLI:

    $ go install github.com/wailsapp/wails/v2/cmd/wails@v2.3.1

    Latest version at the time of writing is: v2.3.1

  2. Once installation has completed, it is recommended to run the Wails system check, making sure the correct Wails dependencies are also installed. At the CLI, type the following:

    $ wails doctor

Wails app project initialisation

This section is about allowing Wails to initialise a project workspace.

  1. Create the project folder in the workspace – substitute your own project name for ‘project-name’.

    $ mkdir project-name \
    && cd project-name
  2. Initialise the new project workspace using Wails.

    $ wails init -n project-name -d .

    Flags used with the init command:

    • -n Set the name of the project – this is mandatory.
    • -d followed by the project directory; I specified ‘.’ meaning the current directory.

    Wails will create the following folder and files structure.

    .
    ├── README.md
    ├── app.go
    ├── build
    ├── frontend
    ├── go.mod
    ├── go.sum
    ├── main.go
    └── wails.json

Set up JavaScript dev environment

For this project, I wish to develop views using sass files, bundled into a single css asset. I also wish to structure the project's JavaScript, CSS and image files under a sub-directory; maintaining an organisation and working convention.

  1. Within the frontend folder, remove the src folder and its files.
  2. Rename the frontend folder to src.
  3. Within the renamed ‘src’ folder create folders for: js, sass and image, these will hold the JavaScript, CSS and view images respectively.
  4. Within src/js create a new file labelled: index.js.
  5. Within src/sass create a new file labelled: index.sass.

    Our src folder should be similar to the following:

    src
    ├── dist
    ├── image
    ├── index.html
    ├── js
    │   └── index.js
    ├── package.json
    ├── sass
    │   └── index.sass
    └── wailsjs

The src/index.html created by Wails is used to present the view and content within the desktop app window.

  1. Edit the src/index.html file, adding to the HTML <head> element a link to our new src/sass/index.sass file.

    <link rel="stylesheet" href="sass/index.sass">
  2. Lower down in the same src/index.html file, modify the script tag to source the js/index.js file.

    <script src="js/index.js" type="module">
  3. Now back in the CLI, change working directory to src.

    $ cd src

Vite uses RollUp to bundle JavaScript code. In order to get the desired backwards compatibility, we need to utilise a Babel plugin.

  1. At the CLI, install the following npm modules as dev dependencies:

    • vite@latest
    • sass
    • @rollup/plugin-babel
    • @babel/plugin-proposal-class-properties
    • @babel/plugin-proposal-nullish-coalescing-operator
    • @babel/plugin-proposal-optional-chaining
    • @babel/plugin-syntax-dynamic-import

    e.g. $ npm i -D vite@latest Repeat for each listed npm module.

  2. Open the src/package.json file, which should indicate the installed devDependencies:

    "devDependencies": {
      "@babel/plugin-proposal-class-properties": "^7.18.6",
      "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
      "@babel/plugin-proposal-optional-chaining": "^7.21.0",
      "@babel/plugin-syntax-dynamic-import": "^7.8.3",
      "@rollup/plugin-babel": "^6.0.3",
      "sass": "^1.58.3",
      "vite": "^4.1.4"
    }

    Note: that installed version numbers indicated in your package.json file may differ from those above.

We now need to let Vite know that the RollUp and Babel dependencies are to be used when bundling JavaScript files.

  1. Create a Babel configuration file: src/babel.config.json and place within it the following JSON:

    {
      "plugins": [
        "@babel/plugin-proposal-class-properties",
        "@babel/plugin-proposal-nullish-coalescing-operator",
        "@babel/plugin-proposal-optional-chaining",
        "@babel/plugin-syntax-dynamic-import"
      ]
    }
  2. Now create a Vite configuration file: src/vite.config.js and place within it the following JavaScript:

    import { babel } from '@rollup/plugin-babel';
    
    export default {
      plugins: [
        babel({
          babelHelpers: 'bundled'
        })
      ],
    }

We have now configured Vite to utilise a plugin for the Babel compiler to convert modern ECMAScript code into a backwards compatible version of JavaScript, specifically using the @babel plugins defined in the src/babel.config.json file.

Adding a custom icon for our Desktop app

Our app will need an icon, giving it a unique visual identity wherever it is viewable on a target platform, e.g. the Mac OS Dock and Windows Taskbar.

  1. Create a PNG image file (1024 × 1024 pixels) containing your custom icon artwork and place it within the build folder, replacing the existing file: appicon.png.

    Wails will use appicon.png as the application icon when building desktop app binaries.

  2. Whilst in the build folder, remove both the darwin and windows folders. These will be rebuilt by Wails later, at which point our custom icon file will be utilised.

Set up Go dev environment

Wails generates bindings for Inter-process Communication (IPC) between Go application code and JavaScript code.

  1. Within the root of the project folder, edit the go.mod file changing the module name, ‘changeme’ declared at the top, to something relevant to you and the project, e.g. project-domain/project-name – substituting your own project name.
  2. Remove the app.go file.
  3. Create a folder labelled: ‘go’. This folder will hold all the app Go packages and files.
  4. Within the new go folder, create a new subfolder labelled: IPC (Inter-process Communication), and within that a file labelled: service.go.
  5. Open the go/IPC/service.go file and add to the top: package IPC.

    The Service will act as the main pipe through which all communications will be handled between the Go and JavaScript code.

  6. Within service.go append the following:

    package IPC
    
    import (
      "context"
    )
    
    // Service struct
    type Service struct {
      ctx context.Context
    }
    
    // NewService creates a new IPC Service struct.
    func NewService() *Service {
      return &Service{}
    }
    
    // Represents IPC shared data.
    type data map[string]interface{}
    
    // Expected form of Service request parameters.
    type Request struct {
      Module string `json:"module"`
      Method string `json:"method"`
      Data data `json:"data"`
    }
    
    // Expected form of Service response data.
    type response struct {
      Module string `json:"module"`
      Method string `json:"method"`
      Data data `json:"data"`
    }
    
    // Request – method through which all IPC service requests are received.
    func (s *Service) Request(req Request) response {
      res := response{}
      res.Module = req.Module
      res.Method = req.Method
    
      // Set content to be returned to the request caller (JavaScript code):
      res.Data = data{"content": "I am thinking, therefore I exist"}
      
      return res
    }
  7. From the project's root folder, open to edit the main.go file.

    To keeps things simple at this level, the main.go file will contain only the definition of options for Wails and its instantiation, it will also import our IPC service API.

    We are going to modify the code generated by the Wails initialisation process.

  8. Edit the line embedding the assets for the client view to make sure it now points to our renamed src folder: substitute src for frontend making the embed command read as //go:embed all:src/dist.

    The dist folder is created by Vite and used by Wails to pull our client-side assets into the desktop app.

  9. We wish also to embed our custom icon into our app binary, so that it can be used within desktop views, e.g. within an ‘About’ view. Add to the main.go file the lines:

    //go:embed build/appicon.png
    var appIcon []byte
  10. Within the func main() remove all the contained code and then insert the following line:

    // assign an instance of our IPC Service API:
    service := IPC.NewService()

    We also need to make sure we add to the upper part of main.go an import statement for:

    import (
      …
      "project-domain/project-name/go/IPC"
    )
  11. Further within the func main() we will configure the Wails app. Add the following to func main():

    // Specific macOS desktop app options.
    macOptions := &mac.Options{
      Appearance: mac.NSAppearanceNameDarkAqua,
      About: &mac.AboutInfo{
        Title: "ProjectName", // substitute your own App's name.
        Message: "© 2023 Project Owner", // subtitute your own ‘Project Owner’.
        Icon: appIcon, // here we reference our embedded custom icon.
      },
    }
    
    // Define Wails operating options:
    wailsOptions := &options.App{
      Title: "ProjectName", // substitute your own App's name.
      Width: 1024,
      Height: 768,
      AssetServer: &assetserver.Options{
        Assets: assets, // passing in our resources embedded from src/dist.
      },
      // Bind our Service instance for IPC.
      Bind: []interface{}{
        service,
      },
      Mac: macOptions,
    }
    
    // Create application with defined options:
    if err := wails.Run(wailsOptions); err != nil {
      fmt.Println("Wails setup error:", err.Error())
    }
  12. We also need to make sure we import the required packages:

    import (
      "embed"
      "fmt"
      
      "github.com/wailsapp/wails/v2"
      "github.com/wailsapp/wails/v2/pkg/options"
      "github.com/wailsapp/wails/v2/pkg/options/assetserver"
      "github.com/wailsapp/wails/v2/pkg/options/mac"
    
      // substitute your own domain and project name as specified in the go.mod file.
      "project-domain/project-name/go/IPC" 
    )

Connecting up the IPC pipe

Wails, when generating bindings for IPC, builds JavaScript files within which the dependencies are defined. These dependencies are determined from exported (public) properties and methods defined within the Go app. They can be imported to the client code as needed.

However, on the client-side, the IPC is also represented by a global object assigned to the window object; this is how dependencies are referenced directly by the Wails generated JavaScript file.

We have, therefore, two ways to reference our IPC service, either by importing it from the Wails generated JavaScript file or directly off the window object. I favour using the latter.

We can now, within our src/js/index.js file, establish a connection to the Go app IPC service.

  1. Within the src/js folder open to edit index.js and add the following:

    // assign a reference to the IPC servive.
    const service = window.go && window.go.IPC.Service;

    In the example code above, the service reference is a global but could be assigned any way deemed appropriate.

    Any methods exported via the Go app service can be accessed via the service reference, e.g. to send a request to the Go runtime – using the Service API Request method – we would use: service.Request().

Let's add some more to our src/index.js file to make a simple request to the Service IPC to get content for our app main view.

  1. Append to src/index.js the following:

    // assign a reference to the <div id="app"> element:
    const app = document.querySelector('#app');
    
    // set up service request parameters:
    const params = {
      module: 'ergo',
      method: 'get',
    };
    
    // call the IPC service Request method, which returns a Promise:
    service.Request(params).then(res => {
      // display the content returned by the service Request method:
      app.innerText = res.data.content;
    });

    When the Service request is made, we would expect the Promise to resolve with a response object returned from the Service IPC written in Go. The response object carries data with a content property value, which is set as the text inside our referenced app HTML element.

Let's also add some simple styling for our app view.

  1. Open to edit src/sass/index.sass and add the following:

    body
      margin: 0
      height: 100vh
      font-family: verdana, helvetica, sans-serif
      font-size: 2rem
    
      display: flex
      justify-content: center
      align-items: center

Configure Wails for building our app

  1. From the project root folder, open to edit the Wails configuration file: wails.json.

  2. Change the fields: name and outputfilename to a value you wish to use.

  3. Add fields for: frontend:dir and reloaddirs setting each with the value of "src". These tell Wails the name of our client code folder, in which it observes the Vite dist directory and that files within the src folder should be watched for changes.

    The wails.json file should now include the following properties and values:

    {
      ...
      "frontend:dir": "src",
      "reloaddirs": "src",
      ...
    }

Let's now take the opportunity to test that all is working Ok.

  1. On the CLI in the root of the project folder, i.e. change directory up one level from src, type:

    $ wails dev

    Wails will go through steps to prepare the client-side code, run Vite, then build and run a desktop app.

As a result you should see a desktop window open for your app, similar to figure 1 below.

Figure 1: running desktop app window
Example of the running desktop app window.

Wails will now operate in watching mode; watching for changes to the app files and assets. You can now make changes to the JavaScript and Go code and Wails will either push those changes to the app or rebuild the app, as needed.

You can exit out of watching mode by either quitting the open app, or pressing Ctrl+C in the CLI.

Build a final binary

With the development phase working, let's now build a final binary file, one each to run on Mac and Windows.

On the CLI in the root of the project folder, type the build command:

$ wails build -clean -platform "darwin,windows/amd64" -o MyApp.exe

Flags used with the build command:

  • -clean removes previous builds before compiling.
  • -platform followed by the platform types, separated by a comma, informs Wails which OS platforms to build binaries for. See more information on the supported platforms.
  • -o followed by a filename, specifies the output filename and extension to use, although this appears to apply only to Windows binaries.

The folder build/bin should contain two versions of your app: one for Mac and another .exe file for Windows.

Summary

The article outlined:

  • Installing and configuring Wails.
  • Configuring Vite and its JavaScript bundling options.
  • Establishing an IPC (Inter-process Communcation) service between Go and JavaScript.
  • Adding an application custom icon.
  • Building final binaries for Mac and Windows.

It would be worth exploring further the Wails configuration options, particularly how to configure application menus, and the Wails runtime library.

I hope this article has been helpful for you to begin developing desktop apps with Go and Wails.

Article relevant links: