Skip to content

Preferences

Preferences are small values your app wants to remember between runs: a user’s name, the last tab they opened, whether they turned on dark mode. You set a value and Bellui saves it for you.

Getting the preferences

// Preferences are stored per app, so the app needs a unique ID.
a := app.NewWithID("com.example.preferences")
prefs := a.Preferences()

App.Preferences returns the app’s bellui.Preferences. Saving to disk needs an app ID, so create the app with app.NewWithID. Without an ID, values still work while the app runs but are not saved.

Reading and writing values

You read and write with generic functions in the bellui package. The type parameter is the type you want back.

launches := bellui.PrefOr(prefs, "launches", 0) + 1
bellui.SetPref(prefs, "launches", launches)

name, ok := bellui.Pref[string](prefs, "name")
if !ok {
	name = "stranger"
}
FunctionWhat it does
Pref[T](p, key)Returns the value and true, or the zero value and false if the key is missing or holds another type.
PrefOr(p, key, fallback)Returns the value, or fallback if it is missing. The type is taken from fallback.
SetPref(p, key, value)Stores the value.
p.RemoveValue(key)Deletes the key.

The types you can store are bool, int, float64 and string, and slices of those. The compiler rejects anything else. Numbers are converted for you where needed: an int saved earlier can be read back as an int even though the stored form may not keep the difference between whole numbers and decimals.

Writing from a widget’s callback is the usual pattern:

entry := widget.NewEntry()
entry.SetText(name)
entry.OnChanged = func(s string) {
	bellui.SetPref(prefs, "name", s)
}

dark := widget.NewCheck("Dark mode", func(on bool) {
	bellui.SetPref(prefs, "dark", on)
})
dark.SetChecked(bellui.PrefOr(prefs, "dark", false))

Lists

PrefList, PrefListOr and SetPrefList do the same for slices. The type parameter is the element type.

recent := bellui.PrefListOr(prefs, "recent", []string{})
recent = append([]string{"opened at launch " + strconv.Itoa(launches)}, recent...)
if len(recent) > 3 {
	recent = recent[:3]
}
bellui.SetPrefList(prefs, "recent", recent)

Reacting to changes

AddChangeListener registers a function that runs after any preference changes. It does not tell you which key changed, so read the values you care about.

changes := widget.NewLabel("")
prefs.AddChangeListener(func() {
	// Runs on the goroutine that made the change: the UI thread when your
	// code sets a value, a background one when the file changes on disk.
	bellui.RunOnMain(func() {
		changes.SetText("Name is now " + bellui.PrefOr(prefs, "name", ""))
	})
})

On the desktop, Bellui watches the preferences file and reloads it when another process changes it. Listeners then run on that watcher’s goroutine, not the UI thread. bellui.RunOnMain runs your UI update inline when you are already on the UI thread, and moves it there otherwise.

Where preferences are stored

On desktop and mobile platforms the values are written to a file named preferences.pxf in the app’s configuration folder (the same folder as app storage). The file is written when a value changes, with rapid bursts of changes batched together, and once more when the app exits. On the web the values are kept in the browser’s localStorage.

The complete example

// Typed app preferences that persist between runs.
package main

import (
	"strconv"

	"github.com/belluikit/bellui"
	"github.com/belluikit/bellui/app"
	"github.com/belluikit/bellui/container"
	"github.com/belluikit/bellui/widget"
)

func main() {
	// Preferences are stored per app, so the app needs a unique ID.
	a := app.NewWithID("com.example.preferences")
	prefs := a.Preferences()
	w := a.NewWindow("Preferences")

	launches := bellui.PrefOr(prefs, "launches", 0) + 1
	bellui.SetPref(prefs, "launches", launches)

	name, ok := bellui.Pref[string](prefs, "name")
	if !ok {
		name = "stranger"
	}

	greeting := widget.NewLabel("Hello " + name + ", launch number " + strconv.Itoa(launches))

	entry := widget.NewEntry()
	entry.SetText(name)
	entry.OnChanged = func(s string) {
		bellui.SetPref(prefs, "name", s)
	}

	dark := widget.NewCheck("Dark mode", func(on bool) {
		bellui.SetPref(prefs, "dark", on)
	})
	dark.SetChecked(bellui.PrefOr(prefs, "dark", false))

	recent := bellui.PrefListOr(prefs, "recent", []string{})
	recent = append([]string{"opened at launch " + strconv.Itoa(launches)}, recent...)
	if len(recent) > 3 {
		recent = recent[:3]
	}
	bellui.SetPrefList(prefs, "recent", recent)

	changes := widget.NewLabel("")
	prefs.AddChangeListener(func() {
		// Runs on the goroutine that made the change: the UI thread when your
		// code sets a value, a background one when the file changes on disk.
		bellui.RunOnMain(func() {
			changes.SetText("Name is now " + bellui.PrefOr(prefs, "name", ""))
		})
	})

	reset := widget.NewButton("Forget my name", func() {
		prefs.RemoveValue("name")
	})

	w.SetContent(container.NewVBox(greeting, entry, dark, reset, changes,
		widget.NewLabel("Recent: "+joinLines(recent))))
	bellui.ShowAndRun(w)
}

func joinLines(items []string) string {
	s := ""
	for _, it := range items {
		s += "\n  " + it
	}
	return s
}