Building the app
Desktop APIs
Dialogs, notifications, the clipboard, the shell, displays, dark mode, power, the network status, secrets, watching files and global shortcuts.
MyGo wraps the parts of the operating system desktop apps use most, for apps whose windows show web pages, native UI or both. Like the rest of MyGo, each can be called from any goroutine.
Dialogs
mygo.Dialog shows the system's file and message dialogs. Its methods
block until the user answers: call them from bound methods, which run on
goroutines of their own, or start a goroutine for them in event listeners
and menu items, which should return quickly. Parent attaches a dialog to a
window, as a sheet on macOS.
// Pick files.
paths, err := mygo.Dialog.Open(mygo.OpenDialogOptions{
Parent: win,
Title: "Import",
Filters: []mygo.FileFilter{{Name: "Images", Extensions: []string{"png", "jpg"}}},
Multiple: true,
}) // nil when canceled
// Choose where to save.
path, err := mygo.Dialog.Save(mygo.SaveDialogOptions{
Parent: win,
DefaultPath: "notes.json",
}) // "" when canceled
// Ask a question.
res, err := mygo.Dialog.Message(mygo.MessageOptions{
Parent: win,
Type: mygo.MessageWarning,
Message: "Delete 3 notes?",
Detail: "You cannot undo this.",
Buttons: []string{"Cancel", "Delete"},
DestructiveButtons: []int{1},
CheckboxLabel: "Don't ask again",
})
if res.Button == 1 { /* Delete */ }
// Report an error.
mygo.Dialog.Error("Import failed", err.Error())OpenDialogOptions also pick directories (Directory), show hidden files
and, on macOS, a message and a New Folder button.
A filter takes files by type too, as HTML's accept: MIMETypes such as
"application/pdf", or all of a kind, "image/*", which the system's
picker matches as it knows them, rather than a list of every extension:
mygo.FileFilter{Name: "Pictures", MIMETypes: []string{"image/*"}}macOS 11 and later take them as types of files (UTType), the kinds
image/*, audio/*, video/*, text/* and font/* as theirs
(public.image and the others), which is all earlier versions take; GTK
matches them, wildcards too; Windows filters by the extensions its
registry gives the type, or every type of the kind and every file its
PerceivedType says is of it.
In message dialogs Enter activates DefaultButton and Escape
CancelButton, also when they are one button, as Cancel above. On macOS
Cmd+. cancels too, and a DefaultButton of -1 leaves Enter to no button.
CancelButton defaults to the first button labeled Cancel or No, else to
the first button. For Delete / Keep, set CancelButton: 1 to choose Keep;
otherwise Escape chooses Delete. DestructiveButtons sets apart buttons
that delete or can't be undone.
Notifications
n := mygo.NewNotification(mygo.NotificationOptions{
Title: "Export finished",
Body: "notes.json was saved to Documents.",
})
n.OnClick(func() { mainWindow().Focus() })
if err := n.Show(); err != nil {
log.Println(err)
}Close removes a notification. macOS shows notifications of packaged apps
only, which mygo dev and mygo build make; mygo.NotificationsSupported()
reports whether the app can show them. Linux shows them through the
desktop's notification service, Windows as notification-area balloons.
Show returns once the system has the notification. On macOS the first
one asks the user whether to allow notifications, and Show waits for the
answer; it returns mygo.ErrNotificationsDenied when they are not allowed,
by that answer or later in System Settings. ShowContext(ctx) stops
waiting when ctx is done, as an app quits with the question still open,
and takes away a notification that the answer lets through after that.
macOS keeps a notification in Notification Center after it is clicked,
until the app removes it: Close it in OnClick to take it away, or call
mygo.ClearNotifications(), which removes all of the app's notifications,
those of earlier runs too, as the app comes to the front:
n.OnClick(func() {
mainWindow().Focus()
n.Close()
})
// What the user has seen in the app does not wait in Notification Center.
mygo.App.OnDidBecomeActive(func() { mygo.ClearNotifications() })Linux asks the desktop's notification service to close them, which may have closed them already, and Windows hides its balloon.
A notification can outlive the run that showed it, in Notification Center
on macOS, where a click launches the app again. Give it an ID of the
app's own, which App.OnNotificationClick receives for every click, that
one included when it is registered before Run; showing a notification
with the ID of one still shown replaces it. Group gathers notifications
in one stack of Notification Center, such as the messages of a
conversation (macOS):
// IDs are "<conversation>/<message>".
mygo.App.OnNotificationClick(func(id string) {
conversation, _, _ := strings.Cut(id, "/")
openConversation(conversation)
})
mygo.NewNotification(mygo.NotificationOptions{
ID: "ada/" + msg.ID,
Group: "ada",
Title: "Ada",
Body: msg.Text,
}).Show()Image shows an image with the notification, the bytes of a PNG or JPEG,
such as the cover of the track that started playing. macOS attaches it,
as a thumbnail that grows as the notification expands; Linux gives the
desktop's notification service its pixels, scaled down to fit 256 pixels;
Windows shows it as the balloon's large icon, in place of the app's. An
image the system cannot decode leaves the notification without one.
NewNotification copies it, so the app can reuse its buffer; MyGo does
not fetch remote images:
// One notification for what plays, replaced as the track changes.
mygo.NewNotification(mygo.NotificationOptions{
ID: "now-playing",
Title: track.Title,
Body: track.Artist + " — " + track.Album,
Image: track.Cover,
Silent: true,
}).Show()Sender avatars on macOS
An incoming direct message can supply a sender to display a prominent avatar with the app's small icon on macOS 12 and later:
mygo.NewNotification(mygo.NotificationOptions{
ID: "message:7", Group: "conversation:3",
Title: "Ada", Subtitle: "Lunch", Body: "See you at noon",
Sender: &mygo.NotificationSender{
ID: "user:ada", Name: "Ada", Avatar: avatarPNG,
},
}).Show()Sender.ID identifies the person in the app; it is not inferred to be an
email address or phone number. Name defaults to Title. Avatar holds
encoded image bytes, such as PNG, JPEG or SVG; macOS rasterizes them as PNG
before donation. The app fetches them and decides
whether the message's sender and content may be shared with the system.
NewNotification copies the sender and image, so the app can reuse its
buffers. Group is the conversation identifier; without one, it uses the
notification's ID. This API represents a direct incoming message, not a
group chat or outgoing message. macOS controls the communication layout;
the subtitle is included above the message in the body.
Enable Communication Notifications for the app's Apple App ID and add
INSendMessageIntent to macos.infoPlist.NSUserActivityTypes. Sign with
com.apple.developer.usernotifications.communication and embed a matching
provisioning profile, including for Developer ID distribution: set
macos.provisioningProfile, which mygo build copies to
Contents/embedded.provisionprofile before signing (see
Provisioning profiles). The
profile must authorize the app's identifier, certificate and entitlements.
Do not add the restricted entitlement to an ad-hoc build: macOS can refuse
to launch it.
MyGo checks the signed entitlement before donating the incoming message to
SiriKit and updating its notification content. Without the capability, with
an incomplete sender or invalid avatar, or when donation/content conversion
fails, it sends the ordinary notification. Windows, Linux and unsupported
platforms retain their existing behavior. IDs, sound, grouping, click
handlers, Close and ClearNotifications use the same notification lifecycle.
Clipboard
mygo.Clipboard.WriteText("hello")
text := mygo.Clipboard.ReadText()
mygo.Clipboard.WriteHTML("<b>hello</b>")
mygo.Clipboard.WriteImage(png) // a PNG
img := mygo.Clipboard.ReadImage()
formats := mygo.Clipboard.AvailableFormats()
mygo.Clipboard.Clear()Pages can use the web's navigator.clipboard too.
Clipboard.Write(transfer.Data) and Clipboard.Read(formats...) share the
native drag/drop model: ordered items, alternative representations, file
lists, custom MIME formats, and lazy providers. Formats discovers portable
names without requesting bytes; ReadFormat, ReadFiles, WriteFiles, and
Flush provide explicit negotiation, file transfer, and persistence. See
Clipboard and drag data for provider lifetimes and native
format mappings. The clipboard example demonstrates
these operations.
The shell
mygo.Shell.OpenExternal("https://example.com") // the default browser
mygo.Shell.OpenPath("/Users/ada/report.pdf") // the default app for the file
mygo.Shell.ShowItemInFolder(path) // Finder, Explorer, the file manager
mygo.Shell.TrashItem(path) // to the trash
mygo.Shell.PreviewItem(path) // Quick Look, as the space bar does
mygo.Shell.Beep()TrashItem never deletes for good: when the item cannot go to the trash
it fails and leaves the item where it is. On Windows that is an item too
big for the recycle bin, or on a drive without one, as network shares and
many removable drives are, which Windows would otherwise delete without
asking.
PreviewItem shows a file in the Quick Look panel, the preview Finder
opens with the space bar: the panel stays open until the user closes it,
and asking again replaces the item it shows. It is macOS only; other
platforms return ErrUnsupported. The panel is not modal: the app keeps
running and the call returns once the item is set.
FileIcon returns the icon the file manager shows for a file, a folder
or an application, as a PNG of the size asked for in pixels: pass the size
in DIPs times the display's scale factor.
png, err := mygo.Shell.FileIcon("/Applications/Safari.app", 64)On macOS it is Finder's icon (NSWorkspace), drawn from the
representation that fits the size best; a symbolic link has the icon of
what it points to, without Finder's arrow. On Windows it is Explorer's
(IShellItemImageFactory, icons only, no thumbnails), and the path may
also name a shell item, such as an app of the Start menu by
shell:AppsFolder\<AppUserModelID>. On Linux it is the icon of the file's
content type from the icon theme, and for a desktop entry (.desktop), the
icon of its application, or where the theme has none, that of desktop
entries.
TypeIcon returns the icon of a file type, by its extension, with or
without the dot, without a file of that type on disk: the icon of an
attachment not yet downloaded, say. It is drawn as FileIcon draws its
icon, and fails in browsers.
png, err := mygo.Shell.TypeIcon("pdf", 64)InstalledApps lists the applications installed on the computer, sorted
by name, as a launcher shows them, and LaunchApp starts one, or brings
it to the front:
go func() {
apps, err := mygo.Shell.InstalledApps() // reads the disk: off the main thread
if err != nil {
return
}
for _, app := range apps {
fmt.Println(app.Name, app.ID)
}
icon, _ := mygo.Shell.FileIcon(apps[0].Path, 64)
_ = mygo.Shell.LaunchApp(apps[0])
}()Each app has the Name the system shows, in the user's language, an
ID and the Path whose icon FileIcon draws:
| the apps | ID |
Path |
|
|---|---|---|---|
| macOS | the .app bundles of /Applications, /System/Applications, ~/Applications and CoreServices' Applications, and of their folders (Utilities), plus Finder; an app behind symbolic links once, at the link, as /Applications/Safari.app |
the bundle identifier, or the path for a bundle without one | the bundle |
| Windows | the Start menu's All apps, desktop and packaged apps alike (the shell's Applications folder) | the AppUserModelID, or for a desktop app without one, its path under a known folder | shell:AppsFolder\<ID> |
| Linux | the desktop entries menus show (not NoDisplay, Hidden, or meant for another desktop) |
the desktop file ID, such as org.gnome.Nautilus.desktop |
the desktop entry |
InstalledApps does not block the main thread when called from another
goroutine, and returns in tens of milliseconds. Drawing icons takes a few
milliseconds each, on the main thread: draw those that show, and keep
them.
Displays
mygo.Screen describes the connected displays, in device-independent
pixels with the origin at the top-left corner of the primary display:
primary := mygo.Screen.PrimaryDisplay()
fmt.Println(primary.Bounds, primary.WorkArea, primary.ScaleFactor)
for _, d := range mygo.Screen.Displays() { /* ... */ }
cursor := mygo.Screen.CursorScreenPoint()
d := mygo.Screen.DisplayNearestPoint(cursor)
mygo.Screen.OnDisplaysChanged(func() { /* rearrange windows */ })WorkArea leaves out the menu bar, the Dock and taskbars.
On Windows, displays can have different scales. Each display's
coordinates start at its top-left corner in pixels divided by the
smallest scale of the displays, and go on in its own scale, so that
displays never overlap: one with a larger scale than another's leaves a
gap after it. Where every display has the same scale, coordinates are
the pixels divided by that scale. A window's bounds are in the scale of
the display it is the most on.
DisplayMatching(rect) returns the display a rectangle overlaps most, the
one a window is on.
Dark mode
mygo.Theme is the light or dark appearance, which pages follow through
the prefers-color-scheme media query:
dark := mygo.Theme.IsDark()
mygo.Theme.OnUpdated(func() { log.Println("dark:", mygo.Theme.IsDark()) })
mygo.Theme.SetSource(mygo.ThemeDark) // or ThemeLight, or ThemeSystem to follow the systemSetSource may be called before App.Run, for example with an appearance
the user saved in the app's preferences: the app then starts in it.
Accent color and accessibility
mygo.Theme.Preferences() returns the accent color of the desktop's own
controls and the accessibility settings they follow, all read at once:
p := mygo.Theme.Preferences()
p.Accent // a color.NRGBA (image/color), A 0 where the desktop has none
p.ReduceMotion // the desktop asks for less motion
p.ReduceTransparency // the desktop asks for opaque surfaces, e.g. no vibrancy
p.HighContrast // the desktop asks for more contrast
p.TextScale // how many times larger text should be; 1 is the usual sizeThe accent is 8-bit sRGB with straight alpha; ui.RGB(p.Accent.R, p.Accent.G, p.Accent.B)
makes it a ui.Color for drawing of your own.
ReduceMotion is macOS's Reduce Motion, Windows's animation effects and
GNOME's animations turned off; ReduceTransparency macOS's Reduce
Transparency and Windows's transparency effects turned off, which Linux
desktops do not have, so it is false there; HighContrast macOS's
Increase Contrast, Windows's contrast themes and the higher contrast of
the settings portal (or a HighContrast GTK theme) on Linux. macOS has no
text scale, so TextScale is 1 there. The accent is missing on Linux
desktops whose settings portal has none, and in browsers.
Theme.OnUpdated fires when the desktop changes any of them, as
SetSource does. Reading them can call out to the desktop (the settings
portal on Linux), so MyGo reads them once and keeps them until the next
change. Package ui's default theme already follows the accent, the
contrast and the text scale, and Animate reduced motion. Like the other
calls that need the running app, Preferences panics in main before
App.Run, and another goroutine calling it waits for the app to start.
Pages get them from document start on, as preferences() of
mygo-runtime and the --mygo-accent and --mygo-text-scale CSS
variables, see the frontend guide;
prefers-reduced-motion and prefers-contrast already follow the desktop.
A ThemePreferences sent through an event or returned by a bound method
is that same JSON, and the generated client types it with mygo-runtime's
ThemePreferences.
Power
// Keep the computer awake during a long task, and the display on with true.
release := mygo.Power.KeepAwake("Exporting the video", false)
defer release()
// Keep the app at full speed while none of its windows shows.
stop := mygo.Power.KeepRunning("Serving the phones")
defer stop()
mygo.Power.OnSuspend(func() { pauseSync() })
mygo.Power.OnResume(func() { resumeSync() })
mygo.Power.OnLockScreen(func() { hideSecrets() })
mygo.Power.OnUnlockScreen(func() { /* ... */ })
onBattery := mygo.Power.IsOnBattery()
idle := mygo.Power.IdleTime() // since the last keyboard or mouse inputThe reason given to KeepAwake shows where the system lists what keeps it
awake.
macOS puts an app none of whose windows shows, hidden, minimized, covered
or behind the locked screen, into App Nap within a minute: it lowers the
app's priority, coalesces its timers and throttles its I/O, which slows an
app that serves the network, streams or syncs in the background. A tray
icon counts as a window that shows while the menu bar shows it.
KeepRunning keeps App Nap off until its release, and lets the computer
sleep when idle, as an app that stays in the menu bar all day should;
KeepAwake keeps App Nap off too, but the computer awake as well. Other
systems do nothing for KeepRunning.
On Linux, OnLockScreen is called when the screen saver starts,
which usually locks the screen, and IdleTime is 0 on desktops that do not
report it.
Now Playing and media keys
mygo.NowPlaying shows what the app plays where the system shows media:
Now Playing in Control Center and the menu bar on macOS, the media
controls of the volume flyout and the lock screen on Windows, and on Linux
the MPRIS clients, as GNOME's and KDE's media controls and playerctl.
While it shows something, the media keys of keyboards and headsets, and
those controls, send the app commands:
mygo.NowPlaying.Set(mygo.NowPlayingInfo{
Title: "Spring", Artist: "Vivaldi", Album: "The Four Seasons",
Artwork: cover, // PNG or JPEG
Duration: 3*time.Minute + 20*time.Second,
Commands: mygo.MediaPlay | mygo.MediaPause | mygo.MediaNext | mygo.MediaPrevious | mygo.MediaSeek,
})
mygo.NowPlaying.OnCommand(func(cmd mygo.MediaCommand, pos time.Duration) {
switch cmd {
case mygo.MediaPause:
player.Pause()
mygo.NowPlaying.SetPlayback(mygo.PlaybackPaused, pos) // where playback is
case mygo.MediaSeek:
player.Seek(pos)
mygo.NowPlaying.SetPlayback(mygo.PlaybackPlaying, pos)
case mygo.MediaNext:
playNext() // and Set the next track's info
}
})
mygo.NowPlaying.Clear() // the app no longer shows in the controlsThe system advances the position while the state is PlaybackPlaying, so
the app sets it again only when playback jumps. The play/pause key sends
MediaPause while the state is playing and MediaPlay otherwise, and
only the commands the info offers reach OnCommand (Commands 0 offers
play, pause and stop). pos is where MediaSeek moves to, and for the
other commands where playback is as they come. An app playing a player of package audio can let
Control play, pause, stop and seek it, and show its state:
release := mygo.NowPlaying.Control(music) // an *audio.Player
defer release()Control hears the player's end; after the app plays, pauses or seeks it
itself, NowPlaying.Sync shows it. In browsers, NowPlaying is the
page's Media Session.
Network
mygo.Network tells whether the computer is online and whether the
connection is metered — a phone hotspot or a plan that counts the data,
where an app should hold off on large downloads:
status := mygo.Network.Status()
if status.Online && !status.Metered {
syncNow()
}
mygo.Network.OnChanged(func(s mygo.NetworkStatus) {
log.Printf("online: %v, metered: %v", s.Online, s.Metered)
})Online means the system has a connection to a network beyond the
computer, as the browsers' navigator.onLine does, which does not prove
that a server answers: a network behind a captive portal, or one whose
way to the internet is down, counts. macOS and Linux count a route out
(a satisfied path, GLib's network-available), Windows a connectivity
level of local access or more. Metered is an expensive path (cellular, a
phone's hotspot) or Low Data Mode on macOS, a metered connection on Linux,
and on Windows a connection whose cost is fixed or variable, or that
roams, is over its data limit or approaches it. In browsers Online is
navigator.onLine, and Metered is false.
Status is safe from any goroutine; on macOS the first call waits for
the system's first answer, which comes within milliseconds. Changes are
watched from the first OnChanged on, whose listener runs on the main
thread, only when the status changed; it returns a function that removes
the listener.
Pages already have navigator.onLine. To show the native status in the
UI, broadcast a typed event from the listener, so the generated client
receives it:
// Connectivity is pushed to every window when the connection changes, as
// {online, metered}.
var Connectivity = mygo.NewEvent[mygo.NetworkStatus]("connectivity")
func init() {
mygo.Network.OnChanged(func(s mygo.NetworkStatus) { Connectivity.Broadcast(s) })
}Proxies
Web pages reach the network through the webview, which follows the proxy the user set in the system's settings, with its bypass list and setup script (PAC file): WebView2 as Chromium does, from Windows' proxy settings; WKWebView from macOS's; WebKitGTK through GIO's default proxy resolver, which reads GNOME's settings, or KDE's through libproxy.
Go's http.DefaultClient reads only HTTP_PROXY, HTTPS_PROXY and
NO_PROXY. Package netproxy makes Go's requests follow the system's
settings as the pages do, and MyGo's own requests use it: those of the
updater and, unless their options name a client, of the
fetch and websocket plugins.
Your own clients opt in with its transport:
import "github.com/egoist/mygo/netproxy"
client := &http.Client{Transport: netproxy.Transport(), Timeout: time.Minute}or, for a transport of your own, t.Proxy = netproxy.Proxy. For each
request:
- Loopback hosts (
localhost,*.localhost,127.0.0.0/8,::1) are reached directly. - When
HTTP_PROXY,HTTPS_PROXY,ALL_PROXYorNO_PROXY(or its lowercase name) is set, the environment decides, ashttp.ProxyFromEnvironmentdoes, so that a proxy set for one run of the app wins. - Otherwise the system's settings for the URL decide: on Windows the user's, read by WinHTTP, with automatic detection (WPAD) and setup scripts; on macOS those of System Settings > Network > Proxies, read by CFNetwork, with setup scripts; on Linux GIO's default proxy resolver, as WebKitGTK. Without an answer, requests go direct.
The system's answer for a scheme and host is kept for 5 seconds, so turning the proxy on or off, or changing it, applies to the requests that follow while the app runs. Requests under way, such as a download or a stream, keep their connection. Go's transport speaks to HTTP and HTTPS proxies and SOCKS5, not SOCKS4; it sends a proxy's password only when the system's answer holds it, as GNOME's settings can. In browsers, the browser makes Go's requests, through its own proxy.
Secrets
mygo.Secrets keeps small secrets, such as access tokens and API keys, in
the system's credential store, which encrypts them for the user, unlike a
file in the user data directory: the login Keychain on macOS, the Secret
Service on Linux (GNOME Keyring, KWallet, KeePassXC…, through libsecret)
and the Credential Manager on Windows.
if err := mygo.Secrets.Set("github-token", []byte(token)); err != nil {
log.Println(err)
}
token, err := mygo.Secrets.Get("github-token")
if errors.Is(err, mygo.ErrSecretNotFound) {
// Ask the user to sign in.
}
mygo.Secrets.Delete("github-token") // on sign out; nil when there is noneA secret may be at most mygo.MaxSecretSize (2560) bytes, the most the
Credential Manager keeps, and a key at most 256 bytes. To keep more,
encrypt it into a file in the user data directory with a key kept in
Secrets. The methods are safe from any goroutine, also before Run, and
may block while the system asks the user something, e.g. to unlock the
keyring; on the main thread the app keeps handling events meanwhile. To
use them from the page, bind a backend method that calls them, so that
only what the page needs crosses into it.
A store keeps a secret under a service and an account: Secrets takes the
key for the account and the app's identifier (identifier of
mygo.config.ts) for the service. The development app of mygo dev is
<identifier>.dev, so it keeps its secrets apart from the installed app's;
an app run without an identifier, as with go run, uses its name.
Secrets.Service chooses another service, to share secrets with another
program of the app, such as its command line tool, or to read those an
Electron app stored with keytar, which keeps them as Secrets does under
the service the app passed it:
// keytar.getPassword("MyApp", "refresh-token") in the Electron app
token, err := mygo.Secrets.Service("MyApp").Get("refresh-token")Secrets protect against other users and stolen disks, not against other
apps of the same user: macOS asks the user before another app reads them,
and before a rebuilt app does when it is not signed with the same
identity, while Linux and Windows let them read. mygo dev signs every
build with the user's Apple Development identity, so "Always Allow" lasts
across rebuilds; apps signed ad hoc, as without one, with -sign - or with
go run, are asked again after each build.
Where there is no credential store, the methods fail with an error for
which errors.Is(err, errors.ErrUnsupported) holds: in browsers, and on
Linux without libsecret (the libsecret-1-0 package on Debian and Ubuntu),
a session bus or a Secret Service on it, as on servers and minimal
desktops. An app can then fall back, e.g. ask for the secret each time:
token, err := mygo.Secrets.Get("github-token")
switch {
case errors.Is(err, errors.ErrUnsupported):
// No credential store: ask the user, keep it in memory.
case errors.Is(err, mygo.ErrSecretNotFound):
// Ask the user to sign in.
}Watching files
mygo.WatchFiles watches a file or a directory, and with Recursive the
directories in it, for changes. It returns once everything is watched, and
Next waits for the next changes and returns them in a batch; use it on a
goroutine of its own:
w, err := mygo.WatchFiles(ctx, dir, mygo.FileWatchOptions{
Recursive: true,
Ignore: func(p string, isDir bool) bool {
return isDir && (path.Base(p) == "node_modules" || path.Base(p) == ".git")
},
})
if err != nil {
return err
}
go func() {
defer w.Close()
for {
events, err := w.Next(ctx)
if err != nil {
return // io.EOF once closed, or once dir went away
}
for _, e := range events {
log.Println(e.Op, e.Path)
}
}
}()An event's Path is relative to what is watched (w.Root(), its path
with symlinks resolved) and separated by slashes, . for itself, and Op
is mygo.FileCreate, FileWrite, FileRemove or FileRename, with
OldPath where a rename moved from. What is already there is not
reported. Changes wait for 50 ms of quiet (Debounce, right away when
negative) before they are reported, and never more than 500 ms
(MaxDelay). A watch compares what it sees with what it saw, so events say
what is different, not every step that made it so: treat them as hints to
read the files again. A write is a file whose size or modification time
changed, or that the system says was written, or a file replaced at its
path by another, as editors and atomic saves replace it (a temporary file
renamed over it): permissions and other metadata, which indexers and
antivirus touch, are not reported, nor are directories written, whose
changes are their entries'.
A watch doesn't look again until Next has taken the last batch: when the
app is busy, what changed meanwhile comes in the next batch, so nothing is
lost and nothing piles up. What the watch can't look at, such as a
directory without permission, is left out rather than ending it.
Ignore leaves out files and directories by
their path: a directory it ignores is never opened, and doesn't count in
MaxEntries.
A file is watched through its directory, as fsnotify
recommends: a file replaced, as editors save, is a write of ., one
removed a removal, and one that appears again a creation. When the
directory itself, or the directory of the file, is removed, renamed or
replaced, the last batch has a FileRemove of ., and Next then returns
io.EOF.
Close, or canceling the context given to WatchFiles, stops the watch:
Next returns io.EOF, and Done is closed once it has released its
handles. The app stops its watches when it quits. A watch stops with
mygo.ErrWatchLimit when its directory has more than MaxEntries files
and directories: 65536 on macOS, where one FSEvents stream covers the
whole tree, and 8192 elsewhere, where a backend may take a handle per
entry. The watch plugin gives
pages watches of the directories and files an app chooses, and says how
each platform watches.
HintsOnly trades all of that precision for cost. The watch keeps no
snapshot and stats no file: it reports only mygo.FileHint events, each
naming a directory in or under which the system said something changed,
and . after the system lost changes. Which file changed, how, and whether
it was renamed are never known. It suits a dev reloader, which rebuilds on
any change:
w, err := mygo.WatchFiles(ctx, dir, mygo.FileWatchOptions{
Recursive: true,
HintsOnly: true, // only "something under this directory changed"
Ignore: func(p string, isDir bool) bool {
return isDir && path.Base(p) == "node_modules"
},
})Never use it where the file matters: following a document, syncing, or
anything that would act on a path it can't know. With it, Ignore is asked
about directories only (a change to a file it would ignore still hints at
the file's directory), and MaxEntries counts the directories enrolled. On
macOS and Windows a recursive hints-only watch enrolls only its directory,
which the system watches whole; on Linux it enrolls every directory,
reading their entries' types without stat'ing them. Symlinks are still not
followed, nothing outside the directory is watched, and the watched
directory going away still ends the watch with a FileRemove of ..
Global shortcuts
Global shortcuts work while the app is in the background, for example to bring it to the front:
err := mygo.GlobalShortcut.Register("CmdOrCtrl+Shift+Space", func() {
win.Show()
win.Focus()
})The function runs on the main thread. Shortcuts use the syntax of
accelerators; Unregister, UnregisterAll and
IsRegistered manage them. Register fails when the shortcut is taken,
by the app or, where the system tells, by another app.
On Linux under X11 the app grabs the keys itself. Under Wayland, where apps
cannot, the desktop binds shortcuts for the app through the XDG desktop
portal, which KDE Plasma 6 and GNOME 48 and later provide: the desktop may
ask the user to confirm new shortcuts and lets them pick other keys, and it
needs the app installed, since it identifies apps by their desktop entry.
The shortcut works shortly after Register returns. Windows the function
shows or focuses take the focus with the key press's activation token where
the desktop provides one, as GNOME does.