Skip to content

API Reference desktop

Andres Felipe Martinez Salazar edited this page Nov 2, 2026 · 3 revisions

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.

Index

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

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

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

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.

Clipboard, shortcuts, launches, registration, secrets, menus and the trash

ReadClipboard and WriteClipboard copy and paste a tela.DataTransfer (text, HTML, images, files, custom types). GlobalShortcut is a component that registers a system-wide shortcut while mounted. SingleInstance keeps one process per app: a second start hands its Launch to the first, whose UseLaunch handlers open its files and links. Register makes the app a handler of a URLScheme or a FileType for the user, without a running app too (an installer); MakeDefault is a separate, explicit request. SetSecret, Secret and DeleteSecret keep small secrets in the OS's keyring, from a goroutine. PopupMenu and MenuBar are the OS's menus where it has them, for kit's menus to use. MoveToTrash moves a file where the user can restore it, never deleting it, also from a goroutine.

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.

Tests

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

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.

const MaxSecretSize = platform.MaxSecretSize

MaxSecretSize is the largest secret, in bytes, every OS stores (the limit of the Windows Credential Manager). Keep keys and tokens in secrets; encrypt larger data with a key kept there.

Variables

var ErrAppNotRegistered = platform.ErrAppNotRegistered

ErrAppNotRegistered is wrapped by the error a GlobalShortcut reports (ShortcutFailed) when the desktop refused the app's ID: on Linux the GlobalShortcuts portal (xdg-desktop-portal 1.22) accepts an app only when the desktop has an entry for its ID (AppID + ".desktop"), which RegisterApp writes. The error names the entry. On X11 the shortcut is grabbed instead and works without one.

var ErrAssociationTaken = platform.ErrAssociationTaken

ErrAssociationTaken is wrapped by the error of Register when another application holds the association and Replace was not given; nothing was written. The error names the other application.

var ErrDevExecutable = errors.New("under tela dev the program is a temporary build; give the installed program's path with desktop.ForApp(desktop.HandlerApp{Exec: ...})")

ErrDevExecutable is wrapped by the error of Register and MakeDefault under tela dev when no Exec was given (ForApp): the running program is a temporary dev build, which the OS must not be told to run. Nothing is written. Give the installed program's path, or register from a normal build.

var ErrSecretCanceled = platform.ErrSecretCanceled

ErrSecretCanceled is returned by the secret functions when the user canceled the OS's prompt to unlock the keyring (Linux): nothing was read or written. Asking again later is fine; do not ask in a loop.

var ErrSecretNotFound = platform.ErrSecretNotFound

ErrSecretNotFound is returned by Secret when no secret has the name. errors.Is(err, fs.ErrNotExist) also holds.

var ErrSecretTooLarge = platform.ErrSecretTooLarge

ErrSecretTooLarge is returned by SetSecret for a secret larger than MaxSecretSize.

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

func AppID

func AppID() string

AppID 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

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 ClipboardTypes

func ClipboardTypes(done func(types []string, err error)) error

ClipboardTypes calls done, later, on the UI thread, with the types the clipboard offers (MIME types, best first), without reading the data: enough to enable a Paste button. Same rules as ReadClipboard.

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

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 DeleteSecret

func DeleteSecret(name string) error

DeleteSecret removes the secret stored under name for this app; a missing one is not an error. It blocks; see SetSecret.

func FlushAll

func FlushAll() error

FlushAll writes the changes of every open store (Store.Flush) and returns the first error. tela.Run calls it when it returns.

func LoadPlacement

func LoadPlacement(key string) tela.WindowPlacement

LoadPlacement 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

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

func MessagePreview(m Message) tela.Component

MessagePreview 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 MoveToTrash

func MoveToTrash(path string) error

MoveToTrash moves the file or folder at path to the user's trash (the Recycle Bin on Windows, the Trash of the Linux desktop), where the user can restore it. A relative path is taken from the working directory. It never deletes: when the trash cannot take the file (a desktop without one, a file on a network drive or on another disk than the trash), the file stays where it is and the error wraps ErrUnsupported, so the app can tell the user the file is still there. A path that does not exist is an error wrapping fs.ErrNotExist.

Ask the user first, and call it from a goroutine: it blocks on file I/O and the desktop's file operation, and dev mode warns when it runs on the UI thread.

go func() {
    err := desktop.MoveToTrash(path)
    tela.Post(func() {
        switch {
        case err == nil:
            toast.Show("Moved " + filepath.Base(path) + " to the Trash")
        case errors.Is(err, desktop.ErrUnsupported):
            toast.Show("The file is still in " + filepath.Dir(path))
        default:
            toast.Error("Could not move the file", kit.ToastDescription(err.Error()))
        }
    })
}()

It needs a running app (tela.Run or telatest). In tests, telatest's headless trash moves the file into a temporary folder and App.Trashed lists what went there; App.FailTrash and App.Unsupported(telatest.Trash) play the failures.

