API reference: package desktop
import "github.com/kuronosu/tela/desktop"This page is generated from the doc comments of the package; the same text is on pkg.go.dev. For guides and examples, start at the wiki home.
- Overview
- Constants
- Variables
- Functions:
AppID·CacheDir·ConfigDir·DataDir·DefaultName·Directory·Filter·Filters·FlushAll·LoadPlacement·LogDir·MessagePreview·Notify·OpenFile·OpenFiles·OpenStore·Parent·PersistApp·PickFolder·SaveFile·SavePlacement·SetAppID·Settings·ShowMessage·StateDir·Title·UseSavePlacement·UseSetting·UseStoreSetting - Types
type Answer:Stringtype Buttons:DefaultAnswer·DismissAnswer·Hastype DialogOption:DefaultName·Directory·Filter·Filters·Parent·Titletype FileFiltertype MenuItemtype Messagetype MessageKind:Stringtype Notificationtype NotificationActiontype SavedWindow:Buildtype ShownNotification:Notify·Close·IsZerotype Store:OpenStore·Settings·Delete·Err·Flush·Get·Has·Keys·Path·Set·Watchtype Tray:Build
Overview
Section titled “Overview”Package desktop gives a Tela app the services of the desktop it runs on (ADR-178, ADR-181): file and folder dialogs, message boxes, notifications, a tray icon, the app’s own folders, a settings store and windows that open where the user left them. It works the same on Linux (Wayland and X11, through the XDG Desktop Portal and the freedesktop D-Bus services) and Windows, and in the headless backend of telatest and tela snapshot.
Dialogs
Section titled “Dialogs”OpenFile, OpenFiles, SaveFile and PickFolder show the OS’s dialogs; Title, Directory, DefaultName, Filter and Parent configure them. ShowMessage shows a message box with a fixed set of buttons: the OS’s on Windows, drawn by Tela with the app’s theme on Linux, whose portal has none. They are asynchronous like a browser’s APIs: the call returns at once, and the callback runs later on the UI thread, so it may set state. Cancel is not an error: a dialog answers “” (or nil paths) and a nil error.
Button(OnClick(func() { desktop.OpenFile(func(path string, err error) { if err == nil && path != "" { setPath.Set(path) } }, desktop.Filter("Notes", "*.md", "*.txt"))}), Text("Open…"))The calls run on the UI thread: from event handlers and effects, or from a goroutine through tela.Post. The dialog is modal to the window the call comes from (Parent chooses another).
Notifications and the tray
Section titled “Notifications and the tray”Notify shows a notification whose OnClick, action and OnClose callbacks run on the UI thread. Tray is a component: the icon is in the tray while it is mounted and follows its fields, like any other element; its MenuItem values run OnSelect. With App.OnClose hiding the main window, the app lives in the tray until tela.Quit.
Folders, settings and window placement
Section titled “Folders, settings and window placement”ConfigDir, DataDir, CacheDir, StateDir and LogDir return the app’s folders, following each OS’s conventions (XDG on Linux, %APPDATA% and %LOCALAPPDATA% on Windows, ~/Library on macOS), named after the app ID (SetAppID, else App.AppID, else the executable) and created on demand. Store is a small JSON key-value file written atomically; Settings is the app’s, and UseSetting is a useState whose value lives there:
theme, setTheme := desktop.UseSetting("theme", "system")PersistApp makes the main window open where it was closed; SavedWindow does the same for a tela.Window, and LoadPlacement with UseSavePlacement are the parts they are made of.
When a service is missing
Section titled “When a service is missing”Not every desktop has every service: GNOME shows no tray icons without an extension, a minimal Linux session may have no portal (no file dialogs) and no notification daemon. The calls then return an error wrapping ErrUnsupported (errors.Is), and the app falls back: a path field instead of a dialog, a message in the window instead of a notification, a window that does not hide itself on close without a tray.
telatest answers dialogs (App.AnswerFileDialog, App.AnswerMessage), reads and clicks notifications and the tray (App.Notifications, App.ClickNotification, App.Tray, App.ClickTrayItem), and points the folders at a temporary directory (telatest.TempDesktopDirs, which sets the DirEnv variable).
Constants
Section titled “Constants”const DirEnv = "TELA_DESKTOP_DIR"DirEnv is the environment variable that moves every app directory under one folder, for tests and portable installs: with TELA_DESKTOP_DIR=/tmp/x, ConfigDir is /tmp/x/<app ID>/config, and likewise data, cache, state and logs. telatest.TempDesktopDirs sets it to a temporary folder for one test.
const MainWindowKey = "main"MainWindowKey is the key PersistApp saves the main window’s placement under.
Variables
Section titled “Variables”var ErrUnsupported = fmt.Errorf("desktop: %w", errors.ErrUnsupported)ErrUnsupported is wrapped by the errors of services the platform or the session does not provide: file dialogs without an XDG Desktop Portal, a tray icon without a StatusNotifierWatcher (some GNOME sessions), any service on a backend that has none. Test for it with errors.Is and fall back:
err := desktop.OpenFile(onPicked, desktop.Filter("Notes", "*.md"))if errors.Is(err, desktop.ErrUnsupported) { showPathField() // let the user type a path instead}errors.Is(err, errors.ErrUnsupported) holds too.
Functions
Section titled “Functions”func AppID
Section titled “func AppID”func AppID() stringAppID returns the name of the app’s directories: SetAppID’s, else the running app’s tela.App.AppID, else the executable’s name (as the platform’s default AppID). Safe from any goroutine.
func CacheDir
Section titled “func CacheDir”func CacheDir() (string, error)CacheDir returns the app’s folder for data it can download or compute again (thumbnails, downloads), creating it if needed. The OS or the user may empty it:
- Linux: $XDG_CACHE_HOME/<app ID> (~/.cache/<app ID>)
- Windows: %LOCALAPPDATA%\<app ID>\cache
- macOS: ~/Library/Caches/<app ID>
func ConfigDir
Section titled “func ConfigDir”func ConfigDir() (string, error)ConfigDir returns the app’s folder for configuration and settings, creating it if needed:
- Linux: $XDG_CONFIG_HOME/<app ID> (~/.config/<app ID>)
- Windows: %APPDATA%\<app ID> (roaming)
- macOS: ~/Library/Application Support/<app ID>
The app ID is AppID’s. DirEnv moves every directory for tests.
func DataDir
Section titled “func DataDir”func DataDir() (string, error)DataDir returns the app’s folder for documents and data the user would miss (a library, a database), creating it if needed:
- Linux: $XDG_DATA_HOME/<app ID> (~/.local/share/<app ID>)
- Windows: %APPDATA%\<app ID> (roaming; the same as ConfigDir)
- macOS: ~/Library/Application Support/<app ID> (the same as ConfigDir)
func FlushAll
Section titled “func FlushAll”func FlushAll() errorFlushAll writes the changes of every open store (Store.Flush) and returns the first error. tela.Run calls it when it returns.
func LoadPlacement
Section titled “func LoadPlacement”func LoadPlacement(key string) tela.WindowPlacementLoadPlacement returns the window placement saved under key (by UseSavePlacement or SavePlacement), or the zero placement when none was saved: give it to tela.App.Placement or tela.Window.Placement, and the window opens where the user left it. Safe from any goroutine, before tela.Run too (call SetAppID first, or use PersistApp).
func LogDir
Section titled “func LogDir”func LogDir() (string, error)LogDir returns the app’s folder for log files, creating it if needed:
- Linux: $XDG_STATE_HOME/<app ID>/logs
- Windows: %LOCALAPPDATA%\<app ID>\logs
- macOS: ~/Library/Logs/<app ID>
func MessagePreview
Section titled “func MessagePreview”func MessagePreview(m Message) tela.ComponentMessagePreview is the drawn message box of m as a component, without a window: what the box shows on Linux. tela snapshot renders it in a preview (the window is 440 pixels wide), and a test can mount it:
func Preview() tela.Component { return desktop.MessagePreview(desktop.Message{Kind: desktop.MessageWarning, Text: "Disk almost full."})}Its buttons do nothing.
func OpenFile
Section titled “func OpenFile”func OpenFile(done func(path string, err error), opts ...DialogOption) errorOpenFile shows the OS’s open-file dialog and calls done with the chosen file. A path of “” with a nil error means the user canceled:
desktop.OpenFile(func(path string, err error) { switch { case err != nil: setError(err.Error()) case path == "": // canceled default: load(path) }}, desktop.Title("Open a note"), desktop.Filter("Notes", "*.md", "*.txt"))Call it on the UI thread: an event handler or an effect (from a goroutine, wrap it in tela.Post). It returns at once; done runs later on the UI thread, exactly once, so it may set state. The returned error says the dialog could not be shown, and done is then never called: no running app, or a desktop without file dialogs (errors.Is(err, ErrUnsupported): on Linux, no XDG Desktop Portal). Tests answer the dialog with telatest’s App.AnswerFileDialog.
Example
An open button: the dialog answers on the UI thread, and a desktop without file dialogs falls back to a path field.
_ = tela.C(func() tela.Node { path, setPath := tela.UseState("") typing, setTyping := tela.UseState(false) open := func() { err := desktop.OpenFile(func(p string, err error) { if err == nil && p != "" { // "" is cancel setPath.Set(p) } }, desktop.Title("Open a note"), desktop.Filter("Notes", "*.md", "*.txt")) if errors.Is(err, desktop.ErrUnsupported) { setTyping.Set(true) // no portal: let the user type the path } } return Div( Button(OnClick(open), Text("Open…")), If(typing, Input(Bind(path, setPath))), Text(path), )})func OpenFiles
Section titled “func OpenFiles”func OpenFiles(done func(paths []string, err error), opts ...DialogOption) errorOpenFiles is OpenFile for several files: done receives the chosen paths, nil when the user canceled.
func PersistApp
Section titled “func PersistApp”func PersistApp(app tela.App) tela.AppPersistApp returns app with the main window’s placement remembered: the placement saved under MainWindowKey becomes App.Placement (unless it is set), and the root saves it while the app runs and when it quits. When app.AppID is set and SetAppID was not called, it names the app’s directories (SetAppID), so the placement is read before tela.Run from the same folder it is saved to.
func main() { tela.Main(desktop.PersistApp(tela.App{ AppID: "com.example.notes", Title: "Notes", Width: 900, Height: 640, Root: Notes{}, }))}Example
The main window opens where the user left it.
app := desktop.PersistApp(tela.App{ AppID: "com.example.notes", Title: "Notes", Width: 900, Height: 640, Root: tela.C(func() tela.Node { return Text("Notes") }),})_ = app // tela.Main(app)func PickFolder
Section titled “func PickFolder”func PickFolder(done func(path string, err error), opts ...DialogOption) errorPickFolder shows the OS’s folder chooser and calls done with the chosen folder (“” with a nil error: canceled). Same rules as OpenFile; filters do not apply.
func SaveFile
Section titled “func SaveFile”func SaveFile(done func(path string, err error), opts ...DialogOption) errorSaveFile shows the OS’s save-file dialog and calls done with the chosen path (“” with a nil error: canceled). The OS asks before overwriting an existing file; done only gets the path, and the app writes the file. DefaultName suggests a name. Same rules as OpenFile.
desktop.SaveFile(func(path string, err error) { if err == nil && path != "" { save(path) }}, desktop.DefaultName("Untitled.md"), desktop.Filter("Markdown", "*.md"))func SavePlacement
Section titled “func SavePlacement”func SavePlacement(key string, p tela.WindowPlacement) errorSavePlacement saves p under key (UseSavePlacement does it by itself). The zero placement deletes the saved one.
func SetAppID
Section titled “func SetAppID”func SetAppID(id string)SetAppID sets the name of the app’s directories and settings (ConfigDir and the others): use the reverse-DNS name given to tela.App.AppID (“com.example.notes”). Call it at the start of main, before anything reads a directory or a setting; PersistApp calls it with the App’s AppID. Without it, the directories are named after the running app’s App.AppID, or before tela.Run (and without an AppID) after the executable. Safe from any goroutine.
func ShowMessage
Section titled “func ShowMessage”func ShowMessage(m Message, done func(Answer)) errorShowMessage shows a message box and calls done with the button the user chose (done may be nil). Closing the box without a button (Escape, its close button) answers Buttons.DismissAnswer; Enter chooses Buttons.DefaultAnswer.
desktop.ShowMessage(desktop.Message{ Kind: desktop.MessageQuestion, Title: "Unsaved changes", Text: "Save the changes to \"Groceries\" before closing?", Buttons: desktop.ButtonsYesNoCancel,}, func(a desktop.Answer) { switch a { case desktop.AnswerYes: saveAndClose() case desktop.AnswerNo: closeWithoutSaving() } // AnswerCancel: keep editing})The box is the OS’s where it has one (Windows); elsewhere (Linux, whose desktop portal has no message box) Tela draws it in a small modal window with the app’s theme tokens, in light and dark. Call it on the UI thread (a handler, an effect; from a goroutine, wrap it in tela.Post). It returns at once and done runs later on the UI thread, exactly once. The error says no box could be shown (no running app, or no window to show a drawn box in); done is then never called. Tests answer it with telatest’s App.AnswerMessage.
Example
A question before closing a document.
_ = tela.C(func() tela.Node { win := tela.UseWindow() return Button(OnClick(func() { desktop.ShowMessage(desktop.Message{ Kind: desktop.MessageQuestion, Title: "Unsaved changes", Text: "Save the changes before closing?", Buttons: desktop.ButtonsYesNoCancel, }, func(a desktop.Answer) { if a != desktop.AnswerCancel { win.Close() } }) }), Text("Close"))})func StateDir
Section titled “func StateDir”func StateDir() (string, error)StateDir returns the app’s folder for state worth keeping across runs but not worth backing up (window placements, recent files, history), creating it if needed:
- Linux: $XDG_STATE_HOME/<app ID> (~/.local/state/<app ID>)
- Windows: %LOCALAPPDATA%\<app ID>
- macOS: ~/Library/Application Support/<app ID>
func UseSavePlacement
Section titled “func UseSavePlacement”func UseSavePlacement(key string)UseSavePlacement saves the placement of the window the calling component renders in (tela.UseWindow) under key: when it changes (the user moved, resized, maximized it; written half a second after the last change) and when the component unmounts (the window closes, the app quits). Read it back with LoadPlacement:
tela.Window{ Title: "Inspector", Placement: desktop.LoadPlacement("inspector"), OnClose: func() { setOpen.Set(false) }, Root: Inspector{}, // Inspector's Build calls desktop.UseSavePlacement("inspector")}SavedWindow and PersistApp do both. key must stay the same for the life of the component. A tree without windows (tela snapshot) saves nothing.
func UseSetting
Section titled “func UseSetting”func UseSetting[T any](key string, def T) (T, tela.Setter[T])UseSetting returns the value of key in the app’s settings (Settings), or def when it has none, and a setter that stores a new value: the useState of a setting. The component builds again when the value changes, whoever changed it (this component, another one with the same key, Settings().Set from any goroutine), and the store writes the file shortly after a change.
func (Editor) Build() tela.Node { size, setSize := desktop.UseSetting("editor.fontSize", 14) return Div( Button(OnClick(func() { setSize.Update(func(n int) int { return n + 1 }) }), Text("A+")), Textarea(Style(tela.FontSize(float32(size)))), )}T must encode as JSON. A stored value that does not decode into T (the setting changed type between versions of the app) reads as def. A field missing from the stored value keeps the default’s, so a field added in a new version of the app has its default; slices and maps are replaced whole. def itself is never modified. key must stay the same for the life of the component (give it another tela key to read another setting).
Example
A setting read and written by a component.
_ = tela.C(func() tela.Node { dark, setDark := desktop.UseSetting("dark", false) return Button(OnClick(func() { setDark.Set(!dark) }), Textf("Dark mode: %v", dark))})func UseStoreSetting
Section titled “func UseStoreSetting”func UseStoreSetting[T any](s *Store, key string, def T) (T, tela.Setter[T])UseStoreSetting is UseSetting for a key of store s instead of the app’s settings: a field missing from the stored value keeps the default’s; slices and maps are replaced whole.
type Answer
Section titled “type Answer”type Answer uint8Answer is the button the user chose in a message box.
const ( AnswerOK Answer = iota AnswerCancel AnswerYes AnswerNo)func (Answer) String
Section titled “func (Answer) String”func (a Answer) String() stringtype Buttons
Section titled “type Buttons”type Buttons uint8Buttons is the set of buttons of a message box: the sets every native message box has.
const ( ButtonsOK Buttons = iota // OK (the default) ButtonsOKCancel // OK and Cancel ButtonsYesNo // Yes and No ButtonsYesNoCancel // Yes, No and Cancel)func (Buttons) DefaultAnswer
Section titled “func (Buttons) DefaultAnswer”func (b Buttons) DefaultAnswer() AnswerDefaultAnswer is the button Enter chooses: OK, or Yes for the sets with Yes.
func (Buttons) DismissAnswer
Section titled “func (Buttons) DismissAnswer”func (b Buttons) DismissAnswer() AnswerDismissAnswer is the answer of a box with buttons b that the user closes without a button (Escape, the close button): Cancel when the set has it, OK for ButtonsOK and No for ButtonsYesNo.
func (Buttons) Has
Section titled “func (Buttons) Has”func (b Buttons) Has(a Answer) boolHas reports whether the set b has the button a.
type DialogOption
Section titled “type DialogOption”type DialogOption struct { // contains filtered or unexported fields}DialogOption configures a file dialog: Title, Directory, DefaultName, Filter and Parent.
func DefaultName
Section titled “func DefaultName”func DefaultName(name string) DialogOptionDefaultName suggests the file name of a SaveFile dialog (“Untitled.md”). The other dialogs ignore it.
func Directory
Section titled “func Directory”func Directory(dir string) DialogOptionDirectory sets the folder the dialog starts in (default: the OS’s choice, usually the folder used last).
func Filter
Section titled “func Filter”func Filter(name string, patterns ...string) DialogOptionFilter adds an entry to the dialog’s list of file types: a label and glob patterns (“*.png”). Repeat it for several entries; the first is selected. Without filters the dialog shows every file. PickFolder ignores it.
desktop.OpenFile(done, desktop.Filter("Images", "*.png", "*.jpg", "*.webp"), desktop.Filter("All files", "*"))func Filters
Section titled “func Filters”func Filters(filters ...FileFilter) DialogOptionFilters adds several entries at once, as Filter does for one.
func Parent
Section titled “func Parent”func Parent(w tela.WindowHandle) DialogOptionParent makes the dialog modal to w’s window (tela.UseWindow). The default is the window the call comes from: the window whose handler or effect is running, else the focused window, else the main window.
func Title
Section titled “func Title”func Title(title string) DialogOptionTitle sets the dialog’s title (default: the OS’s, such as “Open”).
type FileFilter
Section titled “type FileFilter”type FileFilter struct { // Name is the label shown for the entry ("Images"). Name string // Patterns are glob patterns of file names: "*.png", "*.jpg", "*". Patterns []string}FileFilter is one entry of a file dialog’s list of types: a label and the glob patterns of the file names it shows.
type MenuItem
Section titled “type MenuItem”type MenuItem struct { // Label is the item's text (plain text: "&" and "_" are shown as // they are). Label string // Separator makes the item a separator line; the other fields are // ignored. Separator bool // Checkbox makes the item a check item, showing a check mark when // Checked. Checkbox, Checked bool // Disabled shows the item grayed out; it cannot be chosen. Disabled bool // Submenu, if not empty, makes the item open a submenu (OnSelect is // then never called). Submenu []MenuItem // OnSelect runs when the user chooses the item. OnSelect func()}MenuItem is an item of a tray icon’s menu. The backend never toggles a checkbox: OnSelect updates the state that renders Checked.
type Message
Section titled “type Message”type Message struct { // Kind is the icon and tone (MessageInfo by default). Kind MessageKind // Title is the box's title; empty uses the kind's ("Information", // "Warning", "Error", "Question"). Title string // Text is the message. Keep it to a few sentences; a blank line // separates paragraphs. Text string // Buttons is the set of buttons (ButtonsOK by default). Buttons Buttons // Parent is the window the box is modal to. The zero handle is the // window the call comes from (the window whose handler or effect is // running, else the focused window, else the main window). Parent tela.WindowHandle}Message is a message box: a short text with a fixed set of buttons.
type MessageKind
Section titled “type MessageKind”type MessageKind uint8MessageKind is the icon and tone of a message box.
const ( MessageInfo MessageKind = iota // information (the default) MessageWarning // a warning: something may go wrong MessageError // an error: something went wrong MessageQuestion // a question the buttons answer)func (MessageKind) String
Section titled “func (MessageKind) String”func (k MessageKind) String() stringtype Notification
Section titled “type Notification”type Notification struct { // Title is the notification's summary line; required. Title string // Body is the text below it; may be empty. Body string // Icon is the notification's image, the same picture at one or more // sizes; empty uses the app's icon. Icon []image.Image // Actions are buttons on the notification. Windows shows none (its // balloon notifications have no buttons): keep every action reachable // from the app too. Actions []NotificationAction // OnClick runs when the user clicks the notification itself. OnClick func() // OnClose runs when the notification goes away without OnClick or an // action: the user dismissed it, or it expired, as far as the OS // reports it (not after [ShownNotification].Close). OnClose func()}Notification is a desktop notification: a toast in the OS’s notification area.
type NotificationAction
Section titled “type NotificationAction”type NotificationAction struct { // Label is the button's text. Label string // OnClick runs when the user clicks the button. OnClick func()}NotificationAction is a button of a notification.
type SavedWindow
Section titled “type SavedWindow”type SavedWindow struct { // Key names the window's saved placement; required and unique in the // app ("main" is PersistApp's). Key string // Window is the window to open. Window tela.Window}SavedWindow is a tela.Window that opens where the user left it: the placement saved under Key is its Placement (unless Window.Placement is set), and its placement is saved under Key when it changes and when it closes (UseSavePlacement).
If(inspectorOpen, tela.Component(desktop.SavedWindow{ Key: "inspector", Window: tela.Window{ Title: "Inspector", Width: 360, Height: 600, OnClose: func() { setInspectorOpen.Set(false) }, Root: Inspector{}, },}))Example
A tool window that remembers where it was.
_ = tela.C(func() tela.Node { open, setOpen := tela.UseState(false) return Div( Button(OnClick(func() { setOpen.Set(true) }), Text("Inspector")), If(open, tela.Component(desktop.SavedWindow{ Key: "inspector", Window: tela.Window{ Title: "Inspector", Width: 360, Height: 600, OnClose: func() { setOpen.Set(false) }, Root: tela.C(func() tela.Node { return Text("Properties") }), }, })), )})func (SavedWindow) Build
Section titled “func (SavedWindow) Build”func (s SavedWindow) Build() tela.NodeBuild opens the window (see SavedWindow).
type ShownNotification
Section titled “type ShownNotification”type ShownNotification struct { // contains filtered or unexported fields}ShownNotification is a notification on screen, returned by Notify. The zero value is no notification.
func Notify
Section titled “func Notify”func Notify(n Notification) (ShownNotification, error)Notify shows a desktop notification. Its callbacks run on the UI thread, so they may set state; clicking the notification usually brings the app forward:
win := tela.UseWindow()...desktop.Notify(desktop.Notification{ Title: "Export finished", Body: "notes.pdf was saved in Documents.", OnClick: func() { win.Show() }, Actions: []desktop.NotificationAction{ {Label: "Open", OnClick: func() { tela.OpenPath(pdf) }}, },})Call it on the UI thread (a handler, an effect; from a goroutine, wrap it in tela.Post). The error says the notification could not be shown: no running app, or no notification service (errors.Is(err, ErrUnsupported)), where the app should show the news itself. Tests read the notifications with telatest’s App.Notifications and click them with App.ClickNotification.
Example
A notification that brings the app back when clicked.
_ = tela.C(func() tela.Node { win := tela.UseWindow() return Button(OnClick(func() { _, err := desktop.Notify(desktop.Notification{ Title: "Export finished", Body: "notes.pdf was saved in Documents.", OnClick: func() { win.Show() }, }) if err != nil { fmt.Println("no notifications:", err) } }), Text("Export"))})func (ShownNotification) Close
Section titled “func (ShownNotification) Close”func (n ShownNotification) Close() errorClose withdraws the notification; its callbacks are not called any more. Closing one that is gone does nothing. UI thread.
func (ShownNotification) IsZero
Section titled “func (ShownNotification) IsZero”func (n ShownNotification) IsZero() boolIsZero reports whether n is the zero ShownNotification.
type Store
Section titled “type Store”type Store struct { // contains filtered or unexported fields}Store is a small key-value store kept in a JSON file: an app’s settings (Settings) or any other small state. Values are anything encoding/json encodes; reads decode them into the caller’s type. Writes are kept in memory and written to the file shortly after (coalesced: a slider that sets a value every frame writes a few times a second), atomically (a temporary file renamed over the old one, so a crash never leaves half a file), and when the app quits. A Store is safe for use from any goroutine.
The file is read once, when the store opens; changes made to it by other programs while the app runs are not seen, and are overwritten by the next write.
func OpenStore
Section titled “func OpenStore”func OpenStore(path string) (*Store, error)OpenStore opens the store kept in the JSON file at path (created at the first write, with its folder). Opening the same path again returns the same Store. A missing file is an empty store. A file that cannot be read or parsed gives an error, but the store is still returned and usable: it starts empty, and a file that did not parse is renamed to path + “.corrupt” before the first write replaces it, so nothing the user had is lost. A file that exists but cannot be read (permissions, a sharing violation, an I/O error) is never overwritten: the store works in memory, each write tries to read the file again (and merges it with the changes made meanwhile once it can), and Store.Flush returns the error until then.
Example
A store of the app’s own, in its state folder.
dir, _ := os.MkdirTemp("", "store")defer os.RemoveAll(dir)
s, err := desktop.OpenStore(filepath.Join(dir, "recent.json"))if err != nil { fmt.Println(err) return}_ = s.Set("recent", []string{"groceries.md", "ideas.md"})var recent []strings.Get("recent", &recent)fmt.Println(recent)_ = s.Flush()// Output: [groceries.md ideas.md]func Settings
Section titled “func Settings”func Settings() *StoreSettings returns the app’s settings store, the file settings.json in ConfigDir. It never returns nil: when the folder cannot be found, the store lives in memory for this run, and when the file cannot be read or parsed it starts empty (see OpenStore); Store.Err says why.
var s struct{ Theme string; FontSize int }desktop.Settings().Get("editor", &s)Components read settings with UseSetting, which also builds them again when the value changes.
func (*Store) Delete
Section titled “func (*Store) Delete”func (s *Store) Delete(key string)Delete removes key and its value; its watchers run.
func (*Store) Err
Section titled “func (*Store) Err”func (s *Store) Err() errorErr returns the error of the last failed read or write of the file, nil when the last one succeeded.
func (*Store) Flush
Section titled “func (*Store) Flush”func (s *Store) Flush() errorFlush writes the changes still in memory now. It is called by itself shortly after a change and when tela.Run returns; call it before a program that does not use tela.Run exits. A store in memory does nothing.
func (*Store) Get
Section titled “func (*Store) Get”func (s *Store) Get(key string, ptr any) boolGet decodes the value of key into ptr, a non-nil pointer (as json.Unmarshal does), and reports whether it did. A missing key, or a value that does not decode into ptr’s type, leaves *ptr as it was and returns false, so a default set before the call stays:
size := 14 // the defaultdesktop.Settings().Get("fontSize", &size)The stored value is decoded over a copy of *ptr: a struct field the stored value does not have keeps the one *ptr had, while slices and maps the stored value has are replaced whole (a key removed from a stored map does not come back from the default). *ptr (a default’s slice or map included) is never modified in place, and is assigned only when the call returns true.
func (*Store) Has
Section titled “func (*Store) Has”func (s *Store) Has(key string) boolHas reports whether key has a value.
func (*Store) Keys
Section titled “func (*Store) Keys”func (s *Store) Keys() []stringKeys returns the keys that have a value, sorted.
func (*Store) Path
Section titled “func (*Store) Path”func (s *Store) Path() stringPath returns the file the store is kept in (“” for a store in memory).
func (*Store) Set
Section titled “func (*Store) Set”func (s *Store) Set(key string, v any) errorSet stores v under key. It returns an error, and changes nothing, when v cannot be encoded as JSON (a func, a channel). Setting a value equal to the stored one (the same JSON) does nothing; otherwise the watchers of key run, and the file is written shortly after.
func (*Store) Watch
Section titled “func (*Store) Watch”func (s *Store) Watch(key string, f func(key string)) (stop func())Watch calls f after each change of key’s value (Set with a different value, Delete), or of any key when key is “”, until stop is called. f runs on the goroutine that made the change, after it, without the store’s lock: it may read the store and set values. In components, use UseSetting, which builds them again on change.
type Tray
Section titled “type Tray”type Tray struct { // Icon is the tray image, the same picture at one or more sizes (16, // 24 and 32 pixels cover the usual trays; the OS picks). Required. Icon []image.Image // Tooltip is shown when the pointer rests on the icon. Tooltip string // Menu is the icon's context menu (a right click; on some Linux // panels any click). Empty: no menu. Menu []MenuItem // OnClick runs when the user clicks the icon (Windows, KDE; some // Linux panels open the menu instead and never report a click). OnClick func() // OnError receives the error when the icon cannot be shown or // updated. Nil reports it as a dev warning. OnError func(err error)}Tray is a component that shows an icon in the system tray while it is mounted (the Windows notification area, the StatusNotifierItem host of KDE, GNOME with the AppIndicator extension, and most Linux panels), updates it when its fields change, and removes it when it unmounts. It renders nothing where it is declared; put it anywhere in the app:
func (n Notes) Build() tela.Node { win := tela.UseWindow() muted, setMuted := tela.UseState(false) return Div( desktop.Tray{ Icon: appIcon, // []image.Image at 16, 24 and 32 px Tooltip: "Notes", OnClick: func() { win.Show() }, Menu: []desktop.MenuItem{ {Label: "Show Notes", OnSelect: func() { win.Show() }}, {Label: "Mute reminders", Checkbox: true, Checked: muted, OnSelect: func() { setMuted.Set(!muted) }}, {Separator: true}, {Label: "Quit", OnSelect: tela.Quit}, }, }, // ...the window's content )}With App.OnClose hiding the main window (OnClose: func(w tela.WindowHandle) { w.Hide() }), the app keeps running in the tray. Not every desktop has a tray: when the icon cannot be shown, OnError receives an error wrapping ErrUnsupported (GNOME without the AppIndicator extension); then do not hide the window on close, or the app cannot be reached. tela snapshot and windowless trees show no tray. Tests read the tray with telatest’s App.Tray and click it with App.ClickTray and App.ClickTrayItem.
Example
An app that lives in the tray: closing the window hides it, and the tray’s menu shows it again or quits.
icon := []image.Image{image.NewRGBA(image.Rect(0, 0, 16, 16)), image.NewRGBA(image.Rect(0, 0, 32, 32))}notes := tela.C(func() tela.Node { win := tela.UseWindow() trayOK, setTrayOK := tela.UseState(true) return Div( desktop.Tray{ Icon: icon, Tooltip: "Notes", OnClick: func() { win.Show() }, OnError: func(error) { setTrayOK.Set(false) }, Menu: []desktop.MenuItem{ {Label: "Show Notes", OnSelect: func() { win.Show() }}, {Separator: true}, {Label: "Quit", OnSelect: tela.Quit}, }, }, If(!trayOK, Text("The tray is not available: closing the window quits.")), )})_ = tela.App{ Title: "Notes", Root: notes, OnClose: func(w tela.WindowHandle) { w.Hide() }, // keep running in the tray}func (Tray) Build
Section titled “func (Tray) Build”func (t Tray) Build() tela.NodeBuild shows the icon while the component is mounted (see Tray).