Files
notify/Notify/Interop/WinTerminalTabs.cs
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

300 lines
8.4 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using System.Text;
namespace Notify.Interop;
/// <summary>
/// Windows Terminal 标签页的捕获与切换
///
/// save 时记录当前选中标签的 RuntimeId,点击通知激活窗口后再据此切回该标签
/// 全程走源生成 COMGeneratedComInterface),保证 NativeAOT 兼容
/// vtable 顺序与 GUID 均取自 Windows SDK UIAutomationClient.h
/// </summary>
public static partial class WinTerminalTabs
{
private const string WtClass = "CASCADIA_HOSTING_WINDOW_CLASS";
private const int TreeScopeDescendants = 4;
private const int ControlTypePropertyId = 30003;
private const int IsSelectedPropertyId = 30079;
private const int TabItemControlTypeId = 50019;
private const int SelectionItemPatternId = 10010;
private const int CLSCTX_INPROC_SERVER = 1;
private static readonly StrategyBasedComWrappers ComWrappers = new();
private static IUIAutomation? _uia;
private static bool _initTried;
public static bool IsWindowsTerminal(IntPtr hwnd) =>
hwnd != IntPtr.Zero && Win32.GetClassNameOf(hwnd) == WtClass;
// 返回当前选中标签的 RuntimeId 串,失败返回空串
public static string GetSelectedTabRuntimeId(IntPtr hwnd)
{
try
{
var uia = EnsureUia();
if (uia is null)
{
return "";
}
var root = uia.ElementFromHandle(hwnd);
var cond = uia.CreateTrueCondition();
var all = root.FindAll(TreeScopeDescendants, cond);
var count = all.Length();
for (var i = 0; i < count; i++)
{
var el = all.GetElement(i);
if (GetIntProperty(el, ControlTypePropertyId) != TabItemControlTypeId)
{
continue;
}
if (GetBoolProperty(el, IsSelectedPropertyId))
{
return RuntimeIdOf(el);
}
}
}
catch
{
// 任何 COM 异常退回空串
}
return "";
}
// 在已激活的 WT 窗口里找到匹配 RuntimeId 的标签并选中
public static bool SelectTab(IntPtr hwnd, string runtimeId)
{
if (string.IsNullOrEmpty(runtimeId))
{
return false;
}
try
{
var uia = EnsureUia();
if (uia is null)
{
return false;
}
var root = uia.ElementFromHandle(hwnd);
var cond = uia.CreateTrueCondition();
var all = root.FindAll(TreeScopeDescendants, cond);
var count = all.Length();
for (var i = 0; i < count; i++)
{
var el = all.GetElement(i);
if (GetIntProperty(el, ControlTypePropertyId) != TabItemControlTypeId)
{
continue;
}
if (RuntimeIdOf(el) != runtimeId)
{
continue;
}
var pattern = el.GetCurrentPattern(SelectionItemPatternId);
if (pattern is null)
{
return false;
}
pattern.Select();
return true;
}
}
catch
{
// 切换失败不影响窗口已被激活
}
return false;
}
private static IUIAutomation? EnsureUia()
{
if (_initTried)
{
return _uia;
}
_initTried = true;
try
{
var clsid = new Guid("ff48dba4-60ef-4201-aa87-54103eef594e");
var iid = typeof(IUIAutomation).GUID;
var hr = CoCreateInstance(ref clsid, IntPtr.Zero, CLSCTX_INPROC_SERVER, ref iid, out var ptr);
if (hr >= 0 && ptr != IntPtr.Zero)
{
_uia = (IUIAutomation)ComWrappers.GetOrCreateObjectForComInstance(ptr, CreateObjectFlags.None);
Marshal.Release(ptr);
}
}
catch
{
_uia = null;
}
return _uia;
}
private static int GetIntProperty(IUIAutomationElement el, int propertyId)
{
var v = el.GetCurrentPropertyValue(propertyId);
return v.lVal;
}
private static bool GetBoolProperty(IUIAutomationElement el, int propertyId)
{
var v = el.GetCurrentPropertyValue(propertyId);
return v.boolVal != 0;
}
// RuntimeId 是一个 int 数组(SAFEARRAY),拼成点分串用于比较
private static string RuntimeIdOf(IUIAutomationElement el)
{
var psa = el.GetRuntimeId();
if (psa == IntPtr.Zero)
{
return "";
}
try
{
if (SafeArrayGetLBound(psa, 1, out var lb) < 0 || SafeArrayGetUBound(psa, 1, out var ub) < 0)
{
return "";
}
var sb = new StringBuilder();
for (var idx = lb; idx <= ub; idx++)
{
var i = idx;
if (SafeArrayGetElement(psa, ref i, out var val) < 0)
{
return "";
}
if (sb.Length > 0)
{
sb.Append('.');
}
sb.Append(val);
}
return sb.ToString();
}
finally
{
SafeArrayDestroy(psa);
}
}
[LibraryImport("ole32.dll")]
private static partial int CoCreateInstance(ref Guid rclsid, IntPtr pUnkOuter, int dwClsContext, ref Guid riid, out IntPtr ppv);
[LibraryImport("oleaut32.dll")]
private static partial int SafeArrayGetLBound(IntPtr psa, uint nDim, out int plLbound);
[LibraryImport("oleaut32.dll")]
private static partial int SafeArrayGetUBound(IntPtr psa, uint nDim, out int plUbound);
[LibraryImport("oleaut32.dll")]
private static partial int SafeArrayGetElement(IntPtr psa, ref int rgIndices, out int pv);
[LibraryImport("oleaut32.dll")]
private static partial int SafeArrayDestroy(IntPtr psa);
}
// VARIANT 的最小化布局(x64 为 24 字节),只读 VT_I4 / VT_BOOL
[StructLayout(LayoutKind.Explicit, Size = 24)]
internal struct VARIANT
{
[FieldOffset(0)]
public ushort vt;
[FieldOffset(8)]
public int lVal;
[FieldOffset(8)]
public short boolVal;
}
[GeneratedComInterface]
[Guid("30cbe57d-d9d0-452a-ab13-7ac5ac4825ee")]
internal partial interface IUIAutomation
{
void _CompareElements();
void _CompareRuntimeIds();
void _GetRootElement();
IUIAutomationElement ElementFromHandle(IntPtr hwnd);
void _ElementFromPoint();
void _GetFocusedElement();
void _GetRootElementBuildCache();
void _ElementFromHandleBuildCache();
void _ElementFromPointBuildCache();
void _GetFocusedElementBuildCache();
void _CreateTreeWalker();
void _get_ControlViewWalker();
void _get_ContentViewWalker();
void _get_RawViewWalker();
void _get_RawViewCondition();
void _get_ControlViewCondition();
void _get_ContentViewCondition();
void _CreateCacheRequest();
IUIAutomationCondition CreateTrueCondition();
}
[GeneratedComInterface]
[Guid("d22108aa-8ac5-49a5-837b-37bbb3d7591e")]
internal partial interface IUIAutomationElement
{
void _SetFocus();
IntPtr GetRuntimeId();
void _FindFirst();
IUIAutomationElementArray FindAll(int scope, IUIAutomationCondition condition);
void _FindFirstBuildCache();
void _FindAllBuildCache();
void _BuildUpdatedCache();
VARIANT GetCurrentPropertyValue(int propertyId);
void _GetCurrentPropertyValueEx();
void _GetCachedPropertyValue();
void _GetCachedPropertyValueEx();
void _GetCurrentPatternAs();
void _GetCachedPatternAs();
[return: MarshalUsing(typeof(UniqueComInterfaceMarshaller<IUIAutomationSelectionItemPattern>))]
IUIAutomationSelectionItemPattern? GetCurrentPattern(int patternId);
}
[GeneratedComInterface]
[Guid("14314595-b4bc-4055-95f2-58f2e42c9855")]
internal partial interface IUIAutomationElementArray
{
int Length();
IUIAutomationElement GetElement(int index);
}
[GeneratedComInterface]
[Guid("352ffba8-0973-437c-a61f-f64cafd81df9")]
internal partial interface IUIAutomationCondition
{
}
[GeneratedComInterface]
[Guid("a8efa66a-0fda-421a-9194-38021f3578ea")]
internal partial interface IUIAutomationSelectionItemPattern
{
void Select();
}