func OpenFile

func OpenFile(done func(path string, err error), opts ...DialogOption) error

OpenFile 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

func OpenFiles(done func(paths []string, err error), opts ...DialogOption) error

OpenFiles is OpenFile for several files: done receives the chosen paths, nil when the user canceled.

func ParseKeys

func ParseKeys(keys string) (string, error)

ParseKeys checks a key combination as GlobalShortcut.Keys takes it and returns it written the usual way, as menus show it (the text for MenuItem.Accelerator): modifiers then the key, joined by '+'.

desktop.ParseKeys("ctrl+alt+n")   // "Ctrl+Alt+N", nil
desktop.ParseKeys("Ctrl+Hyper+N") // error: "Hyper" is not a modifier

Names ignore case. Modifiers: Ctrl (Control), Alt (Option), Shift, Super (Meta, Win, Cmd), and CmdOrCtrl (Super on macOS, Ctrl elsewhere). The key comes last: one character ("N", "1", "/"; Shift is written as a modifier), a key name ("F1"…"F24", "Escape", "Enter", "Tab", "Space", "Backspace", "Delete", "Insert", "Home", "End", "PageUp", "PageDown", "ArrowUp" or "Up", "PrintScreen", "MediaPlayPause"…), or "Plus" for '+'.

func PersistApp

func PersistApp(app tela.App) tela.App

PersistApp 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

func PickFolder(done func(path string, err error), opts ...DialogOption) error

PickFolder 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 PopupMenu

func PopupMenu(w tela.WindowHandle, at tela.Point, items []MenuItem, done func(chosen bool, err error)) error

PopupMenu shows items as the OS's context menu of w's window, with its top-left corner at at (logical pixels in the window's coordinates, such as a pointer event's position). It returns at once; when the user chooses an item, its OnSelect runs on the UI thread, then done (if not nil) with chosen true; when the user dismisses the menu, done runs with chosen false.

err := desktop.PopupMenu(w, tela.Point{X: e.X, Y: e.Y}, []desktop.MenuItem{
    {Label: "Copy", Accelerator: "Ctrl+C", OnSelect: copySelection},
    {Label: "Paste", Accelerator: "Ctrl+V", OnSelect: paste},
}, nil)
if errors.Is(err, desktop.ErrUnsupported) {
    setMenuOpen.Set(true) // draw the menu instead
}

The zero WindowHandle means the window the call comes from. The returned error says the menu could not be shown, and done is then never called: no running app, or no native context menus (errors.Is(err, ErrUnsupported): Linux, where Tela draws every menu). Call it on the UI thread. Tests answer the menu with telatest's App.ChoosePopupItem and App.DismissPopupMenu.

Example

A context menu: the OS's where there is one, else a drawn menu.

_ = tela.C(func() tela.Node {
    drawnAt, setDrawnAt := tela.UseState[*tela.Point](nil)
    items := []desktop.MenuItem{
        {Label: "Copy", Accelerator: "Ctrl+C", OnSelect: func() {}},
        {Label: "Paste", Accelerator: "Ctrl+V", OnSelect: func() {}},
    }
    return Div(Class("h-full"),
        OnContextMenu(func(e tela.MouseEvent) {
            at := tela.Point{X: e.X, Y: e.Y}
            if err := desktop.PopupMenu(tela.WindowHandle{}, at, items, nil); errors.Is(err, desktop.ErrUnsupported) {
                setDrawnAt.Set(&at) // draw the menu at at
            }
        }),
        If(drawnAt != nil, Text("drawn menu")),
    )
})

func ReadClipboard

func ReadClipboard(done func(dt *tela.DataTransfer, err error), types ...string) error

ReadClipboard reads the clipboard and calls done, later, on the UI thread, with a DataTransfer holding what it read: the given types that the clipboard offers, or, with no types, the well-known ones it offers (text, HTML, a PNG image, files). Types are MIME types or HTML's names ("text", "Files", "url"). An empty clipboard gives an empty DataTransfer.

desktop.ReadClipboard(func(dt *tela.DataTransfer, err error) {
    switch {
    case err != nil:
    case len(dt.Files()) > 0:
        attach(dt.Files())
    case dt.Has("image/png"):
        img, _ := dt.Image()
        insertImage(img)
    default:
        insertText(dt.Text())
    }
})

The DataTransfer is the caller's: it may keep and change it. Call it on the UI thread; done runs exactly once unless ReadClipboard returns an error (no running app, or ErrUnsupported on a desktop whose clipboard has only text: use ReadClipboardText). Tests preset the clipboard with telatest's App.SetClipboard.

Example

Copy rich text, and paste whatever the clipboard holds.

