I Built a Desktop App with Go + Wails3 in 4 Hours

Jin Gong

Jin Gong

2026-03-14T16:56:04Z

3 min read

1. Create a project

rendering

You can directly create a project using the GitHub template, which integrates the following functions out of the box:
✅ Internationalization (i18n) using vue-i18n
🌗 Automatic light/dark theme support with manual override
🗃️ SQLite database integration with CRUD operations
⌨️ Keyboard shortcut (hotkey) support for efficient interaction and easy extension
⚙️ Settings panel and hotkey configuration demo
💡 Built with Wails v3, Vue 3, and TypeScript

Here are two ways to create:

 #1、Create a project directly through the GitHub template
wails3 init -n myProject -t https://github.com/JinGongX/SuiDemo                                                                        
 #2、Clone the project to the local => Create the project
 git clone https://github.com/JinGongX/SuiDemo
 wails3 init -n "Your Project Name" -t ./"suidemo local directory"
Enter fullscreen mode Exit fullscreen mode

II. Project Development

The basic functions in the original template are ready to use out of the box, and basically, only the front-end needs to be continuously developed

  1. Write a prompt display page
 /* /pages/Tips/Index.vue*/
<script setup lang="ts">
import { ref,onMounted,onUnmounted } from'vue'
import { InsTips } from'../../../bindings/changeme/services/suistore'
import{HideTipsWindow} from'../../../bindings/changeme/appservice'
import { applyTheme } from'../../utils/ThemeManager'
const input = ref('')
const sendMessage = () => {
if (!input.value.trim()) return
const now = Math.floor(Date.now() / 1000) 
const expireAt = parseTime(input.value) || (now + 30 * 60)
  InsTips(detectType(input.value), input.value, expireAt)
  input.value = ''
  HideTipsWindow()//Hide
}
// Monitor the Esc key to close the panel
const onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
    input.value = ''
    HideTipsWindow()
  }
}
onMounted(() => {
window.addEventListener('keydown', onKeydown)
})
onUnmounted(() => {
window.removeEventListener('keydown', onKeydown)
})
//--- A simple function for identifying type and time can be subsequently integrated into a large model ---
type PromptType = 'security' | 'device' | 'life' | 'work'|'system' |'default'
function detectType(text: string): PromptType {
if (/password|security|verify/.test(text)) return'security'
if (/backup|storage|photo|computer/.test(text)) return'device'
if (/food|cook|life/.test(text)) return'life'
if (/meeting|task|work/.test(text)) return'work'
return'default'
}
function parseTime(text: string): number | undefined {
const m = text.match(/(\d+)\s*(分钟|分|min|m)/)
if (m) returnDate.now() + Number(m[1]) * 60 * 1000
returnundefined
}
//Adjust the style on the configuration page and synchronize it to the child window
const bc = new BroadcastChannel('theme')
  bc.onmessage = (e) => {
    applyTheme(e.data)
  }
</script>

<template>
  <div
    class="fixed inset-0 flex items-center justify-center bg-black/20 backdrop-blur-sm dark:bg-gray-400/20"
  >
    <div class="w-[420px] rounded-1xl bg-white/90 dark:bg-gray-500/90 dark:text-white backdrop-blur-md shadow-[0_16px_40px_rgba(0,0,0,0.18)] px-5 py-4"
    > 
      <div class="mb-3 text-sm text-neutral-500 dark:text-white">
        Prompt: Enter a reminder for yourself
      </div> 
      <textarea
        v-model="input"  @keydown.enter.prevent="sendMessage"
        rows="2"
        placeholder="For example: Remind me to back up my photos in 30 minutes"
        class="w-full resize-none rounded-xl border border-neutral-200 bg-neutral-50 dark:bg-gray-100 px-4 py-3 text-sm text-neutral-900 placeholder:text-neutral-400 focus:border-neutral-300 focus:outline-none"
      /> 
      <div class="mt-3 flex items-center justify-between text-xs text-neutral-400 dark:text-white">
        <div>Enter confirm · Esc cancel</div>
        <div>Automatic type recognition</div>
      </div>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

III. The complete effect is roughly as follows:

Bright
Dark