Wails in Practice #10 Finishing Touches — Settings, Dark Mode, Data Backup
The final post of the series. Through #9, features are all there. But between an app that works and an app you want to reopen every day lies the difference of finishing. This post fills in that finishing — settings, dark mode, window state, data safety — and closes the series with a recap of the principles running through all ten posts.
This series is ten posts in two parts.
- Part 1: #1 · #2 · #3 · #4 · #5 · #6
- Part 2: #7 · #8 · #9 Implementing auto-update · #10 Finishing touches ← this post
Settings: what to save and where #
As the app grows, users get values they want to change — font size, theme, autosave delay, shortcut combination. These settings differ in nature from note data. Because they are simple key-value pairs with no need for search or relationships, a single JSON file fits better than putting them in #2’s SQLite. The location is under os.UserConfigDir, covered in intro #5, beside the notes DB.
type Settings struct {
Theme string `json:"theme"` // "system" | "light" | "dark"
FontSize int `json:"fontSize"`
AutosaveDelay int `json:"autosaveDelay"` // ms, ties to #7's debounce
}
func defaultSettings() Settings {
return Settings{Theme: "system", FontSize: 15, AutosaveDelay: 600}
}Exposing settings as a service is the same as for notes. Put GetSettings and SaveSettings as bindings and the frontend reads and writes them from a settings screen. The key is to keep the defaults in code. Even if the settings file is missing (first launch) or some keys are absent, fill from defaults so the app always starts with complete settings.
Dark mode: follow the system #
Theme is the flagship setting. Keep three values (system, light, dark), but the practical default is system — following the OS setting. When the user switches the OS to dark, having the app go dark too is natural. On the frontend, take the system value with CSS prefers-color-scheme, and if the user explicitly picks light/dark, that choice wins.
function applyTheme(setting) {
let theme = setting; // "light" | "dark" | "system"
if (setting === "system") {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark" : "light";
}
document.documentElement.dataset.theme = theme; // CSS changes colors from this
}Gather colors into CSS variables in one place and switch with data-theme, and you manage the theme without scattering colors across the screen.
Remembering window state #
A small touch with a big feel is window state. If the user resizes and moves the window but it returns to origin next time, they have to readjust every time. On close (or in #4’s hide hook), save the window’s size and position to the settings file, and restore from it on startup. Read and write with the Wails runtime’s window size/position query and set functions. You only need to guard against a saved position falling off-screen on multi-monitor setups.
Data safety: backup, export, import #
The thing you must protect most in a notes app is the user’s notes. The core of finishing is data safety, and you provide three things.
- Backup: having the app periodically copy the DB file lets you recover from corruption or accidental deletion. SQLite is a single file, so backup is a file copy.
- Export: the user must be able to take their notes out of the app. Export all notes as markdown files or a single JSON, and even if they leave the app the data stays theirs. This is a matter of trust too.
- Import: they must be able to reload what was exported or move it in from another machine. Pick a file with intro #4’s file dialog, and the service parses and stores it.
func (s *NoteService) ExportJSON() ([]byte, error) {
notes, err := s.repo.List()
if err != nil {
return nil, err
}
return json.MarshalIndent(notes, "", " ") // in a human-readable form
}Export and import layer naturally on top of #2’s repository. Thanks to the layer split, the new feature of taking notes out and bringing them in ends up as a few methods added to the service.
Recap of ten posts’ principles #
Looking back at the decisions running through ten posts, one line each.
- #1 Layer split: divide binding, service, repository with dependencies one way. This one thing made #8’s testing and #10’s export easy.
- #2 Pure-Go SQLite: the CGO-avoiding choice came back as a payoff in #6’s cross-compilation and #8’s CI testing.
- #3 Truth in Go: the design of flowing data through events solved #4’s background capture and #7’s list refresh in one line.
- #4 Background residence, #5 signing/notarization, and #6 CI automation made the app actually shippable, and
- #7 the editor, #8 testing, #9 auto-update, and #10 finishing filled in the polish worth reopening.
The running lesson is one: early structural decisions (layer split, pure Go, truth in Go) make every later feature cheap. Practice comes not from flashy features but from these decisions supporting one another.
Closing #
Where the intro series covered Wails’s syntax and concepts, this practice series carried a single app from design through shipping and on to the polish that brings users back. We split the layers, avoided CGO, added search, ran in the background, signed, automated, tested, attached updates, and finished. Having come this far, you hold the whole journey of turning a single idea into a desktop app real users use every day. That concludes the Wails practice course.
Summary #
- Settings are simple key-values with no search or relationships, so put them in a single JSON file, not SQLite. Keep defaults in code so it starts complete even without the file.
- The default for dark mode is following the system. Gather colors into CSS variables and switch with
data-theme. - Save and restore window size and position so users do not readjust every time. Guard only against off-screen positions.
- Provide data safety with backup, export, and import. Export is a matter of trust, and thanks to the layer split it fits neatly into the repository layer.
- The lesson of ten posts is one: early structural decisions make every later feature cheap. That concludes the series.