_ = tela.C(func() tela.Node {
    pasted, setPasted := tela.UseState("")
    copyNote := func() {
        var dt tela.DataTransfer
        dt.SetHTML("<b>Groceries</b>: milk, eggs")
        dt.SetText("Groceries: milk, eggs")
        _ = desktop.WriteClipboard(&dt)
    }
    paste := func() {
        _ = desktop.ReadClipboard(func(dt *tela.DataTransfer, err error) {
            switch {
            case err != nil:
            case len(dt.Files()) > 0:
                setPasted.Set(fmt.Sprintf("%d files", len(dt.Files())))
            default:
                setPasted.Set(dt.Text())
            }
        })
    }
    return Div(
        Button(OnClick(copyNote), Text("Copy")),
        Button(OnClick(paste), Text("Paste")),
        Text(pasted),
    )
})

func ReadClipboardText

func ReadClipboardText(done func(text string, err error)) error

ReadClipboardText reads the clipboard's text and calls done, later, on the UI thread ("" when it holds no text). It works on every desktop.

func Register

func Register(a Association, opts ...RegisterOption) (changed bool, err error)

Register makes the app a candidate handler of a URL scheme or a file type, for the current user (no administrator rights): the app appears in "Open with", and links of its scheme open it when no other application is the default. It does not make it the default (MakeDefault). It looks first, and writes nothing when the app is registered already (changed false).

if _, err := desktop.Register(desktop.URLScheme("notes")); err != nil {
    log.Print(err)
}
desktop.Register(desktop.FileType(".note", "application/x-notes-note", "Notes document"))

Linux writes the app's own .desktop file in ~/.local/share/applications (and a MIME type for a new extension); Windows writes HKEY_CURRENT_USER\Software\Classes. It works without a running app (an installer, a setup command), and is safe from any goroutine; it does file or registry I/O, so an app calls it from a goroutine or at start. The OS opens the app with the link or the files as arguments: read them with UseLaunch (with SingleInstance, in the running app). While an app runs (telatest included), the calls go to its backend, which tests inspect with telatest's App.Registrations.

Example

Register notes: links and .note files for the user, and become their default handler only when asked.

notes := desktop.URLScheme("notes")
doc := desktop.FileType(".note", "application/x-notes-note", "Notes document")
for _, a := range []desktop.Association{notes, doc} {
    if _, err := desktop.Register(a); err != nil {
        fmt.Println(err) // errors.Is(err, desktop.ErrAssociationTaken): ask before desktop.Replace()
    }
}
makeDefault := func() {
    r, err := desktop.MakeDefault(doc)
    if err == nil && r == desktop.DefaultAsked {
        fmt.Println("Choose Notes in the Default apps settings")
    }
}
_ = makeDefault

func RegisterApp

func RegisterApp(opts ...RegisterOption) (changed bool, err error)

