Files
chuan 5ce2c8a982 feat: Claude Code 原生 Windows 通知(C# / .NET 10 + Avalonia 12)
为 Claude Code 提供原生 Windows toast 通知:点击跳回原窗口、切回 Windows
Terminal 标签、跨虚拟桌面、调用方图标、非阻塞投递;NativeAOT 单文件分发。
2026-06-22 18:05:15 +08:00

122 lines
3.7 KiB
C#

using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Notify.Ipc;
using Notify.Models;
using Notify.Services;
using Notify.ViewModels;
using Notify.Views;
namespace Notify;
public partial class App : Application
{
private SettingsWindow? _settingsWindow;
private SpoolWatcher? _spoolWatcher;
public static new App Current => (App)Application.Current!;
public SettingsService Settings { get; } = new();
public ToastManager Toasts { get; private set; } = null!;
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
{
Settings.Load();
Toasts = new ToastManager(Settings);
// 监视 spool 目录,把 CLI 投递的请求转成 toast
_spoolWatcher = new SpoolWatcher(OnNotify);
_spoolWatcher.Start();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// 无主窗口的常驻进程:仅托盘存在,靠托盘菜单或外部请求驱动
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
// --demo:启动即弹一条 toast 并打开设置,便于无托盘交互地验证
if (desktop.Args is { Length: > 0 } args && System.Array.IndexOf(args, "--demo") >= 0)
{
Dispatcher.UIThread.Post(RunDemo);
}
}
base.OnFrameworkInitializationCompleted();
}
// 监视线程收到请求,切回 UI 线程弹出 toast
private void OnNotify(NotifyMessage message)
{
if (Settings.Current.PlaySound)
{
Notify.Interop.Sound.Play();
}
// 目标窗口已是前台(你正盯着看):完成类通知弹一下即可,用更短的停留
int? durationOverride = null;
if (!message.InputMode && message.TargetHwnd != 0 &&
Notify.Interop.Win32.GetForegroundWindow().ToInt64() == message.TargetHwnd)
{
durationOverride = Settings.Current.FocusedDurationSeconds;
}
Dispatcher.UIThread.Post(() => Toasts.Show(new ToastRequest
{
Title = message.Title,
Message = message.Message,
InputMode = message.InputMode,
Sticky = message.Sticky,
TargetHwnd = message.TargetHwnd,
WtRuntimeId = message.WtRuntimeId,
IconPath = message.IconPath,
DurationSecondsOverride = durationOverride,
}));
}
private void RunDemo()
{
// 普通:会自动消失
Toasts.Show(new ToastRequest { Title = "Claude Code", Message = "任务已完成 — 4 秒后自动消失" });
// 常驻:InputMode 且 Sticky,不点不消失
Toasts.Show(new ToastRequest
{
Title = "需要你的输入",
Message = "权限请求 — 常驻,点击 / ✕ 才关闭",
InputMode = true,
Sticky = true,
});
}
// 左键单击托盘图标直接打开设置
private void OnTrayClicked(object? sender, EventArgs e) => OpenSettings();
private void OpenSettings()
{
if (_settingsWindow is { } w)
{
w.Activate();
return;
}
_settingsWindow = new SettingsWindow
{
DataContext = new SettingsViewModel(Settings, Toasts),
};
_settingsWindow.Closed += (_, _) => _settingsWindow = null;
_settingsWindow.Show();
}
private void OnExitClick(object? sender, EventArgs e)
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.Shutdown();
}
}
}