Repository navigation
Desktop Integration
Windows and Desktop covers an app's windows and the basic services of the OS: dialogs, notifications, the tray, settings. This guide covers what makes an app part of the desktop it runs on: files dragged in from a file manager, the clipboard, a shortcut that works while another application has the focus, links that open the app, one running copy per user, secrets kept in the OS's keyring, native menus and the system's accessibility settings.
Everything here follows the model of the rest of Tela. What lives while it is
mounted is a component (GlobalShortcut), what answers later is a callback on
the UI thread (ReadClipboard), and what blocks stays off the UI thread (the
secrets). Each section ends with how a platform differs; the
Platform notes collect them. For a complete app that uses
every feature, see
examples/notes:
it accepts dropped files, has a global quick-note shortcut, reopens in its
running instance from notes: links, registers itself for them and keeps locked
notes encrypted with a key in the keyring.
- Drag and drop
- The clipboard
- Global shortcuts
- One instance and launches
- Links and file types
- Secrets
- Native menus
- System settings
- Under tela dev
- Platform notes
- Testing
Drag and drop is HTML's. It works inside a window, between the windows of the app and with other applications: a file manager drops files on your window, your window drops text in an editor. The data goes through the OS where it can, so the drag leaves the window.
There are two roles. A target accepts a drag, and a source starts one.
Elements that are neither keep the drag gesture (OnDragStart, OnDrag,
OnDragEnd, see Events), which moves things inside the window and
never involves the OS.
A target has four handlers, all of which bubble:
OnDragEnter,
OnDragOver,
OnDragLeave and
OnDrop. They get a
tela.DragEvent whose DataTransfer says
what is being dragged. A target accepts the drag by calling
e.PreventDefault() in OnDragOver, as in HTML. Without it the drop is refused
and OnDrop never runs.
Before the drop only the types are known (Types(), Has("Files"),
Has("text/plain")): the OS does not hand over the data until the user lets go.
OnDrop has all of it:
| Read | Gives |
|---|---|
Files() |
Absolute paths of the dragged files |
Text(), HTML()
|
Plain text, an HTML fragment |
Image() |
A decoded PNG (image.Image, ok) |
GetData(mime), Get(mime)
|
A custom type, as a string or as bytes |
DropEffect starts as the effect the platform's convention picks from what the
source allows and the modifier keys held (Ctrl copies, Shift moves, Ctrl+Shift
links, else copy); a handler that accepts may change it in OnDragOver, and set
it to tela.DropNone in OnDrop to tell the source the data was not taken.
type DropZone struct{ OnFiles func([]string) }
func (d DropZone) Build() tela.Node {
depth, setDepth := tela.UseState(0) // enters minus leaves
files := func(dt *tela.DataTransfer) bool { return dt.Has("Files") }
border, label := "border-border contrast-more:border-foreground", "Drop files here"
if depth > 0 {
border, label = "border-primary bg-primary/5", "Release to add them"
}
return Div(Class("rounded-lg border-2 border-dashed p-8 text-center", border),
OnDragEnter(func(e tela.DragEvent) {
if files(e.DataTransfer) {
setDepth.Update(func(n int) int { return n + 1 })
}
}),
OnDragOver(func(e tela.DragEvent) {
if files(e.DataTransfer) {
e.PreventDefault() // accept: the drop comes here
}
}),
OnDragLeave(func(e tela.DragEvent) {
if files(e.DataTransfer) {
setDepth.Update(func(n int) int { return max(n-1, 0) })
}
}),
OnDrop(func(e tela.DragEvent) {
setDepth.Set(0)
d.OnFiles(e.DataTransfer.Files())
}),
Text(label))
}Why a counter? OnDragEnter and OnDragLeave follow HTML: they fire when the
element under the drag changes, the enter of the new element before the leave
of the old one, and they bubble. When the drag moves from the zone onto a child
of the zone, the zone gets enter (from the child) and then leave (from the
zone itself). A boolean would flicker off; counting enters and leaving at zero is
the standard fix. A drag that is canceled (Escape) or leaves the window also ends
with a leave. examples/notes wraps the counter in a hook, useDrop, that you
can copy.
An element with Draggable(true) is a source.
When a press on it becomes a drag, its OnDragStart gets an empty
DataTransfer to fill. If the handler puts data in it, Tela starts a real drag:
the OS carries the data under the pointer, with a picture of the element (not on X11, which shows none), until
the user drops it.
Div(Draggable(true), Class("cursor-grab rounded-md border p-3"),
OnDragStart(func(e tela.DragEvent) {
e.DataTransfer.SetData("application/x-card", card.ID) // for your own targets
e.DataTransfer.SetText(card.Title) // for any other application
e.DataTransfer.EffectAllowed = tela.DropMove
}),
OnDragEnd(func(e tela.DragEvent) {
if e.DataTransfer.DropEffect == tela.DropMove {
remove(card.ID) // the target took it: the source deletes its copy
}
}),
Text(card.Title))Put the best representation first and add simpler ones after it: a target picks
the one it understands (SetHTML then SetText; SetFiles(paths...) for a file
manager; SetImage for a picture; SetData(mime, s) for your own types, which
only your own targets will read). SetDragImage shows something other than the
element. The source's OnDragEnd runs once, when the drag is over, and
DropEffect says what happened (DropNone if it was canceled or refused).
While a real drag runs the pointer belongs to the OS: OnDrag does not run, and
the window's hover and :active states end. A handler that puts no data in the
DataTransfer, or calls PreventDefault(), keeps the press as the ordinary
drag gesture, so one element can be draggable in two ways.
- Files from a file manager arrive as paths; the files may be on a share or a removable disk, so open them off the UI thread if they can be large.
- A window under a modal child refuses drags.
-
OnDropneeds noPreventDefault; itsDropEffectis the outcome. - Before the drop the getters (
Text(),Files(),GetData...) return nothing. The drop reads the well-known types and the custom ones your handlers looked up before it, so check a custom type withHas("application/x-card")inOnDragEnterorOnDragOver.
The clipboard carries the same tela.DataTransfer as a drag: text, HTML, PNG
images, file lists and custom MIME types. Text fields keep their own text paste;
this is for everything else (pasting an image into a canvas, copying a card).
// Copy: every representation at once; another application pastes the one it understands.
func CopyCard(c Card) error {
var dt tela.DataTransfer
dt.SetData("application/x-card", c.ID)
dt.SetText(c.Title)
return desktop.WriteClipboard(&dt)
}
// Paste: done runs later, on the UI thread, so it may set state.
func Paste(attach func([]string), insert func(string)) error {
return desktop.ReadClipboard(func(dt *tela.DataTransfer, err error) {
switch {
case err != nil:
case len(dt.Files()) > 0:
attach(dt.Files())
default:
insert(dt.Text())
}
})
}ReadClipboard(done, types...) reads
the types you name, or with none the well-known ones (text, HTML, a PNG image,
files); an empty clipboard gives an empty DataTransfer, not an error.
ClipboardTypes calls back with the
types that are on it, without reading the data. WriteClipboard
replaces the whole content; a nil DataTransfer empties it.
ReadClipboardText and WriteClipboardText are the short forms for text, and the
fallback where a desktop's clipboard carries only text (ErrUnsupported).
desktop.GlobalShortcut is a
component that registers a system-wide shortcut while it is mounted: OnPress
runs when the user presses the keys anywhere, even while another application
has the focus. It renders nothing where you put it.
type Quick struct{}
func (Quick) Build() tela.Node {
win := tela.UseWindow()
hint, setHint := tela.UseState("")
return Div(
desktop.GlobalShortcut{
ID: "quick-note", // stable: the OS remembers the user's changes to it
Keys: "Ctrl+Alt+N",
Description: "Create a note", // shown in the desktop's shortcut settings
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"
}
},
},
Text(hint),
)
}The OS has the last word, so a shortcut is a request, not a fact. OnStatus
reports the outcome of the registration and every later change:
| Status | Meaning |
|---|---|
ShortcutBound |
It works. Trigger is what the user presses, which may not be Keys; "" means the desktop assigned no keys yet (Hyprland, below) |
ShortcutTaken |
Another application holds the combination |
ShortcutRefused |
The user or the desktop declined to bind it |
ShortcutRevoked |
The user removed it later, in the desktop's shortcut settings |
ShortcutFailed |
It could not be registered: bad keys, an OS error, no global shortcuts on this desktop (Err wraps ErrUnsupported), or a Linux desktop that does not know the app (Err wraps ErrAppNotRegistered, below) |
Show the state in your preferences ("Quick note: Ctrl+Alt+N", or the reason it is
off) and never assume the shortcut works. A nil OnStatus reports anything but
ShortcutBound as a dev warning. Keys are written Ctrl+Alt+N, Super+F5:
check ones a user typed with desktop.ParseKeys,
which returns the canonical spelling or an error. OnRelease reports the release
where the OS does (not on Windows).
The component re-registers only when ID, Keys or Description change, so
rebuilding it is free. Several can be mounted at once; if two mounted components
use the same ID, the latest wins. To let the user turn it off, render it
conditionally: If(enabled, desktop.GlobalShortcut{...}).
On Wayland, a compositor that implements the GlobalShortcuts portal asks the user
the first time, and the user may bind other keys: that is why Trigger exists.
Compositors without it (or sessions without a portal) report
ShortcutFailed with an ErrUnsupported error: offer the same action in the app
and say that the global shortcut is not available.
Hyprland registers the shortcut but assigns no keys: it ignores Keys, and
OnStatus gets ShortcutBound with an empty Trigger. The user binds it in
hyprland.conf, after which it fires as usual:
bind = CTRL ALT, N, global, com.example.notes:quick-note
The part after global, is the app ID (desktop.AppID),
a colon and the shortcut's ID. Show the keys you asked for and that line
(examples/notes does, in Preferences).
The Linux portal needs the app's desktop entry. xdg-desktop-portal identifies
an app by its ID, and accepts it only when the desktop has an entry
<AppID>.desktop; without one every shortcut fails with an error wrapping
desktop.ErrAppNotRegistered, which names the entry. Give the app an ID
(tela.App.AppID or desktop.SetAppID; the executable's name is the default)
and call desktop.RegisterApp at start,
in the built program, before tela.Run:
func main() {
desktop.SetAppID("com.example.notes")
if _, err := desktop.RegisterApp(); err != nil && !errors.Is(err, desktop.ErrDevExecutable) {
log.Print(err)
}
tela.Main(App)
}RegisterApp writes only the app's own entry, per user
(~/.local/share/applications/<AppID>.desktop), with no launcher in the menus
(NoDisplay=true) and no file types. It looks first: it writes nothing when the
entry is up to date, or when a package already installed an entry of that ID,
which it never takes over without desktop.Replace(). Scheme and file-type
registration (below) share the file; the entry stays while
either needs it. UnregisterApp
removes it (keeping it for the associations that still need it), and
AppStatus reports it. Windows needs no
entry, and nothing is written there. On X11, a refused ID makes Tela grab the keys
itself instead, so shortcuts work without the entry.
A second start of a document app should open the file in the running copy, and a
link clicked in a browser should reach it too.
desktop.SingleInstance(app) makes
the app a single-instance app, keyed by its AppID (so give it one):
var App = desktop.SingleInstance(desktop.PersistApp(tela.App{
AppID: "com.example.notes", Title: "Notes", Root: Notes{},
}))A second process hands its arguments, working directory and activation token to
the first, then exits with status 0 without opening a window (tela.Run returns
nil at once). The first process raises its main window: shown if it was hidden in
the tray, and activated with the token so the compositor lets it come to the
front. The claim is made after the backend exists and before the first window, so
the second process never flashes a window.
The running app learns about the launch with the hook
desktop.UseLaunch:
desktop.UseLaunch(func(l desktop.Launch) {
for _, u := range l.URLs() { // notes:open?id=3, https://...
openLink(u)
}
for _, p := range l.Files() { // absolute; relative paths use the launcher's directory
openNote(p)
}
})The handler gets the process's own start once, and every forwarded start after
that (l.Forwarded tells them apart). So the same code handles
notes todo.md typed into a terminal whether or not the app is running. Files()
and URLs() split the arguments: flags are neither, file: URIs (which Linux
launchers pass) count as files, and anything else with a scheme counts as a URL.
Anything else in l.Args is yours to parse.
UseLaunch is a hook: call it at the top of a component that is always mounted
(the root). The start is given to the handlers mounted with the first one, not to
those that mount later, so a screen that mounts again does not reopen the files.
When the desktop cannot tell instances apart (no D-Bus session on Linux), the app
simply runs on its own, with a dev warning that says why.
To receive notes:open?id=3 links or .note files from the OS, the app must be
registered as a handler. desktop.Register
does it at run time, with no installer and no administrator rights:
// RegisterLinks makes the app a candidate for notes: links.
// It does file or registry I/O: call it at start, off the UI thread.
func RegisterLinks() (desktop.Registration, error) {
scheme := desktop.URLScheme("notes")
if _, err := desktop.Register(scheme); err != nil {
return desktop.Registration{}, err
}
return desktop.RegistrationStatus(scheme)
}
var noteFiles = desktop.FileType(".note", "application/x-notes-note", "Notes document")What registering does, and does not do, follows three rules the API enforces:
-
It is per user. Linux: the app gets its own
.desktopfile in~/.local/share/applications(and a MIME type for a new extension). Windows: keys underHKEY_CURRENT_USER\Software\Classes. Nothing needs administrator rights. -
It makes the app a candidate, not the default. The app appears in "Open
with", and a
notes:link opens it when no other application is the default. Becoming the default is a separate request,desktop.MakeDefault, for when the user asks ("Make Notes the default app"). On Windows the user must choose the default for a file type themselves, soMakeDefaultopens Settings' Default apps page and returnsDefaultAsked: tell the user to pick the app there. -
It never takes what another application holds. If another application
owns the scheme, or its
.desktopentry of the same ID was not written by Tela,Registerreturns an error wrappingdesktop.ErrAssociationTakenand writes nothing. Passdesktop.Replace()only after asking the user.
Register looks before it writes: if the app is registered already it changes
nothing and returns changed == false, so calling it at every start is fine.
Unregister removes the app's
registration, and
RegistrationStatus reports
whether the app is registered, whether it is the default and, in Other, what
holds the association instead. Show that in your preferences, with a Register and
an Unregister button and, when the app is registered but not the default, a "Make
default" one.
The functions work without a running app, so an installer or a setup
subcommand can call them with
desktop.ForApp(desktop.HandlerApp{ID, Name, Exec, Args, Icon})
to register the program it installed. By default the registered program is the
running executable, which is the right answer for a built app and the wrong one
for a temporary build: under go run register from the built program instead,
and under tela dev see below.
When the OS opens the app for a link or a file, it starts it with the link or the
path as an argument; UseLaunch is how the app reads
it, with SingleInstance for the running copy.
desktop.SetSecret,
Secret and
DeleteSecret keep small secrets
(passwords, tokens, encryption keys) in the OS's secure storage, named and scoped
to the app ID: the Credential Manager on Windows, the Secret Service (GNOME
Keyring, KWallet) on Linux. Never keep them in a plain file or in
desktop.UseSetting.
These calls block: the OS may show a prompt to unlock the keyring and wait for
the user. They are safe from any goroutine, so call them from a goroutine, never
from a handler, an effect or a build; dev mode warns when one runs on the UI
thread. Set state from the goroutine with a Setter, which is safe there:
func (Vault) Build() tela.Node {
state, setState := tela.UseState("locked")
unlock := func() {
go func() { // the keyring may be waiting for the user
_, err := desktop.Secret("library-key")
switch {
case err == nil:
setState.Set("unlocked")
case errors.Is(err, desktop.ErrSecretNotFound):
setState.Set("no key yet") // first run: make one and SetSecret it
case errors.Is(err, desktop.ErrSecretCanceled):
setState.Set("the keyring stayed locked")
default: // desktop.ErrUnsupported: no keyring
setState.Set("no keyring")
}
}()
}
return Div(Button(OnClick(unlock), Text("Unlock")), Text(state))
}The errors tell you what to do:
| Error | What happened | Do |
|---|---|---|
ErrSecretNotFound |
Nothing is stored under that name | Make the secret (first run), or say it is gone. Do not silently make a new key that cannot open data encrypted with the old one |
ErrSecretCanceled |
The user dismissed the unlock prompt | Nothing was read or written. Ask again when the user acts; never in a loop |
ErrUnsupported |
The desktop has no secure storage (a Linux session without a keyring) | Turn the feature off, with a reason the user can read |
ErrSecretTooLarge |
The value is above desktop.MaxSecretSize
|
Store a key, not the data |
The secrets need a running app (tela.Run or telatest); in tela snapshot they
return an error. examples/notes uses them for locked notes: a random AES key
made when the first note is locked and kept as the secret library-key, with the
notes in notes.json holding only ciphertext.
desktop.MoveToTrash(path) moves a
file or folder to the user's trash (the Recycle Bin on Windows), where the user can
restore it. It never deletes: when the trash cannot take the file (a desktop
without one, a network drive, another disk than the trash), the file stays where it
is and the error wraps ErrUnsupported, so the app can say where the file still
is. A missing file wraps fs.ErrNotExist. Like the secrets it blocks: ask the user
first, then call it from a goroutine and give the outcome back with tela.Post.
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()))
}
})
}()examples/notes offers it when a note opened from a file is locked: the note is
detached from the file, and the file, which still holds the text, can go to the
Trash after the user confirms.
kit.ContextMenu and kit.Menubar are drawn by Tela, in the look of the app, and
that stays the default: one look on every platform. A menu can opt in to the OS's
own menu, where the platform has one, with no other change:
kit.ContextMenu[string]{
Native: kit.NativeOn, // NativeAuto (the zero value) follows tela.App.NativeMenus
Items: []kit.MenuItem[string]{
{Key: "copy", Label: "Copy link", Shortcut: "Ctrl+L"},
{Kind: kit.MenuSeparator},
{Key: "delete", Label: "Delete", Variant: kit.Destructive, OnSelect: onDelete},
},
Children: Div(Class("rounded-lg border p-4"), Text(title)),
}kit.NativeMode has three values: NativeAuto
follows the app, NativeOn asks for the OS's menu, and NativeOff keeps the menu
drawn. To make every menu native, set tela.App{NativeMenus: true}; a menu's own
Native wins over the app's in both directions, so one menu can stay drawn while
the rest are native.
Where the platform has no such menu, the drawn one is used without any check of
yours: Linux always draws, and a frameless Windows window has no native menu bar
(its title bar is yours). A native context menu leaves the drawn state, Open and
OnOpenChange untouched. DropdownMenu stays drawn: it hangs off a button and has
no OS counterpart.
What a native menu cannot show is dropped: icons, the destructive color, inset,
KeepOpen, classes and custom check or radio icons. Labels, shortcuts (shown as
the item's accelerator text), disabled items, separators, checkbox and radio items
(as check items) stay; a label becomes a disabled item and a group is
flattened. Choosing an item runs the same code as the drawn menu. The
accelerator text is only shown: handle the keys yourself with OnKeyDown.
Below kit, desktop.PopupMenu(win, at, items, done)
and the component desktop.MenuBar{Menus, OnError}
are the OS's menus, with desktop.MenuItem (the same type as the tray's, plus
Accelerator). They report ErrUnsupported where the platform has none, so you can
draw your own, as kit does:
err := desktop.PopupMenu(win, tela.Point{X: e.X, Y: e.Y}, items, nil)
if errors.Is(err, desktop.ErrUnsupported) {
setMenuOpen.Set(true) // draw the menu instead
}Tela follows more of the user's system settings than dark mode and reduced motion: the accent color, high contrast and the text scale. Linux reads them from the settings portal (so dark mode follows the system on Wayland and X11 too), and Windows from the registry and its change notifications. A change reaches the running app at once.
-
High contrast is the
contrast-more:variant:border-border contrast-more:border-foreground.tela.UseHighContrast()reads it in code. (contrast-less:is an error: Tela has no low-contrast setting.) -
The text scale works like a browser's default font size: 1rem is the
theme's rem (16 px) times the scale.
text-*sizes and rem-based Tailwind spacing (p-4,gap-2,w-64) and the breakpoints grow together, while px values (border,text-[13px],w-[320px]) do not. It is on by default; a layout that cannot grow (a fixed-size tool window) setstela.App{IgnoreTextScale: true}.tela.UseTextScale()returns it for sizes computed in code. -
The accent color is
tela.UseAccentColor(), which returns the color and false when the system reports none.
func (Readout) Build() tela.Node {
scale := tela.UseTextScale() // 1.25: 1rem is 20 px
accent, ok := tela.UseAccentColor()
if !ok {
accent = tela.Hex("#4f46e5")
}
return Div(Class("rounded-lg border border-border p-4 text-sm contrast-more:border-2 contrast-more:border-foreground"),
Style(tela.Bg(accent)), Textf("Text scale %.2f", scale))
}Snapshots and tests use a scale of 1, no accent and no high contrast unless set
(telatest.TextScale, AccentColor, HighContrast), so goldens are the same on
every machine. Styling lists the
variant and the units.
tela dev runs your app as a child process of a long-lived host that owns the real
windows, so every service above goes through the host. A few consequences come
from that:
-
Drags are forwarded to the active child, and the host answers the OS with
the latest answer it has, one round trip late: on Windows, a drag that just
entered is refused until the next
DragOver, on X11 the answer waits for the next motion, and aSetDropEffectmade while handling the drop does not reach the source. Your handlers are unchanged. -
Global shortcuts are held by the host, like trays. An equal re-registration is
not sent again, so the Wayland portal does not ask on every save; changes apply
when the new build is promoted, and the shortcut stays after a crash of the
child. The host registers them under your app's ID, not its own, so the app's
desktop entry must exist: run the built program once (
RegisterApp), sinceRegisterAppunder dev returnsErrDevExecutablelikeRegister. -
Single instance: the host claims the app ID and keeps it across reloads. A
second
tela devof the same app is a second instance: it forwards its launch to the first and exits, and the supervisor says why. -
Registration needs an explicit
Exec. The running program is a temporary dev build, which the OS must not be told to run:RegisterandMakeDefaultreturndesktop.ErrDevExecutableand write nothing. Give the installed program's path withdesktop.ForApp(desktop.HandlerApp{Exec: ...}), or skip registration under dev (examples/notesdoes so, quietly). -
UseLaunchdoes not get the process's own start again on a reload, only the forwarded ones. - Secrets, associations, the clipboard and menus go to the host's backend, served off its UI thread.
Linux is Wayland and X11 over the XDG Desktop Portal and the freedesktop D-Bus
services. On macOS the clipboard works with every data type; drag and drop, menus,
shortcuts, single instance, registration, secrets and the Trash are the next phase
and return ErrUnsupported.
| Linux | Windows | |
|---|---|---|
| Drag and drop | Wayland wl_data_device, X11 XDND |
OLE DoDragDrop and RegisterDragDrop
|
| Clipboard | Text, HTML, PNG images, files and custom types on Wayland and X11 | The same types |
| Global shortcuts | Wayland: the GlobalShortcuts portal, which asks the user and may bind other keys (Trigger; none on Hyprland), and needs the app's desktop entry (RegisterApp); X11: the portal, else key grabs; ShortcutFailed with ErrUnsupported without a portal |
RegisterHotKey: ShortcutTaken if another application holds the keys; no release events |
| Single instance | A well-known D-Bus name; without a D-Bus session the app runs on its own | A named mutex and a message to the first instance |
| Registration | The app's own .desktop file in ~/.local/share/applications, mimeinfo.cache kept by Tela, and a MIME type for a new extension (made with update-mime-database when it is installed); xdg-mime query default x-scheme-handler/notes shows the default |
HKCU\Software\Classes; the default for a file type is chosen by the user in Settings, Default apps |
| Secrets | The Secret Service (GNOME Keyring, KWallet) over D-Bus, with an encrypted session; ErrUnsupported without one |
Credential Manager |
| Trash | The Trash portal, else the freedesktop home trash (~/.local/share/Trash) written by Tela for files on its disk; ErrUnsupported for files elsewhere |
IFileOperation to the Recycle Bin; ErrUnsupported where Windows would delete the file instead (a network drive) |
| Native menus | Never: kit draws them | Context menus, and menu bars on windows with a title bar of the OS (not frameless) |
| Settings | The settings portal: dark mode, accent color, contrast, reduced motion, text scale | The registry, SPI_GETHIGHCONTRAST and WM_SETTINGCHANGE
|
Linux runs two freedesktop helper programs when they are installed and works
without them: xdg-open (tela.OpenURL and OpenPath without a portal) and
update-mime-database (a MIME type for a new file extension; without it, files of a
new type are recognized later, once something else runs it). No other external
program is run.
telatest plays the OS for all of this: a drag from a file manager, the user
pressing the shortcut, a second start with a link, the keyring, the registry, native
menus, the trash and the system's settings. Nothing touches the real clipboard,
keyring, registry, trash or shortcuts. See Testing for the drag helpers
and the services for the rest:
func TestDropAndShortcut(t *testing.T) {
telatest.TempDesktopDirs(t)
var dropped []string
zone := telatest.Mount(t, DropZone{OnFiles: func(f []string) { dropped = f }})
path := filepath.Join(t.TempDir(), "ideas.md")
zone.HoverDrag(telatest.ByTestID("drop"), telatest.Carrying(telatest.DragFiles(path)))
zone.ExpectText("Release to add them") // the drag-over state
if zone.DropOn(telatest.ByTestID("drop")) == tela.DropNone || len(dropped) != 1 {
t.Fatalf("the drop was refused: %v", dropped)
}
app := telatest.MountApp(t, App)
app.PressShortcut("quick-note")
app.InjectLaunch(desktop.Launch{Args: []string{"notes:open?id=3"}})
app.SetShortcutStatus("quick-note", desktop.ShortcutTaken, "")
app.ExpectText("taken by another application")
}