RegisterApp makes the desktop know the app, without any URL scheme or file type (ADR-217). On Linux it writes the app's own desktop entry, ~/.local/share/applications/<AppID>.desktop, per user, with NoDisplay=true (no launcher in the menus) and no file types: the Linux global shortcuts portal refuses an app without one (GlobalShortcut). Scheme and file-type registration (Register) share that file. It looks first and writes nothing when the entry is up to date or another program's entry of the ID exists (a package's: never taken over without Replace). On Windows, which needs no entry, it writes nothing.

func main() {
    desktop.SetAppID("com.example.notes")
    if _, err := desktop.RegisterApp(); err != nil {
        log.Print(err)
    }
    tela.Main(App)
}

Call it from the installed program, at start: the entry runs os.Executable (ForApp gives another path, as an installer does). Under tela dev it returns ErrDevExecutable without Exec; under go run the program is a temporary build, so skip it there. It does file I/O and is safe from any goroutine; without a running app it uses the OS directly, and while an app runs (telatest included) its backend, which tests inspect with telatest's App.RegisteredApps.

func SaveFile

func SaveFile(done func(path string, err error), opts ...DialogOption) error

SaveFile 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

func SavePlacement(key string, p tela.WindowPlacement) error

SavePlacement saves p under key (UseSavePlacement does it by itself). The zero placement deletes the saved one.

func Secret

func Secret(name string) ([]byte, error)

Secret returns the secret stored under name for this app, or an error wrapping ErrSecretNotFound. It blocks; see SetSecret.

key, err := desktop.Secret("notes-key")
switch {
case errors.Is(err, desktop.ErrSecretNotFound):
    key = newKey() // first run
case errors.Is(err, desktop.ErrUnsupported):
    disableLockedNotes()
}

Example

A key kept in the OS's keyring, read off the UI thread.

_ = tela.C(func() tela.Node {
    status, setStatus := tela.UseState("unlocking…")
    tela.UseGo(func(ctx context.Context) {
        key, err := desktop.Secret("notes-key")
        switch {
        case errors.Is(err, desktop.ErrSecretNotFound):
            setStatus.Set("no key yet")
        case errors.Is(err, desktop.ErrSecretCanceled):
            setStatus.Set("the keyring stayed locked")
        case errors.Is(err, desktop.ErrUnsupported):
            setStatus.Set("locked notes need a keyring")
        case err == nil:
            setStatus.Set(fmt.Sprintf("a %d-byte key", len(key)))
        }
    }, tela.Once)
    return Text(status)
})

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.

The same ID (AppID) is the one the desktop knows the app by: the name of its desktop entry (RegisterApp), which the Linux global shortcuts portal needs, and the app of its notifications. tela.Run gives it to the OS services when it starts, so a later SetAppID changes only the directories.

func SetSecret

func SetSecret(name string, secret []byte) error

SetSecret stores secret under name in the OS's secure storage, for this app (AppID), replacing an earlier value: the Credential Manager on Windows, the Secret Service (GNOME Keyring, KWallet) on Linux. Use it for passwords, tokens and encryption keys, never in a plain file.

tela.UseGo(func(ctx context.Context) {
    if err := desktop.SetSecret("sync-token", token); err != nil {
        tela.Post(func() { setError.Set(err.Error()) })
    }
}, token)

The secret functions block: the OS may show a prompt to unlock the keyring and wait for the user. Call them from a goroutine (tela.UseGo), not from a handler or an effect, which would freeze the app; they are safe from any goroutine, and dev mode warns when one runs on the UI thread. A desktop without secure storage returns an error wrapping ErrUnsupported; a canceled prompt ErrSecretCanceled. They need a running app (tela.Run or telatest). Tests preset and read secrets with telatest's App.SetStoredSecret and App.StoredSecret.

func ShowMessage

func ShowMessage(m Message, done func(Answer)) error

ShowMessage 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 SingleInstance

func SingleInstance(app tela.App) tela.App

SingleInstance returns app made a single-instance app: when the app is started while it already runs, the new process hands its launch (its arguments and working directory) to the running one and exits with status 0 before opening any window (tela.Run returns nil at once). The running app brings its main window to the front (showing it if it was hidden in the tray) and gives the launch to its UseLaunch handlers:

func main() {
    tela.Main(desktop.SingleInstance(desktop.PersistApp(tela.App{
        AppID: "com.example.Notes",
        Title: "Notes",
        Root:  Notes{},
    })))
}

func (n Notes) Build() tela.Node {
    desktop.UseLaunch(func(l desktop.Launch) {
        for _, p := range l.Files() { openNote(p) }
        for _, u := range l.URLs() { openLink(u) } // notes:open?id=3
    })
    // ...
}

Instances are told apart by the app ID (AppID: SetAppID's, else app.AppID, else the executable's name), per user session: give the app an AppID. When the desktop cannot tell (no D-Bus session on Linux), the app runs on its own and a dev warning says why. Under tela dev the supervisor holds the claim across reloads (a second tela dev of the same app exits as a second instance). telatest claims nothing: App.InjectLaunch plays a second start.

Example

One window for every start of the app: a second start, from a file manager or a notes: link, opens its files and links in the running app.

app := desktop.SingleInstance(tela.App{
    AppID: "com.example.Notes",
    Title: "Notes",
    Root: tela.C(func() tela.Node {
        open, setOpen := tela.UseState([]string(nil))
        desktop.UseLaunch(func(l desktop.Launch) {
            setOpen.Update(func(o []string) []string { return append(o, append(l.Files(), l.URLs()...)...) })
        })
        return Div(Map(open, func(p string) tela.Node { return Div(Key(p), Text(p)) }))
    }),
})
_ = app // tela.Main(app)

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 Unregister

func Unregister(a Association, opts ...RegisterOption) (changed bool, err error)

Unregister removes what Register and MakeDefault wrote for a: the app is no longer a candidate, and a default that names it is removed. Another application's entries are never touched. Nothing to remove is not an error (changed false).

func UnregisterApp

func UnregisterApp(opts ...RegisterOption) (changed bool, err error)

UnregisterApp removes the entry RegisterApp wrote, unless schemes or file types registered with Register still need it: they keep it, and the Unregister of the last one removes it. An entry Tela did not write is never touched. Nothing to remove is not an error.

func UseLaunch

func UseLaunch(f func(Launch))

UseLaunch calls f, on the UI thread, with each launch of the app: once with the process's own start, after the component mounts, and with every later start that SingleInstance forwarded to this process (Launch.Forwarded). Use Launch.Files and Launch.URLs to open what the user asked for:

desktop.UseLaunch(func(l desktop.Launch) {
    for _, p := range l.Files() {
        openNote(p)
    }
})

The process's own start is given to the UseLaunch calls mounted when the first of them mounts (normally the app's first frame), not to those that mount later, so a screen that mounts again does not reopen the files. telatest gives no start launch (App.InjectLaunch gives later ones), and neither does a reload of tela dev.

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

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. def is the value only while key has none: a stored value that does not decode into T (the setting changed type between versions of the app) reads as def too. A stored value replaces def, with one exception: a struct field the stored value does not have keeps def's, so a field added in a new version of the app has its default. Slices, arrays and maps the stored value has are replaced whole and their elements start empty, never from def's element at the same index: a field an element omits ("omitempty") is the zero value, not def's. def itself is never modified, and the value shares no memory with it. 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

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; the default and the stored value combine as UseSetting describes.

