DEV Community

Toul
Toul

Posted on

GoLang, Svelte, and Wails 2.0: Cross Platform Desktops just got even easier

Intro

In the past building, a desktop app required you to learn a completely different tech stack. However, if you're a GoLang enthusiast, then you're in luck. In the many frameworks written, there's Wails v2. Which has been designed to let users stick with GoLang and their JavaScript (JS) framework choice to build a fully cross-platform desktop application.

Wails works similarly to Electron if you're familiar with it. If not, here's the gist: you can build a desktop app without learning anything other than JS, HTML, and CSS.

However, Wails has the added advantage of requiring less computer memory to run and a smaller final file size-- thanks to GoLang. GoLang is the Backend language for processing, and JS is used to pass the data.

I first encountered Wails v1 in a hackathon. I had a lot of fun building a DevOps GitHub Admin Desktop App for doing things such as bulk user removal, bulk branch protections, and more. The project was taking an existing CLI and giving it a GUI. Much like what we will be doing in this article with FreeResumeScanner.com, a single-page web application powered by a lambda for the backend,which is sort of like a CLI.

However, today I will be using Wails v2, which only recently has been released, so it's partly to explore the changes between v1 and v2.

TL;DR
Final Project Source Code

I. Installing Wails 2.0

For those coding along on MacOS

Make sure you have Xcode command line tools Xcode-select --install

I.a Now go install wails cli

  • go install github.com/wailsapp/wails/v2/cmd/wails@latest

I.b Check that wails cli is working

Run wails doctor, which should output the following if everything went smoothly.

Wails CLI v2.0.0-rc.1.1 Scanning system - Please wait (this may take a long time)...Done. System ------ OS: MacOS Version: 12.4 ID: 21F79 Go Version: go1.19.1 Platform: darwin Architecture: arm64 Wails ------ Version: v2.0.0-rc.1.1 Dependency Package Name Status Version ---------- ------------ ------ ------- xcode command line tools N/A Installed 2395 npm N/A Installed 8.4.0 *upx N/A Available *nsis N/A Available * - Optional Dependency Diagnosis --------- Your system is ready for Wails development! Optional package(s) installation details: - upx : Available at https://upx.github.io/ - nsis : Available at https://nsis.sourceforge.io/Download If Wails is useful to you or your company, please consider sponsoring the project: https://github.com/sponsors/leaanthony 
Enter fullscreen mode Exit fullscreen mode

Alright, we've made it to our first break. Congrats on getting Wails working on your Operating System of choice. Go have a walking break or a snack.

II. Generating FreeResumeScannerDesktop project

I am choosing to use Svelte for this project because I find it a simple and relatively light programming language. The basic paradigm of Svelte is that it compiles plain vanilla JS, and then the code calls it as needed. This helps reduce the final project size and keep things simple compared to using React.js for this project. Although React is excellent, once you know it it can take considerable time and effort to get productive. You could also use plain JS, which is also simpler than React.

NB I'd been meaning to try out Svelte for some time, and this seemed like an easy enough project to do so with.

II.a Generate Project Layout

Wails has an excellent CLI tool that makes working with the framework a breeze. Here's how easy it is to get started with our Svelte-based project:

wails init -n FreeResumeDesktop -t svelte

Which will lay down this directory structure for the project:

. ├── build/ │ ├── appicon.png │ ├── darwin/ │ └── windows/ ├── frontend/ ├── go.mod ├── go.sum ├── main.go ├── app.go └── wails.json 
Enter fullscreen mode Exit fullscreen mode

II.b Testing the plumbing

Whenever a project is generated, it will always be the same demo project. So let's test that it works by building the project and then dot slashing the binary.

First, build the binary:
wails build
Which will do all the heavy work of gluing together GoLang and JS by converting the code to JS functions that target the GoLang binary. And place the files in their targeted directories. For MacOs, the directory is build/bin/<your-app-name>.app/Contents/MacOs/<your-app-name>

To run it on MacOS type the following:
./build/bin/FreeResumeScannerDesktop.app/Contents/MacOS/FreeResumeScannerDesktop

NB if you're using a different OS, then substitute the MacOS for Windows or Linux.

Once the app loads, you should see the following

Wails v2 Demo

III. Dev Workflow

Now that the program has been verified to work let's start breaking things-- I mean building.

First, let's use the hot-reload feature, which means that each time a file changes, the code will be rebuilt and served in the browser.

III.a Start dev server

wails dev, the app will build and open a local version and one in a browser.

A convenient feature to save you the time of running wails build & ./.... to rebuild and restart the desktop app.

III.b Frontend

Previously, I shared How to use GoLang, Hugo, and Lambda for a Single Page Application and it contains the elements of the Frontend that is found in Start.

