feat(notify): Windows toasts under talk's own identity, icon on Linux
The daemon (and talk test --toast) now register the AppUserModelID m3ta.talk under HKCU with display name and the installed icon, so toasts appear as "talk" with the icon instead of Windows PowerShell; the PowerShell identity stays as fallback until the registration has run once. The redundant "talk" title line is dropped under the own identity. notify.Setup bundles icon install, identity and the talk: protocol handler for both callers. Linux notifications carry the installed icon as app_icon. doctor names the active identity. Verified on the Windows 11 VM: registry key, icon file, toast with working Cancel button, exe icon; Linux via quickshell. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e77cf0a11
commit
2d9d2a86e9
10 files changed
+290
-48
No files matched your search
+65
-11
@@ -19,17 +19,69 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
|
||||
"code.m3ta.dev/m3tam3re/talk/internal/icon"
|
||||
)
|
||||
|
||||
// Title is the title of all talk notifications.
|
||||
const Title = "talk"
|
||||
|
||||
// AppID is talk's AppUserModelID on Windows: the identity (name, icon) its
|
||||
// toasts are shown under once UseIcon has registered it.
|
||||
const AppID = "m3ta.talk"
|
||||
|
||||
var (
|
||||
iconMu sync.Mutex
|
||||
iconFile string
|
||||
)
|
||||
|
||||
func setIconPath(p string) {
|
||||
iconMu.Lock()
|
||||
iconFile = p
|
||||
iconMu.Unlock()
|
||||
}
|
||||
|
||||
// appIcon is the icon a freedesktop notification carries: the installed
|
||||
// talk icon, or a theme icon while none is installed.
|
||||
func appIcon() string {
|
||||
iconMu.Lock()
|
||||
defer iconMu.Unlock()
|
||||
if iconFile != "" {
|
||||
return iconFile
|
||||
}
|
||||
return "audio-speakers"
|
||||
}
|
||||
|
||||
// Setup prepares the desktop side once per process (daemon start, talk
|
||||
// test --toast): installs the icon into dir and registers it (Windows: the
|
||||
// toast identity and, with exe, the talk: protocol handler the Cancel
|
||||
// button needs). Best effort: the error is for the log, notifications still
|
||||
// work under the fallback identity.
|
||||
func Setup(dir, exe string) error {
|
||||
path, err := icon.Install(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var errs []error
|
||||
if err := UseIcon(path); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if exe != "" {
|
||||
if err := RegisterProtocol(exe); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// timeout bounds the call of the OS tool.
|
||||
const timeout = 10 * time.Second
|
||||
|
||||
@@ -60,7 +112,10 @@ func Describe() string {
|
||||
case "darwin":
|
||||
return "osascript (Notification Center; no cancel button)"
|
||||
case "windows":
|
||||
return "powershell (Windows toast with cancel button via the talk: link)"
|
||||
if ownIdentity() {
|
||||
return "powershell (Windows toast as talk, cancel button via the talk: link)"
|
||||
}
|
||||
return "powershell (Windows toast under the Windows PowerShell identity until the daemon has run once; cancel button via the talk: link)"
|
||||
default:
|
||||
return "notify-send (libnotify; no session bus, so no cancel button)"
|
||||
}
|
||||
@@ -153,7 +208,7 @@ func showWithTool(ctx context.Context, text string) error {
|
||||
script := fmt.Sprintf("display notification %s with title %s", appleString(text), appleString(Title))
|
||||
cmd = exec.CommandContext(ctx, "osascript", "-e", script)
|
||||
case "windows":
|
||||
cmd = exec.CommandContext(ctx, "powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", psEncode(toastScript(text)))
|
||||
cmd = exec.CommandContext(ctx, "powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", psEncode(toastScript(text, toastAppID())))
|
||||
default:
|
||||
cmd = exec.CommandContext(ctx, "notify-send", "--app-name="+Title, "--urgency=normal", Title, text)
|
||||
}
|
||||
@@ -168,18 +223,17 @@ func appleString(s string) string {
|
||||
return `"` + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(s) + `"`
|
||||
}
|
||||
|
||||
// toastScript is the PowerShell toast (Windows 10/11); the text is passed
|
||||
// base64-encoded so there is no quoting problem.
|
||||
func toastScript(text string) string {
|
||||
// toastScript is the PowerShell for a plain toast (Windows 10/11) under
|
||||
// appID; the text is passed base64-encoded so there is no quoting problem.
|
||||
func toastScript(text, appID string) string {
|
||||
return fmt.Sprintf(`$ErrorActionPreference = 'Stop'
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
$msg = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('%s'))
|
||||
$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
|
||||
$nodes = $xml.GetElementsByTagName('text')
|
||||
$nodes.Item(0).AppendChild($xml.CreateTextNode('%s')) | Out-Null
|
||||
$nodes.Item(1).AppendChild($xml.CreateTextNode($msg)) | Out-Null
|
||||
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
||||
$msg = [System.Security.SecurityElement]::Escape([System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('%s')))
|
||||
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
||||
$xml.LoadXml("<toast><visual><binding template='ToastGeneric'>%s<text>$msg</text></binding></visual></toast>")
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('%s').Show([Windows.UI.Notifications.ToastNotification]::new($xml))
|
||||
`, base64.StdEncoding.EncodeToString([]byte(text)), Title, Title)
|
||||
`, base64.StdEncoding.EncodeToString([]byte(text)), titleLine(appID), appID)
|
||||
}
|
||||
|
||||
// psEncode encodes a script for -EncodedCommand (UTF-16LE + base64).
|
||||
|
||||
Reference in new issue
Block a user