func WriteClipboard

func WriteClipboard(dt *tela.DataTransfer) error

WriteClipboard replaces the clipboard's content with dt's, every representation of it, so another application pastes the one it understands:

var dt tela.DataTransfer
dt.SetHTML("<b>Groceries</b>: milk, eggs")
dt.SetText("Groceries: milk, eggs")
err := desktop.WriteClipboard(&dt)

A nil or empty dt empties the clipboard. Call it on the UI thread. It returns an error when no app runs, or wrapping ErrUnsupported on a desktop whose clipboard takes only text (use WriteClipboardText).

func WriteClipboardText

func WriteClipboardText(text string) error

WriteClipboardText puts text on the clipboard, replacing its content. Call it on the UI thread.

Types

type Answer

type Answer uint8

Answer is the button the user chose in a message box.

const (
    AnswerOK Answer = iota
    AnswerCancel
    AnswerYes
    AnswerNo
)

func (Answer) String

func (a Answer) String() string

type AppRegistration

type AppRegistration struct {
    // Registered: the desktop knows the app's ID (on Linux an entry
    // <ID>.desktop exists, whoever wrote it; always on Windows, which
    // needs none).
    Registered bool
    // Requested: RegisterApp wrote the entry, and UnregisterApp would
    // remove it.
    Requested bool
    // Other is the path of an entry of the app's ID that Tela did not
    // write (a package's, an installer's), "" when there is none.
    Other string
}

AppRegistration is what AppStatus reports.

func AppStatus

func AppStatus(opts ...RegisterOption) (AppRegistration, error)

AppStatus reports whether the desktop knows the app, and whether RegisterApp wrote its entry.

type Association

type Association struct {
    // contains filtered or unexported fields
}

Association is a URL scheme or a file type the app opens: build it with URLScheme or FileType.

func FileType

func FileType(extension, mimeType, description string) Association

FileType returns the association of a file extension, given with its dot (".note"), with its MIME type ("application/x-notes-note") and a description of the type in the user's language ("Notes document"), all required: Linux associates apps with MIME types, and registers this one for the extension when no other type claims it.

func URLScheme

func URLScheme(name string) Association

URLScheme returns the association of a URL scheme of the app's own, given without ':' ("notes" for notes:open?id=3 links): a letter, then letters, digits, '+', '-' or '.'. Standard schemes (http, mailto…) belong to other applications and panic.

func (Association) String

func (a Association) String() string

String returns "notes:" for a scheme, ".note" for a file type.

type Buttons

type Buttons uint8

Buttons 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

func (b Buttons) DefaultAnswer() Answer

DefaultAnswer is the button Enter chooses: OK, or Yes for the sets with Yes.

func (Buttons) DismissAnswer

func (b Buttons) DismissAnswer() Answer

DismissAnswer 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

func (b Buttons) Has(a Answer) bool

Has reports whether the set b has the button a.

type DefaultResult

type DefaultResult uint8

DefaultResult is what MakeDefault did.

const (
    // DefaultNone: nothing was done; MakeDefault returns it with every
    // error (the zero value: a failure never reads as DefaultSet).
    DefaultNone DefaultResult = iota
    // DefaultSet: the app is now the default handler.
    DefaultSet
    // DefaultAsked: the OS lets only the user choose (Windows file
    // types), so the system's Default apps settings opened; tell the user
    // to pick the app there, and check later with RegistrationStatus.
    DefaultAsked
)

func MakeDefault

func MakeDefault(a Association, opts ...RegisterOption) (DefaultResult, error)