You'll notice that it becomes much simpler thanks to Svelte versus using AJAX and JQUERY. By simpler, I mean fewer lines of code. Less is more, as they say in Linux land.

In general, for this basic app, only two files must be changed in the generated project layout so that the Frontend can talk to the backend.

File 1: App.svelte => UI/UX

The App.svelte file is the one that renders the final user review on the desktop app, and this is where you'll spend most of the time changing things for UI/UX.

<script> import logo from './assets/images/logo-universal.png' import {ScanResume} from '../wailsjs/go/main/App.js' // Change the default to the following let resumeLength = "" let resumeMeasurables = "" let resumeLinkedIn = "" let resumeSoftSkills = "" let resumeHardSkills = "" let resume let job // Changed the default to the following function scan() { ScanResume(resume, job).then((result) => { resumeLength = result.resume_length; resumeMeasurables = result.measurable; resumeLinkedIn = result.linked_in; resumeSoftSkills= result.soft_skills; resumeHardSkills= result.hard_skills; }) } </script> <main> <img alt="Wails logo" id="logo" src="{logo}"> <div class="container"> <div class="input-box" id="input1"> <input autocomplete="off" bind:value={resume} class="input" id="resume" type="text" placeholder="Paste Resume Here"/> </div> <div class="table-responsive"> <table class="table"> <thead> <tr> <th scope="col">Metric</th> <th scope="col">Value</th> <th scope="col">Suggestion</th> </tr> </thead> <tbody> <tr> <td>Measurable</td> <td id="resume-measurable">{resumeMeasurables}</td> <td><a href="https://freeresumescanner.com/blog/2022/make-your-resume-pop-with-measurable-bullet-points">Make your Resume Pop with Measurable Bullet Points </a></td> </tr> <tr> <td>Missing Keywords</td> <td id="job-description-skills">{resumeHardSkills} , {resumeSoftSkills}</td> <td><a href="https://freeresumescanner.com/blog/2022/get-into-a-recruiters-inbox-with-keywords">Get into a Recruiters Inbox with Keywords</a></td> </tr> <tr> <td>Resume Length</td> <td id="resume-length-result">{resumeLength}</td> <td><a href="https://freeresumescanner.com/blog/2022/dont-get-auto-rejected-keep-your-resume-between-500-1000-words">Don't get auto-rejected keep your Resume between 500-1000 words </a></td> </tr> <tr> <td>Has LinkedIn</td> <td id="resume-has-linkedin-result">{resumeLinkedIn}</td> <td><a href="https://freeresumescanner.com/blog/2022/use-linkedin-so-that-recruiters-can-dm-you">Use LinkedIn so that Recruiters can DM you</a></td> </tr> </tbody> </table> </div> <div class="input-box" id="input2"> <input autocomplete="off" bind:value={job} class="input" id="job" type="text" placeholder="Paste Job Description Here"/> </div> </div> <button class="btn" on:click={scan}>Scan Resume</button> </main> <style> #logo { display: block; width: 30%; height: 30%; margin: auto; padding: 10% 0 0; background-position: center; background-repeat: no-repeat; background-size: 100% 100%; background-origin: content-box; } div.container{ display: flex; flex-wrap: nowrap; } table, th, td { margin-top: 10px; border: 1px solid black; border-collapse: collapse; background-color: antiquewhite; flex: 40%; color: black; } .input-box { margin: 10px; padding: 10px; flex: 40%; } .btn { width: 70%; height: 30px; line-height: 30px; border-radius: 3px; border: none; margin: 0 0 0 20px; padding: 0 8px; cursor: pointer; } .input-box .btn:hover { background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); color: #333333; } .input-box .input { border: none; border-radius: 3px; outline: none; height: 200px; line-height: 30px; padding: 0 10px; background-color: rgba(240, 240, 240, 1); -webkit-font-smoothing: antialiased; } .input-box .input:hover { border: none; background-color: rgba(255, 255, 255, 1); } .input-box .input:focus { border: none; background-color: rgba(255, 255, 255, 1); } </style> 
Enter fullscreen mode Exit fullscreen mode

File 2: App.js => Plumbing

As mentioned earlier, Wails does all the heavy lifting of binding GoLang code to JavaScript. This file contains the directions for what JavaScript should pass to the GoLang binary.

// @ts-check // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT export function ScanResume(arg1, arg2) { // changed the name of the func and the number of arguments expected return window['go']['main']['App']['ScanResume'](arg1, arg2); } 
Enter fullscreen mode Exit fullscreen mode

All we need to do here is tell it (1) What the GoLang function name is and (2) how many arguments it expects.

