外观
桌面 API
MyGo 封装了桌面应用最常使用的操作系统部分,适用于窗口显示网页、原生 UI 或两者兼有的应用。与 MyGo 的其他部分一样,每个 API 都可以在任意 goroutine 中调用。
对话框
mygo.Dialog 显示系统的文件和消息对话框。它的方法会阻塞直到用户作出应答:请在绑定的方法中调用它们(它们运行在自己的 goroutine 上),或者在事件监听器和菜单项中为它们启动一个 goroutine——后者应当尽快返回。Parent 将对话框附加到某个窗口上,在 macOS 上以 sheet 形式显示。
go
// 选择文件。
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
// 选择保存位置。
path, err := mygo.Dialog.Save(mygo.SaveDialogOptions{
Parent: win,
DefaultPath: "notes.json",
}) // "" when canceled
// 询问一个问题。
res, err := mygo.Dialog.Message(mygo.MessageOptions{
Parent: win,
Type: mygo.MessageWarning,
Message: "Delete 3 notes?",
Detail: "You cannot undo this.",
Buttons: []string{"Delete", "Cancel"},
CheckboxLabel: "Don't ask again",
})
if res.Button == 0 { /* Delete */ }
// 报告一个错误。
mygo.Dialog.Error("Import failed", err.Error())OpenDialogOptions 还可以选择目录(Directory)、显示隐藏文件,并在 macOS 上显示一条消息和一个"新建文件夹"按钮。在消息对话框中,Enter 激活 DefaultButton,Escape 激活 CancelButton,其默认值是第一个标签为 Cancel 或 No 的按钮。
通知
go
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 会移除一条通知。macOS 只为打包后的应用显示通知,也就是 mygo dev 和 mygo build 生成的应用;mygo.NotificationsSupported() 用于报告应用能否显示通知。Linux 通过桌面的通知服务显示,Windows 则以通知区域的弹出气泡显示。Show 在系统接收到通知后即返回。在 macOS 上,第一条通知会询问用户是否允许通知,Show 会等待答复;当通知不被允许时——无论是通过该答复,还是之后在"系统设置"中设置的——它返回 mygo.ErrNotificationsDenied。
macOS 会把通知保留在通知中心里,即使用户点击之后仍然如此,直到应用将其移除:在 OnClick 中调用 Close 把它带走,或者调用 mygo.ClearNotifications()——它会移除该应用的全部通知,包括之前几次运行的——在应用来到前台时:
go
n.OnClick(func() {
mainWindow().Focus()
n.Close()
})
// 用户已在应用中看到的通知不再停留在通知中心。
mygo.App.OnDidBecomeActive(func() { mygo.ClearNotifications() })Linux 会请求桌面的通知服务关闭它们,而该服务可能早已关闭;Windows 则隐藏它的气泡。
一条通知可能比显示它的那次运行存在得更久:在 macOS 的通知中心里,点击它会再次启动应用。为它指定一个应用自己的 ID,每次点击都会由 App.OnNotificationClick 接收——只要它在 Run 之前注册,启动应用的那次点击也包括在内;显示的 ID 与仍在展示的通知相同的消息会将其替换。Group 把通知归入通知中心的一个堆叠组,例如某个会话的消息(macOS):
go
// ID 的格式为 "<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()macOS 上的发送者头像
在 macOS 12 及更高版本上,收到的私聊消息可以提供一个发送者,以在应用小图标旁显示醒目的头像:
go
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 在应用内标识这个人;它不会被推断为电子邮件地址或电话号码。Name 默认为 Title。Avatar 存放编码后的图像字节,如 PNG、JPEG 或 SVG;macOS 会在捐赠前将其栅格化为 PNG。应用负责获取这些字节,并决定消息的发送者和内容是否可以共享给系统。NewNotification 会复制发送者和图像,因此应用可以复用它的缓冲区。Group 是会话标识符;不设置时使用通知的 ID。这个 API 表示的是收到的私聊消息,不是群聊或发出的消息。通信通知的布局由 macOS 控制;副标题包含在正文中消息的上方。
在应用的 Apple App ID 上启用 Communication Notifications(通信通知),并将 INSendMessageIntent 添加到 macos.infoPlist.NSUserActivityTypes。使用 com.apple.developer.usernotifications.communication 签名并嵌入匹配的描述文件,Developer ID 分发也不例外:设置 macos.provisioningProfile,mygo build 会在签名前将其复制到 Contents/embedded.provisionprofile(参见描述文件)。描述文件必须授权应用的标识符、证书和 entitlements。不要把该受限 entitlement 加入 ad-hoc 构建:macOS 可能拒绝启动它。
MyGo 在把收到的消息捐赠给 SiriKit 并更新其通知内容之前检查已签名的 entitlement。缺少该能力、发送者信息不完整或头像无效,或捐赠/内容转换失败时,它发送普通通知。Windows、Linux 和不支持的平台保持原有行为。ID、声音、分组、点击处理程序、Close 和 ClearNotifications 使用相同的通知生命周期。
剪贴板
go
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()页面也可以使用 Web 的 navigator.clipboard。
Clipboard.Write(transfer.Data) 和 Clipboard.Read(formats...) 共用原生的拖放模型:有序条目、备选表示、文件列表、自定义 MIME 格式以及惰性提供者。Formats 无需请求数据即可发现可移植的名称;ReadFormat、ReadFiles、WriteFiles 和 Flush 提供显式的协商、文件传输与持久化。有关提供者的生命周期和原生格式映射,请参见剪贴板与拖放数据。剪贴板示例演示了这些操作。
系统 Shell
go
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.Beep()TrashItem 永远不会彻底删除:当条目无法放入废纸篓时,它会失败并让条目留在原地。在 Windows 上,这包括对回收站来说过大的条目,或位于没有回收站的驱动器上的条目——网络共享和许多可移动驱动器就是如此——否则 Windows 会不加询问地直接删除它们。
FileIcon 返回文件管理器为文件、文件夹或应用程序显示的图标,以 PNG 形式给出,尺寸为按像素请求的大小:传入以 DIP 为单位的尺寸乘以显示器的缩放系数。
go
png, err := mygo.Shell.FileIcon("/Applications/Safari.app", 64)在 macOS 上,它是 Finder 的图标(NSWorkspace),取自最贴合该尺寸的表示绘制;符号链接显示其指向对象的图标,没有 Finder 的箭头。在 Windows 上,它是资源管理器的图标(IShellItemImageFactory,只有图标,不含缩略图),路径也可以指向一个 shell 条目,例如通过 shell:AppsFolder\<AppUserModelID> 指向"开始"菜单中的应用。在 Linux 上,它来自图标主题中该文件内容类型对应的图标;对于 desktop 条目(.desktop),是其应用程序的图标,若主题中没有,则用 desktop 条目自身的图标。
InstalledApps 列出计算机上安装的应用程序,按名称排序,如同启动器显示的那样;LaunchApp 启动其中一个,或将其带到前台:
go
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])
}()每个应用都有系统显示的 Name(以用户的语言)、一个 ID,以及 FileIcon 据以绘制图标的 Path:
| 应用列表 | ID | Path | |
|---|---|---|---|
| macOS | /Applications、/System/Applications、~/Applications 以及 CoreServices 的 Applications 中的 .app bundle,以及它们子文件夹(实用工具)中的 bundle,外加 Finder;位于符号链接之后的应用只解析一次链接,在链接处取,如 /Applications/Safari.app | bundle 标识符;没有标识符的 bundle 则用其路径 | 该 bundle |
| Windows | "开始"菜单"所有应用"中的应用,桌面应用和打包应用皆含(shell 的 Applications 文件夹) | AppUserModelID;没有该 ID 的桌面应用,则用其在已知文件夹下的路径 | shell:AppsFolder\<ID> |
| Linux | 菜单中显示的 desktop 条目(排除 NoDisplay、Hidden 或面向其他桌面的条目) | desktop 文件 ID,如 org.gnome.Nautilus.desktop | 该 desktop 条目 |
从其他 goroutine 调用时,InstalledApps 不会阻塞主线程,并在几十毫秒内返回。绘制图标每个需要几毫秒,且在主线程上进行:只绘制要显示的图标,并把它们保留下来。
显示器
mygo.Screen 描述已连接的显示器,使用设备无关像素,原点位于主显示器的左上角:
go
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 不包括菜单栏、Dock 和任务栏。DisplayMatching(rect) 返回与给定矩形重叠最多的显示器,即窗口所在的显示器。
深色模式
mygo.Theme 是浅色或深色外观,页面通过 prefers-color-scheme 媒体查询跟随它:
go
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 可以在 App.Run 之前调用,例如传入用户保存在应用偏好设置中的外观:应用随后就会以该外观启动。
强调色与无障碍
mygo.Theme.Preferences() 返回桌面自身控件的强调色以及这些控件遵循的无障碍设置,一次全部读取:
go
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 size强调色是带直通 alpha 的 8 位 sRGB;ui.RGB(p.Accent.R, p.Accent.G, p.Accent.B) 可将其转换为 ui.Color,供你自己绘制时使用。
ReduceMotion 对应 macOS 的"减弱动态效果"、Windows 关闭"动画效果"以及关闭 GNOME 动画;ReduceTransparency 对应 macOS 的"降低透明度"和 Windows 关闭"透明效果",Linux 桌面没有这一设置,因此在 Linux 上为 false;HighContrast 对应 macOS 的"增强对比度"、Windows 的对比度主题,以及 Linux 上 settings portal(或 HighContrast GTK 主题)的高对比度设置。macOS 没有文本缩放,因此 TextScale 在其上为 1。当 Linux 桌面的 settings portal 没有强调色时,以及在浏览器中,强调色缺失。
当桌面改变其中任何一项时,Theme.OnUpdated 会触发,SetSource 也是如此。读取这些值可能需要调用桌面(如 Linux 上的 settings portal),因此 MyGo 只读取一次,并保留结果直到下次变化。ui 包的默认主题已经跟随强调色、对比度和文本缩放,Animate 则遵循减弱动态效果。与其他需要应用运行的调用一样,在 App.Run 之前于 main 中调用 Preferences 会 panic;由其他 goroutine 调用时,它会等待应用启动。
页面从文档开始加载起就能获取它们,即 mygo-runtime 的 preferences() 以及 --mygo-accent 和 --mygo-text-scale CSS 变量,参见前端指南;prefers-reduced-motion 和 prefers-contrast 已经跟随桌面设置。通过事件发送或由绑定方法返回的 ThemePreferences 就是同一份 JSON,生成的客户端会用 mygo-runtime 的 ThemePreferences 为其标注类型。
电源
go
// 在长时间任务期间保持计算机唤醒,第二个参数为 true 时还保持显示器常亮。
release := mygo.Power.KeepAwake("Exporting the video", false)
defer release()
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 input传给 KeepAwake 的原因会显示在系统列出"阻止睡眠的项目"的地方。在 Linux 上,屏保启动时调用 OnLockScreen,而屏保启动通常意味着锁屏;在不报告空闲时间的桌面上,IdleTime 为 0。
Now Playing 与媒体键
mygo.NowPlaying 在系统显示媒体的地方展示应用正在播放的内容:macOS 上控制中心和菜单栏中的"正在播放",Windows 上音量浮动窗口和锁屏界面的媒体控件,以及 Linux 上的 MPRIS 客户端,如 GNOME 和 KDE 的媒体控件与 playerctl。当它有展示内容时,键盘和耳机的媒体键以及这些控件都会向应用发送命令:
go
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 controls当状态为 PlaybackPlaying 时,系统会自行推进播放位置,因此应用只在播放发生跳转时才需要再次设置它。播放/暂停键在状态为播放中时发送 MediaPause,否则发送 MediaPlay;只有信息中提供的命令才会到达 OnCommand(Commands 为 0 时提供播放、暂停和停止)。pos 是 MediaSeek 要跳转到的位置,对其他命令而言则是命令到来时的播放位置。使用 audio 包的播放器进行播放的应用,可以让 Control 负责播放、暂停、停止和跳转,并显示其状态:
go
release := mygo.NowPlaying.Control(music) // an *audio.Player
defer release()Control 会监听播放器的结束事件;当应用自行播放、暂停或跳转后,用 NowPlaying.Sync 同步显示。在浏览器中,NowPlaying 就是页面的 Media Session。
网络
mygo.Network 告知计算机是否联网,以及连接是否为按流量计费——例如手机热点或按量计费的套餐,此时应用应暂缓大文件下载:
go
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 表示系统与计算机之外的网络建立了连接,与浏览器的 navigator.onLine 相同;它并不能证明有服务器在应答:认证门户背后的网络,或通往互联网的路径已中断的网络,同样算作在线。macOS 和 Linux 以存在外出路由为准(已满足的路径,GLib 的 network-available),Windows 则以连接级别达到本地访问或更高为准。在 macOS 上,Metered 指昂贵的路径(蜂窝网络、手机热点)或低数据模式;在 Linux 上指按流量计费的连接;在 Windows 上指费用为固定或可变的连接,或处于漫游、超出或接近数据限额的连接。在浏览器中,Online 即 navigator.onLine,Metered 为 false。
Status 在任意 goroutine 中都是安全的;在 macOS 上,首次调用会等待系统的第一次应答,这通常在几毫秒内到达。从第一次调用 OnChanged 开始就会监听变化,其监听器运行在主线程上,且仅在状态变化时触发;它返回一个用于移除监听器的函数。
页面本来就有 navigator.onLine。要在 UI 中显示原生状态,请在监听器中广播一个类型化事件,这样生成的客户端就能收到它:
go
// 连接变化时,Connectivity 以 {online, metered} 的形式推送到每个窗口。
var Connectivity = mygo.NewEvent[mygo.NetworkStatus]("connectivity")
func init() {
mygo.Network.OnChanged(func(s mygo.NetworkStatus) { Connectivity.Broadcast(s) })
}代理
网页通过 webview 访问网络,webview 遵循用户在系统设置中配置的代理,包括其绕过列表和配置脚本(PAC 文件):WebView2 与 Chromium 相同,读取 Windows 的代理设置;WKWebView 读取 macOS 的设置;WebKitGTK 通过 GIO 的默认代理解析器,读取 GNOME 的设置,或通过 libproxy 读取 KDE 的设置。
Go 的 http.DefaultClient 只读取 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY。netproxy 包让 Go 的请求像页面一样遵循系统设置,MyGo 自身的请求也使用它:即更新器的请求,以及 fetch 和 websocket 插件的请求(除非其选项中指定了客户端)。你自己的客户端可以通过它的 transport 启用:
go
import "github.com/egoist/mygo/netproxy"
client := &http.Client{Transport: netproxy.Transport(), Timeout: time.Minute}或者,对于你自己的 transport,设置 t.Proxy = netproxy.Proxy。对每个请求而言:
- 回环主机(
localhost、*.localhost、127.0.0.0/8、::1)直接连接。 - 当设置了
HTTP_PROXY、HTTPS_PROXY、ALL_PROXY或NO_PROXY(或其小写形式)时,由环境变量决定,与http.ProxyFromEnvironment相同,因此为应用某一次运行设置的代理优先。 - 否则由系统针对该 URL 的设置决定:在 Windows 上读取用户设置,由 WinHTTP 完成,支持自动检测(WPAD)和配置脚本;在 macOS 上由 CFNetwork 读取"系统设置 > 网络 > 代理"中的设置,支持配置脚本;在 Linux 上使用 GIO 的默认代理解析器,与 WebKitGTK 相同。没有结果时,请求直接连接。
系统对某个协议和主机的应答会缓存 5 秒,因此在应用运行期间开启、关闭或更改代理,会对之后的请求生效。进行中的请求(如下载或流)会保持其连接。Go 的 transport 支持 HTTP 和 HTTPS 代理以及 SOCKS5,不支持 SOCKS4;只有当系统的应答中包含代理密码时才会发送它,GNOME 的设置可以包含密码。在浏览器中,Go 的请求由浏览器通过其自身的代理发出。
机密存储
mygo.Secrets 将小型机密(如访问令牌和 API 密钥)保存在系统的凭据存储中。凭据存储会为用户加密它们,不同于放在用户数据目录里的文件:macOS 上是登录钥匙串,Linux 上是 Secret Service(通过 libsecret 使用 GNOME Keyring、KWallet、KeePassXC 等),Windows 上是凭据管理器。
go
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) {
// 请用户登录。
}
mygo.Secrets.Delete("github-token") // on sign out; nil when there is none单条机密最大为 mygo.MaxSecretSize(2560)字节——这是凭据管理器能保存的上限,键最长 256 字节。要保存更多内容,可用存放在 Secrets 中的密钥将其加密后写入用户数据目录中的文件。这些方法在任意 goroutine 中都是安全的,也可以在 Run 之前调用;当系统向用户提问(例如解锁钥匙串)时它们可能阻塞;在主线程上调用时,应用在此期间仍会继续处理事件。要在页面中使用它们,请绑定一个调用这些方法的后端方法,这样只有页面需要的内容才会跨越到页面中。
存储以"服务"和"账户"两级保存机密:Secrets 将键作为账户,将应用标识符(mygo.config.ts 的 identifier)作为服务。mygo dev 的开发应用使用 <identifier>.dev,因此它的机密与已安装应用的机密相互隔离;没有标识符而运行的应用(如通过 go run)则使用其名称。Secrets.Service 可以选择另一个服务,以便与应用的其他程序(如其命令行工具)共享机密,或读取 Electron 应用用 keytar 保存的机密——keytar 会在应用传入的服务名之下,以与 Secrets 相同的方式保存它们:
go
// 对应 Electron 应用中的 keytar.getPassword("MyApp", "refresh-token")
token, err := mygo.Secrets.Service("MyApp").Get("refresh-token")Secrets 防的是其他用户和被盗的磁盘,防不了同一用户下的其他应用:macOS 在其他应用读取前会询问用户,重建后的应用若未用同一身份签名,读取前同样会询问;而 Linux 和 Windows 则允许直接读取。
在没有凭据存储的地方,这些方法会失败,返回的错误满足 errors.Is(err, errors.ErrUnsupported):在浏览器中,以及在缺少 libsecret(Debian 和 Ubuntu 上的 libsecret-1-0 包)、会话总线或总线上的 Secret Service 的 Linux 上,如服务器和极简桌面。此时应用可以退而求其次,例如每次都要用户输入机密:
go
token, err := mygo.Secrets.Get("github-token")
switch {
case errors.Is(err, errors.ErrUnsupported):
// 没有凭据存储:询问用户,保存在内存中。
case errors.Is(err, mygo.ErrSecretNotFound):
// 请用户登录。
}监视文件
mygo.WatchFiles 监视一个文件或目录的变化;设置 Recursive 还会监视其中的目录。它在所有内容都纳入监视后返回,Next 等待下一批变化并成批返回;请在自己的 goroutine 中使用它:
go
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)
}
}
}()事件的 Path 相对于被监视的对象(w.Root(),即解析了符号链接后的路径),以斜杠分隔,. 表示其自身;Op 是 mygo.FileCreate、FileWrite、FileRemove 或 FileRename,重命名时 OldPath 给出来源路径。已经存在的内容不会被报告。变化会先等待 50 毫秒的静默期(Debounce,为负值时立即报告)才被上报,且最长不超过 500 毫秒(MaxDelay)。监视器将看到的内容与此前看到的内容作比较,因此事件说明的是差异,而不是造成差异的每一步:把它们当作"重新读取文件"的提示即可。所谓写入,是指文件大小或修改时间发生变化的文件、系统报告被写入的文件,或路径上被另一个文件替换的文件(编辑器和原子保存就是这样替换的:临时文件被重命名覆盖到目标路径):权限及其他元数据(索引器和杀毒软件会触碰它们)不会被报告;目录的"写入"也不报告,目录的变化体现为其条目的变化。
在 Next 取走上一批事件之前,监视器不会再次扫描:当应用繁忙时,期间的变化会归入下一批,因此既不丢失也不堆积。监视器无法查看的内容(如无权限的目录)会被跳过,而不会终止监视。Ignore 按路径排除文件和目录:被忽略的目录永远不会被打开,也不计入 MaxEntries。
文件通过其所在目录被监视,正如 fsnotify 所建议的:被替换的文件(编辑器保存时就是如此)报告为 . 的写入,被删除的文件报告为删除,重新出现的文件报告为创建。当目录本身,或文件所在的目录被删除、重命名或替换时,最后一批事件中会包含一条 . 的 FileRemove,随后 Next 返回 io.EOF。
调用 Close,或取消传给 WatchFiles 的 context,都会停止监视:Next 返回 io.EOF,并且在释放其句柄后 Done 会被关闭。应用退出时会停止它的所有监视。当目录中的文件和目录数量超过 MaxEntries 时,监视会以 mygo.ErrWatchLimit 停止:macOS 上是 65536(一条 FSEvents 流覆盖整个目录树),其他平台是 8192(其后端可能为每个条目占用一个句柄)。watch 插件让页面监视应用选定的目录和文件,并说明各平台的监视方式。
HintsOnly 以精度换取开销。监视器不保存快照,也不对文件调用 stat:它只报告 mygo.FileHint 事件,每个事件指出一个目录——系统说该目录内或其下有变化;当系统丢失了变化时,则报告 .。哪个文件变了、如何变的、是否被重命名,都无从得知。它适合开发用的重载器——任何变化都触发重新构建:
go
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"
},
})在文件身份重要的场合绝不要使用它:跟踪文档、同步,或任何会依据它无法得知的路径采取行动的用途。启用它时,Ignore 只会被询问目录(即使被忽略文件发生变化,也仍会给出该文件所在目录的提示),MaxEntries 计算的是纳入监视的目录数。在 macOS 和 Windows 上,递归的仅提示监视只纳入其根目录,由系统整体监视;在 Linux 上,它纳入每个目录,读取其条目的类型而不对条目调用 stat。符号链接仍然不被跟随,目录之外的一切不被监视;被监视的目录消失时,仍会以一条 . 的 FileRemove 结束监视。
全局快捷键
全局快捷键在应用处于后台时也能工作,例如把应用带到前台:
go
err := mygo.GlobalShortcut.Register("CmdOrCtrl+Shift+Space", func() {
win.Show()
win.Focus()
})该函数运行在主线程上。快捷键采用accelerator 语法;用 Unregister、UnregisterAll 和 IsRegistered 管理它们。当快捷键已被占用时——被本应用占用,或在系统能够告知的情况下被其他应用占用——Register 会失败。
在 Linux 的 X11 下,应用自己抓取按键。在 Wayland 下应用无法这样做,桌面会通过 XDG desktop portal 为应用绑定快捷键,KDE Plasma 6 和 GNOME 48 及以后版本提供了该接口:桌面可能要求用户确认新的快捷键,并允许用户另选按键;而且它要求应用已经安装,因为它通过 desktop 条目识别应用。快捷键在 Register 返回后很快即可生效。对于函数所显示或聚焦的窗口,当桌面提供按键激活令牌时(如 GNOME),窗口凭该令牌获得焦点。