MakeDefault asks for the app to become the default handler of a, registering it first if needed (with Register's rules and options). Where the OS lets an app set it (Linux; Windows schemes) it is set at once (DefaultSet). For Windows file types only the user can choose: MakeDefault opens the system's Default apps settings and returns DefaultAsked. On an error it returns DefaultNone. Call it only when the user asked ("Make Notes the default app for .note files").

func (DefaultResult) String

func (r DefaultResult) String() string

type DialogOption

type DialogOption struct {
    // contains filtered or unexported fields
}

DialogOption configures a file dialog: Title, Directory, DefaultName, Filter and Parent.

func DefaultName

func DefaultName(name string) DialogOption

DefaultName suggests the file name of a SaveFile dialog ("Untitled.md"). The other dialogs ignore it.

func Directory

func Directory(dir string) DialogOption

Directory sets the folder the dialog starts in (default: the OS's choice, usually the folder used last).

func Filter

func Filter(name string, patterns ...string) DialogOption

Filter 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

func Filters(filters ...FileFilter) DialogOption

Filters adds several entries at once, as Filter does for one.

func Parent

func Parent(w tela.WindowHandle) DialogOption

Parent 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

func Title(title string) DialogOption

Title sets the dialog's title (default: the OS's, such as "Open").

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 GlobalShortcut

type GlobalShortcut struct {
    // ID identifies the shortcut to the OS, which keeps the user's
    // changes to it across runs (the portal): keep it stable
    // ("new-note"). Required; unique among the mounted shortcuts. When two
    // mounted components share an ID (a dev warning), the one that
    // registered last owns it and gets its events; the other stays
    // displaced, even after the owner unmounts, until its own ID, Keys or
    // Description change, when it registers again and becomes the owner.
    ID string
    // Keys is the preferred key combination, such as "Ctrl+Alt+N" or
    // "Super+F5" (see [ParseKeys] for the names). Required; a combination
    // that cannot be parsed panics in dev mode and is reported to
    // OnStatus as ShortcutFailed otherwise (keys a user typed: check them
    // with ParseKeys first).
    Keys string
    // Description says what the shortcut does, in the user's language
    // ("Create a note"); the desktop's shortcut settings show it.
    // Required.
    Description string
    // OnPress runs when the user presses the shortcut.
    OnPress func()
    // OnRelease runs when the user releases it, where the OS reports
    // releases (Linux; not Windows).
    OnRelease func()
    // OnStatus receives the outcome of the registration and its later
    // changes (the user bound other keys, or removed the shortcut). Nil
    // reports a status other than ShortcutBound as a dev warning.
    OnStatus func(ShortcutStatusEvent)
}

GlobalShortcut is a component that registers a system-wide keyboard shortcut while it is mounted: OnPress runs, on the UI thread, when the user presses Keys anywhere, even while another application has the focus. It follows its fields (registering again only when ID, Keys or Description change) and unregisters when it unmounts. It renders nothing where it is declared; several, with different IDs, may be mounted at once.

desktop.GlobalShortcut{
    ID:          "new-note",
    Keys:        "Ctrl+Alt+N",
    Description: "Create a note",
    OnPress:     func() { win.Show(); newNote() },
    OnStatus: func(e desktop.ShortcutStatusEvent) {
        if e.Status != desktop.ShortcutBound {
            setHint.Set(e.Err.Error()) // "Ctrl+Alt+N is taken by another application"
        }
    },
}

The OS decides: Windows refuses a combination another application holds (ShortcutTaken), and on Wayland the desktop asks the user first (through the GlobalShortcuts portal) and may bind other keys, which ShortcutStatusEvent.Trigger reports, or none (Hyprland: see ShortcutStatusEvent.Trigger). Where the desktop has no global shortcuts (Wayland without the portal), OnStatus receives ShortcutFailed with an error wrapping ErrUnsupported. Tests press it with telatest's App.PressShortcut.

On Linux the portal needs the app's desktop entry: call RegisterApp once in the installed program (at start, before tela.Run), or the shortcut fails with an error wrapping ErrAppNotRegistered that names the entry. Give the app an ID (SetAppID or tela.App.AppID): the executable's name is the default.

Example

A shortcut that works from any application, and a hint when the OS would not bind it.

_ = tela.C(func() tela.Node {
    win := tela.UseWindow()
    hint, setHint := tela.UseState("")
    return Div(
        desktop.GlobalShortcut{
            ID:          "new-note",
            Keys:        "Ctrl+Alt+N",
            Description: "Create a note",
            OnPress:     func() { win.Show() },
            OnStatus: func(e desktop.ShortcutStatusEvent) {
                if e.Status == desktop.ShortcutBound {
                    setHint.Set("Press " + e.Trigger + " anywhere for a new note")
                } else {
                    setHint.Set(e.Err.Error())
                }
            },
        },
        Text(hint),
    )
})

func (GlobalShortcut) Build

func (g GlobalShortcut) Build() tela.Node

Build registers the shortcut while the component is mounted (see GlobalShortcut).

type HandlerApp

type HandlerApp struct {
    // ID is the app ID, which names the app's registration (its .desktop
    // file on Linux, its ProgIDs on Windows). Default: [AppID].
    ID string
    // Name is shown in the desktop's "Open with" lists. Default: the
    // last part of the ID, capitalized ("com.example.notes" is "Notes").
    Name string
    // Exec is the absolute path of the program to run, with Args and then
    // the link or the files. Default: os.Executable. Under tela dev the
    // executable is a temporary dev build, so Register and MakeDefault
    // there need Exec given (ErrDevExecutable otherwise); under go run it
    // is a temporary build too: register from the installed program, or
    // give its path.
    Exec string
    // Args are arguments put before the link or the files.
    Args []string
    // Icon is the absolute path of the app's icon (PNG or SVG on Linux,
    // .ico on Windows). Optional.
    Icon string
}

HandlerApp describes the application registered as a handler: what the OS runs to open a link or a file. Its zero fields are the running program's (see RegisterOption ForApp).

type Launch

type Launch struct {
    // Args are the command-line arguments after the program's name.
    Args []string
    // Dir is the working directory of the process that was started, for
    // relative paths in Args.
    Dir string
    // Forwarded reports a launch of another process of the app, which
    // [SingleInstance] forwarded to this one and exited; false for this
    // process's own start.
    Forwarded bool
}

Launch is how the app was started: the command-line arguments and the working directory of a process of the app. UseLaunch gives the app its own start's, and, with SingleInstance, those of every later start that another process forwarded: a file opened from a file manager, a deep link such as "notes:open?id=3" clicked in a browser.

Example

The files a launch names, made absolute.

l := desktop.Launch{Dir: "/home/ana", Args: []string{"--new-window", "notes:open?id=3", "file:///tmp/a%20b.note", "lists/groceries.note"}}
fmt.Println(l.URLs())
fmt.Println(l.Files())
// Output:
// [notes:open?id=3]
// [/tmp/a b.note /home/ana/lists/groceries.note]

func (Launch) Files

func (l Launch) Files() []string

Files returns the arguments that are files or folders, as absolute paths, in order: relative paths are made absolute against Dir, and file: URIs (which Linux launchers pass for a .desktop file's %U) are turned into paths. The files may not exist. Flags and URLs are left out (see URLs).

func (Launch) URLs

func (l Launch) URLs() []string

URLs returns the arguments that are URLs, in order: any "scheme:…" with a scheme of at least two characters ("notes:open?id=3", "https://example.com"), except file: URIs, which are files. Arguments that start with '-' are flags, neither URLs nor files (after a "--", every argument counts).

type MenuBar

type MenuBar struct {
    // Menus are the bar's top-level entries ("File", "Edit"), each with
    // its Submenu.
    Menus []MenuItem
    // OnError receives the error when the bar cannot be shown or
    // updated (ErrUnsupported where windows have no native menu bar).
    // Nil reports it as a dev warning.
    OnError func(err error)
}

MenuBar is a component that gives the window it is in the OS's menu bar while it is mounted (a Windows window with a title bar), follows its fields, and removes the bar when it unmounts. Menus are the top-level entries, each with its Submenu; choosing an item runs its OnSelect on the UI thread. It renders nothing where it is declared.

desktop.MenuBar{
    Menus: []desktop.MenuItem{
        {Label: "File", Submenu: []desktop.MenuItem{
            {Label: "New", Accelerator: "Ctrl+N", OnSelect: newNote},
            {Separator: true},
            {Label: "Quit", OnSelect: tela.Quit},
        }},
    },
    OnError: func(err error) { setDrawnMenu.Set(true) },
}

Most desktops have no native window menu bars (Linux; frameless windows, which draw their own title bar; macOS, whose menu bar belongs to the app), and where there is no window of a running app (tela snapshot): there OnError receives an error wrapping ErrUnsupported after the first build, and the app draws its menu instead (kit's Menubar does). Accelerator texts are only shown: handle the keys yourself. Tests choose items with telatest's App.ChooseMenuItem.

Example

A native menu bar, with a drawn one where windows have none.

_ = tela.C(func() tela.Node {
    drawn, setDrawn := tela.UseState(false)
    menus := []desktop.MenuItem{
        {Label: "File", Submenu: []desktop.MenuItem{
            {Label: "New", Accelerator: "Ctrl+N", OnSelect: func() {}},
            {Separator: true},
            {Label: "Quit", OnSelect: tela.Quit},
        }},
    }
    return Div(
        desktop.MenuBar{Menus: menus, OnError: func(error) { setDrawn.Set(true) }},
        If(drawn, Text("a drawn menu bar")),
    )
})

func (MenuBar) Build

func (m MenuBar) Build() tela.Node

Build sets the menu bar while the component is mounted (see MenuBar).

type MenuItem

type MenuItem struct {
    // Label is the item's text (plain text: "&" and "_" are shown as
    // they are).
    Label string
    // Accelerator is the text of the item's keyboard shortcut, shown at
    // its right ("Ctrl+S"; [ParseKeys] writes it the usual way). It is
    // display only: the app handles the key itself (h.OnKeyDown). Tray
    // menus may not show it.
    Accelerator 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 native menu: a tray icon's (Tray), a window's menu bar (MenuBar) or a context menu (PopupMenu). The backend never toggles a checkbox: OnSelect updates the state that renders Checked.

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

type MessageKind uint8

MessageKind 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

func (k MessageKind) String() string

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

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 RegisterOption

type RegisterOption struct {
    // contains filtered or unexported fields
}

RegisterOption configures Register, Unregister, MakeDefault, RegistrationStatus, RegisterApp, UnregisterApp and AppStatus: Replace and ForApp.

func ForApp

func ForApp(app HandlerApp) RegisterOption

ForApp sets what is registered instead of the running program: an installer registers the program it installed. Fields left empty keep their defaults (see HandlerApp).

desktop.Register(desktop.URLScheme("notes"), desktop.ForApp(desktop.HandlerApp{
    ID:   "com.example.Notes",
    Name: "Notes",
    Exec: `C:\Program Files\Notes\notes.exe`,
    Icon: `C:\Program Files\Notes\notes.ico`,
}))

func Replace

func Replace() RegisterOption

Replace lets Register (and MakeDefault, which registers first) take over an association another application holds: a Windows scheme key whose command runs another program, a Linux desktop entry of the app's ID that Tela did not write, an extension another MIME type claims. Without it Register returns an error wrapping ErrAssociationTaken and writes nothing. Ask the user before passing it.

type Registration

type Registration struct {
    // Registered: the app is a candidate handler (offered in "Open with",
    // or for the scheme).
    Registered bool
    // Default: the app is the default handler.
    Default bool
    // Other names what holds the association instead of the app (the
    // default handler, the owner of a Windows scheme key, the MIME type
    // that claims an extension on Linux); "" when nothing does.
    Other string
}

Registration is what RegistrationStatus reports.

func RegistrationStatus

func RegistrationStatus(a Association, opts ...RegisterOption) (Registration, error)

RegistrationStatus reports whether the app is registered for a, whether it is the default, and what else holds a.

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

func (s SavedWindow) Build() tela.Node

Build opens the window (see SavedWindow).

type ShortcutStatus

type ShortcutStatus uint8

ShortcutStatus is the state of a GlobalShortcut's registration.

const (
    // ShortcutBound: the shortcut works; Trigger is what the user presses.
    ShortcutBound ShortcutStatus = iota
    // ShortcutTaken: another application holds the combination.
    ShortcutTaken
    // ShortcutRefused: the user or the desktop declined to bind it.
    ShortcutRefused
    // ShortcutRevoked: the user or the desktop removed it after it was
    // bound.
    ShortcutRevoked
    // ShortcutFailed: it could not be registered (no global shortcuts on
    // this desktop: ErrUnsupported; invalid Keys; an OS error).
    ShortcutFailed
)

func (ShortcutStatus) String

func (s ShortcutStatus) String() string

type ShortcutStatusEvent

type ShortcutStatusEvent struct {
    // ID is the shortcut's.
    ID string
    // Status is the registration's state.
    Status ShortcutStatus
    // Trigger is the combination the user presses, as the desktop shows
    // it ("Ctrl+Alt+N"), for ShortcutBound: the desktop may bind other
    // keys than Keys. "" when it does not say, or has assigned no keys
    // yet: Hyprland registers the shortcut but ignores Keys, and the user
    // binds it in hyprland.conf with
    // "bind = CTRL ALT, N, global, <AppID>:<ID>"; it fires from then on.
    // Show the keys asked for, and say so.
    Trigger string
    // Err says why, for every status but ShortcutBound, in a sentence an
    // app can show ("desktop: the global shortcut Ctrl+Alt+N is taken by
    // another application").
    Err error
}

ShortcutStatusEvent is what GlobalShortcut's OnStatus receives.

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

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

func (n ShownNotification) Close() error

Close withdraws the notification; its callbacks are not called any more. Closing one that is gone does nothing. UI thread.

func (ShownNotification) IsZero

func (n ShownNotification) IsZero() bool

IsZero reports whether n is the zero ShownNotification.

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

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 []string
s.Get("recent", &recent)
fmt.Println(recent)
_ = s.Flush()
// Output: [groceries.md ideas.md]

func Settings

func Settings() *Store

Settings 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

func (s *Store) Delete(key string)

Delete removes key and its value; its watchers run.

func (*Store) Err

func (s *Store) Err() error

Err returns the error of the last failed read or write of the file, nil when the last one succeeded.

func (*Store) Flush

func (s *Store) Flush() error

Flush 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

func (s *Store) Get(key string, ptr any) bool

Get 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 default
desktop.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, arrays and maps the stored value has are replaced whole, and their elements start empty (a key removed from a stored map does not come back from the default, nor does a field an element omits through "omitempty"). *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

func (s *Store) Has(key string) bool

Has reports whether key has a value.

func (*Store) Keys

func (s *Store) Keys() []string

Keys returns the keys that have a value, sorted.

func (*Store) Path

func (s *Store) Path() string

Path returns the file the store is kept in ("" for a store in memory).

func (*Store) Set

func (s *Store) Set(key string, v any) error

Set 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

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

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

func (t Tray) Build() tela.Node

Build shows the icon while the component is mounted (see Tray).

Clone this wiki locally