Now that the Front-end changes have been made, let's hook up the backend.

Whoot second break time. We're almost there. It's probably time for a coffee break if you're going through this in one sitting.

IV. Backend

As I mentioned, I shared How to use GoLang, Hugo, and Lambda for a Single Page Application and the Backend Code that powers the Frontend.

In that article, it was an AWS Lambda. This one is essentially the same, except we get the benefit of Deleting all of the AWS Lambda dependencies and code.

Feels Good Man

Anytime you can reduce the codebase size is a good time so let's do so.

package main import ( "context" "fmt" "regexp" "sort" "strings" ) // App struct type App struct { ctx context.Context } // var (...) is omitted to keep the code snippet short... // Analyze contains the algorithm for performing the critique func Analyze(r, d string) (*Result, error) { resu := strings.ToLower(r) desc := strings.ToLower(d) linkedIn := HasWord(resu, "linkedin") rLength := ResumeLength(resu) // Collect keywords from Job description JobDescrHardSkills := Find(desc, Hard) JobDescrSoftSkills := Find(desc, Soft) // Collect keywords from resume ResumeHardSkills := Find(resu, Hard) ResumeSoftSkills := Find(resu, Soft) // If Resume does not have hard skills that were found in the job description // then add the Result as recommendation hSkills := Diff(JobDescrHardSkills, ResumeHardSkills) // If Resume does not have soft skills that were found in the job description // then add the result as recommendation sSkills := Diff(JobDescrSoftSkills, ResumeSoftSkills) mSkillCount := MeasurableSkillCount(resu) res := &Result{ LinkedIn: linkedIn, HardSkills: hSkills, SoftSkills: sSkills, ResumeLength: rLength, Measurable: mSkillCount, } return res, nil } func Find(t string, set []string) (skills []string) { for _, s := range set { s = strings.ToLower(s) t = strings.ToLower(t) if strings.Contains(t, s) { skills = append(skills, s) } } return RemoveDups(skills) } func HasWord(t, h string) bool { return strings.Contains(t, h) } // Diff returns the elements in `a' that aren't in `b`. func Diff(a, b []string) []string { a = SortIfNeeded(a) a = RemoveDups(a) b = SortIfNeeded(b) b = RemoveDups(b) mb := make(map[string]struct{}, len(b)) for _, x := range b { mb[x] = struct{}{} } var diff []string for _, x := range a { if _, found := mb[x]; !found { diff = append(diff, x) } } return diff } func SortIfNeeded(a []string) []string { if sort.StringsAreSorted(a) { return a } s := append(a[:0:0], a...) sort.Strings(s) return s } func RemoveDups(elements []string) (nodups []string) { encountered := make(map[string]bool) for _, element := range elements { if !encountered[element] { nodups = append(nodups, element) encountered[element] = true } } return } // ResumeLength should be between 400 and 1000 words func ResumeLength(res string) float64 { return float64(len(strings.Split(res, " "))) } func MeasurableSkillCount(t string) float64 { re := regexp.MustCompile(`[\$ ]+?(\d+([,\.\d]+)?)`) nums := re.FindAllString(t, -1) return float64(len(nums)) } // Result is the conclusion of the analysis type Result struct { LinkedIn bool `json:"linked_in"` ResumeLength float64 `json:"resume_length"` Measurable float64 `json:"measurable"` HardSkills []string `json:"hard_skills"` SoftSkills []string `json:"soft_skills"` } // NewApp creates a new App application struct func NewApp() *App { return &App{} } // startup is called when the app starts. The context is saved // so we can call the runtime methods func (a *App) startup(ctx context.Context) { a.ctx = ctx } // ScanResume analyzes the resume against the job description func (a *App) ScanResume(resume, jobDescription string) *Result { res, err := Analyze(resume, jobDescription) if err != nil { fmt.Printf("err: ", err) } return res } 
Enter fullscreen mode Exit fullscreen mode

Woah, and just like that, we've added the backend code necessary to make FreeResumeScannerDesktop run locally. A pretty neat thing to add to one's resume.

End Result

It's not the prettiest desktop app, but it works. If you wanna make changes, then feel free to open a pull request and shine up that UI/UX. Getting, Pull Request merges approved and accepted is also a good thing to show recruiters; sometimes, if they're like me, they check the GitHub link on the resume.

Conclusion

Wails v2 makes building cross-platform desktop apps much more manageable than Wails v1, and this is because, first, the documentation has improved dramatically, in my opinion. Second, the wails-cli has gotten even better. It is easy to generate the file structure in several different javascript frameworks and one language ;).

I hope this article has been helpful to you and helps you with learning GoLang!

Top comments (0)