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

Jin Gong
2026-03-14T16:56:04Z
1. Create a project
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"
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
- 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>
III. The complete effect is roughly as follows:

