toast

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 8 Imported by: 0

README

Toast

English | 简体中文

Toast is a small Go library for sending desktop notifications from native applications and lightweight tools. It supports macOS, Windows, and JavaScript WASM, with helper utilities for Windows toast click-to-focus workflows.

Features

  • Simple toast.Push API with functional options
  • macOS notifications through osascript or Objective-C
  • Windows toast notifications through Windows Runtime APIs
  • Windows protocol activation for clickable toast actions
  • Optional Windows focus helper for bringing the originating terminal or app to front
  • JavaScript/WASM notification support for browser environments

Install

go get github.com/hellolib/toast

Quick Start

package main

import "github.com/hellolib/toast"

func main() {
    _ = toast.Push("Build finished", toast.WithTitle("Agent Notify"))
}

Platform Examples

macOS
package main

import "github.com/hellolib/toast"

func main() {
    _ = toast.Push("Permission required",
        toast.WithTitle("Agent Notify"),
        toast.WithSubtitle("15:04:05"),
        toast.WithAudio(toast.Submarine),
    )
}

For Objective-C delivery:

_ = toast.Push("Task completed",
    toast.WithTitle("Agent Notify"),
    toast.WithObjectiveC(),
)
Windows
package main

import "github.com/hellolib/toast"

func main() {
    _ = toast.Push("Task completed",
        toast.WithAppID("agent-notify"),
        toast.WithTitle("Agent Notify"),
        toast.WithAudio(toast.Default),
        toast.WithLongDuration(),
    )
}

To add an image:

_ = toast.Push("Task completed",
    toast.WithTitle("Agent Notify"),
    toast.WithIcon(`C:\path\to\icon.png`),
)
JavaScript / WASM
package main

import (
    "fmt"

    "github.com/hellolib/toast"
)

func main() {
    _ = toast.Push("Saved",
        toast.WithTitle("WASM App"),
        toast.WithOnClick(func(event interface{}) {
            fmt.Println("clicked")
        }),
        toast.WithOnClose(func() {
            fmt.Println("closed")
        }),
    )
}

Windows Click-To-Focus

Windows toast clicks are delivered through activation. For ordinary Go command line tools, the practical approach is:

  1. Ship a small GUI-subsystem helper executable.
  2. Register a custom URL protocol that launches the helper.
  3. Send the toast with WithActivationType("protocol").
  4. Pass the protocol URI through WithActivationArguments.

This repository provides both the library API and a helper command for that flow. The helper is a separate executable so toast clicks do not flash a console window.

Target Selection

PrepareFocusActivation resolves the target window before the toast is sent. It walks up from the supplied PID, finds a visible top-level window, and embeds that window handle in the activation URI. When the toast is clicked, the helper first focuses that exact window handle. If the handle is no longer valid, it falls back to the PID-based process-tree lookup.

This avoids focusing the wrong window in shells launched from apps with multiple top-level windows, such as VS Code, Windows Terminal, or other Electron-based hosts.

Library Usage
package main

import (
    "fmt"
    "os"

    "github.com/hellolib/toast"
)

func main() {
    focus, err := toast.PrepareFocusActivation(
        os.Getppid(),
        `C:\path\to\toast-focus-helper.exe`,
    )
    if err != nil {
        panic(err)
    }

    _ = toast.Push("Click to focus the current terminal",
        toast.WithAppID("agent-notify"),
        toast.WithTitle("Agent Notify"),
        toast.WithMessage(fmt.Sprintf("helper: %s\nhwnd: 0x%x", focus.Helper, focus.Window)),
        toast.WithActivationType("protocol"),
        toast.WithActivationArguments(focus.Arguments),
    )
}

PrepareFocusActivation checks explicit helper candidates first. If none are provided or found, it looks next to the current executable for conventional names:

  • toast-focus-helper.exe
  • toast-focus-helper-arm64.exe
  • <app>-focus-helper.exe
  • <app>-helper.exe
Demo Commands

cmd/toast-focus is a runnable demo that sends a clickable toast. It expects the helper binary to be in the same directory.

make build

Artifacts are written to dist/:

  • toast-focus.exe
  • toast-focus-helper.exe
  • toast-focus-arm64.exe
  • toast-focus-helper-arm64.exe

For applications that only need the helper binaries:

make build-helpers

Make Targets

make test                 # Run Go tests
make build                # Build all Windows demo/helper binaries
make build-helpers        # Build only Windows focus helpers
make build-windows-amd64  # Build Windows amd64 demo/helper
make build-windows-arm64  # Build Windows arm64 demo/helper
make clean                # Remove dist/

API Overview

Common options:

  • toast.Push(message, opts...)
  • toast.WithTitle(title)
  • toast.WithMessage(message)
  • toast.WithAudio(audio)

macOS options:

  • toast.WithSubtitle(subtitle)
  • toast.WithObjectiveC()

Windows options:

  • toast.WithAppID(appID)
  • toast.WithIcon(path)
  • toast.WithIconRaw(bytes)
  • toast.WithActivationType(kind)
  • toast.WithActivationArguments(uri)
  • toast.WithProtocolAction(label, uri)
  • toast.WithLongDuration()
  • toast.WithShortDuration()

Windows focus helpers:

  • toast.FindFocusHelper(candidates...)
  • toast.RegisterFocusProtocol(helperPath, protocol...)
  • toast.PrepareFocusActivation(pid, helperCandidates...)
  • toast.FocusActivationForWindow(pid, hwnd, logPath, helperCandidates...)
  • toast.FocusActivationArguments(pid, protocol...)

FocusActivation.Window is the resolved Windows HWND used for precise click-to-focus. It may be zero when no suitable window is found before sending the toast.

JavaScript/WASM options:

  • toast.WithIcon(url)
  • toast.WithImage(url)
  • toast.WithRequireInteraction(true)
  • toast.WithOnClick(fn)
  • toast.WithOnShow(fn)
  • toast.WithOnClose(fn)
  • toast.WithOnError(fn)

License

MIT

Documentation

Index

Constants

View Source
const (
	// DefaultFocusProtocol is the protocol used by the bundled focus helper.
	DefaultFocusProtocol = "anfocus"
)

Variables

This section is empty.

Functions

func FindFocusHelper

func FindFocusHelper(candidates ...string) (string, error)

FindFocusHelper locates the helper executable used for toast click-to-focus. Explicit candidates are checked first, followed by conventional helper names next to the current executable.

func FocusActivationArguments

func FocusActivationArguments(pid int, protocols ...string) string

FocusActivationArguments formats the URI passed to WithActivationArguments.

func IsUsableWindow added in v0.4.0

func IsUsableWindow(hwnd uintptr) bool

IsUsableWindow 校验单个 HWND(供 helper 直连路径):存在 + 可见 + 无 owner + 有标题。

func ParseFocusActivation added in v0.4.0

func ParseFocusActivation(uri string) (pid int, hwnd uintptr, logPath string, err error)

ParseFocusActivation 解析 helper 收到的激活 URI。 未识别的 hwnd/base64 一律降级为零值/空串,绝不报错阻断聚焦。

func PrepareFocusActivationVerbose added in v0.4.0

func PrepareFocusActivationVerbose(pid int, logPath string, helperCandidates ...string) (FocusActivation, FocusDiag, error)

PrepareFocusActivationVerbose 找 helper、注册协议、解析宿主窗口,并返回完整诊断。 logPath 非空时嵌入激活 URI(helper 点击时据此写日志)。

func Push

func Push(message string, opts ...NotificationOption) error

func RegisterAppID added in v0.4.0

func RegisterAppID(appID, displayName, iconPath string) error

RegisterAppID registers appID as a Windows AppUserModelID with a display name and an attribution icon, under HKCU\Software\Classes\AppUserModelId\<appID>. After registration, toasts shown via CreateToastNotifier(appID) display the name and icon in the notification's top (attribution) row. Idempotent.

func RegisterFocusProtocol

func RegisterFocusProtocol(helperPath string, protocols ...string) error

RegisterFocusProtocol registers protocol as a user-local URL protocol that launches helperPath with the clicked toast URI.

func SelectHostWindow added in v0.4.0

func SelectHostWindow(chain []AncestorWindows) (hwnd uintptr, pid uint32, reason string)

SelectHostWindow 从祖先链中挑选宿主终端窗口。 P0 语义(行为保持):按 chain 顺序取第一个"可见 + 无 owner + 有标题"的窗口。 返回选中的 HWND(无则 0)、其所属 PID、以及人类可读的原因。

Types

type Action

type Action struct {
	Type      string
	Label     string
	Arguments string
}

Action defines an actionable button on a notification.

Only protocol-type action buttons are useful, as there's no way to receive feedback from the user's choice. Example:

Action{"protocol", "Open Maps", "bingmaps:?q=sushi"}

type AncestorWindows added in v0.4.0

type AncestorWindows struct {
	PID     uint32
	PPID    uint32
	Exe     string
	Windows []WindowInfo
}

AncestorWindows 归组一个祖先进程及其拥有的顶层窗口。

func EnumerateAncestorWindows added in v0.4.0

func EnumerateAncestorWindows(start uint32) []AncestorWindows

EnumerateAncestorWindows 从 start 沿进程树向上,逐祖先归组其全部顶层窗口。

type Audio

type Audio string
const (
	Silent         Audio = "silent"
	Default        Audio = "ms-winsoundevent:Notification.Default"
	IM             Audio = "ms-winsoundevent:Notification.IM"
	Mail           Audio = "ms-winsoundevent:Notification.Mail"
	Reminder       Audio = "ms-winsoundevent:Notification.Reminder"
	SMS            Audio = "ms-winsoundevent:Notification.SMS"
	LoopingAlarm   Audio = "ms-winsoundevent:Notification.Looping.Alarm"
	LoopingAlarm2  Audio = "ms-winsoundevent:Notification.Looping.Alarm2"
	LoopingAlarm3  Audio = "ms-winsoundevent:Notification.Looping.Alarm3"
	LoopingAlarm4  Audio = "ms-winsoundevent:Notification.Looping.Alarm4"
	LoopingAlarm5  Audio = "ms-winsoundevent:Notification.Looping.Alarm5"
	LoopingAlarm6  Audio = "ms-winsoundevent:Notification.Looping.Alarm6"
	LoopingAlarm7  Audio = "ms-winsoundevent:Notification.Looping.Alarm7"
	LoopingAlarm8  Audio = "ms-winsoundevent:Notification.Looping.Alarm8"
	LoopingAlarm9  Audio = "ms-winsoundevent:Notification.Looping.Alarm9"
	LoopingAlarm10 Audio = "ms-winsoundevent:Notification.Looping.Alarm10"
	LoopingCall    Audio = "ms-winsoundevent:Notification.Looping.Call"
	LoopingCall2   Audio = "ms-winsoundevent:Notification.Looping.Call2"
	LoopingCall3   Audio = "ms-winsoundevent:Notification.Looping.Call3"
	LoopingCall4   Audio = "ms-winsoundevent:Notification.Looping.Call4"
	LoopingCall5   Audio = "ms-winsoundevent:Notification.Looping.Call5"
	LoopingCall6   Audio = "ms-winsoundevent:Notification.Looping.Call6"
	LoopingCall7   Audio = "ms-winsoundevent:Notification.Looping.Call7"
	LoopingCall8   Audio = "ms-winsoundevent:Notification.Looping.Call8"
	LoopingCall9   Audio = "ms-winsoundevent:Notification.Looping.Call9"
	LoopingCall10  Audio = "ms-winsoundevent:Notification.Looping.Call10"
)

type FocusActivation

type FocusActivation struct {
	Protocol  string
	Helper    string
	Arguments string
	Window    uintptr
}

FocusActivation describes the protocol activation data used to focus the source window when a Windows toast is clicked.

func FocusActivationForWindow added in v0.5.0

func FocusActivationForWindow(pid int, hwnd uintptr, logPath string, helperCandidates ...string) (FocusActivation, error)

FocusActivationForWindow 为一个「已知的」窗口句柄拼装协议激活数据,跳过 PrepareFocusActivation 的进程树窗口选择(EnumerateAncestorWindows + SelectHostWindow)。用于调用方在别处(如 SessionStart 时刻)已捕获到精确的宿主 窗口 HWND、只想让点击回到那扇窗的场景。仍会定位 helper 并注册协议。

hwnd 为 0 时返回 error(无句柄可用)。logPath 非空则嵌入激活 URI,helper 点击时 据此写诊断日志(与 PrepareFocusActivationVerbose 行为一致)。

func PrepareFocusActivation

func PrepareFocusActivation(pid int, helperCandidates ...string) (FocusActivation, error)

PrepareFocusActivation 保留原签名,委托 Verbose(无 logPath、丢弃诊断)。

type FocusDiag added in v0.4.0

type FocusDiag struct {
	StartPID     uint32
	Chain        []AncestorWindows
	SelectedHWND uintptr
	SelectedPID  uint32
	Reason       string
}

FocusDiag 记录一次 send(或 click 重走)的完整解析,用于可观测。

func (FocusDiag) String added in v0.4.0

func (d FocusDiag) String() string

type NotificationDuration

type NotificationDuration string
const (
	Short NotificationDuration = "short"
	Long  NotificationDuration = "long"
)

type NotificationOption

type NotificationOption func(*notification)

func WithActivationArguments

func WithActivationArguments(activationArguments string) NotificationOption

WithActivationArguments

// The activation/action arguments (invoked when the user clicks the notification)

func WithActivationType

func WithActivationType(activationType string) NotificationOption

WithActivationType

The type of notification level action (like Action)

func WithAppID

func WithAppID(appID string) NotificationOption

WithAppID

The name of your app. This value shows up in Windows 10's Action Centre, so make it something readable for your users. It can contain spaces, however special characters (eg. é) are not supported.

func WithAudio

func WithAudio(audio Audio) NotificationOption

WithAudio

The audio to play when displaying the notification

func WithAudioLoop

func WithAudioLoop(b bool) NotificationOption

WithAudioLoop

Whether to loop the audio (default false)

func WithDuration

WithDuration

How long the notification should show up for (short/long)

func WithIcon

func WithIcon(pathIcon string) NotificationOption

WithIcon

An optional path to an image on the OS to display to the left of the title & message.

func WithIconRaw

func WithIconRaw(raw []byte) NotificationOption

func WithLongDuration

func WithLongDuration() NotificationOption

func WithMessage

func WithMessage(msg string) NotificationOption

WithMessage

The single/multi line message to display for the notification.

func WithProtocolAction

func WithProtocolAction(label string, arguments ...string) NotificationOption

WithProtocolAction

Defines an actionable button. See https://msdn.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-adaptive-interactive-toasts for more info.

Only protocol type action buttons are actually useful, as there's no way of receiving feedback from the user's choice. Examples of protocol type action buttons include: "bingmaps:?q=sushi" to open up Windows 10's maps app with a pre-populated search field set to "sushi".

Action{"protocol", "Open Maps", "bingmaps:?q=sushi"}

func WithShortDuration

func WithShortDuration() NotificationOption

func WithTitle

func WithTitle(title string) NotificationOption

WithTitle

The main title/heading for the notification.

type WindowInfo added in v0.4.0

type WindowInfo struct {
	HWND       uintptr
	OwnerPID   uint32
	Class      string
	Title      string
	Visible    bool
	HasOwner   bool
	Minimized  bool
	ExStyle    uint32
	X, Y, W, H int32
}

WindowInfo 是一个顶层窗口的朴素快照(除数值 HWND 外不含任何 Win32 句柄)。 中立类型,供 send 侧、helper、探针共用与单测。

Directories

Path Synopsis
assets
_generate command
Command generate renders the default "terminal" logo used as the Windows toast appLogoOverride image when a caller does not supply WithIcon.
Command generate renders the default "terminal" logo used as the Windows toast appLogoOverride image when a caller does not supply WithIcon.
cmd
toast-focus command
toast-focus sends a Windows toast notification whose click action focuses the terminal window that launched this process.
toast-focus sends a Windows toast notification whose click action focuses the terminal window that launched this process.
toast-focus-helper command
toast-focus-helper is launched by the anfocus: protocol.
toast-focus-helper is launched by the anfocus: protocol.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL