From a67cdf574d79a49674baccfc2e1fee15b344cf11 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sat, 4 Apr 2026 19:37:52 +0900 Subject: [PATCH] =?UTF-8?q?[Phase=20L27]=20=EA=B2=BD=EC=9F=81=20=EA=B3=B5?= =?UTF-8?q?=EB=B0=B1=20=EC=A6=89=EC=8B=9C=20=ED=95=B4=EC=86=8C=20=E2=80=94?= =?UTF-8?q?=20Windows=20=EC=9D=BC=EC=83=81=20=EC=A0=9C=EC=96=B4=20?= =?UTF-8?q?=EB=8F=84=EA=B5=AC=205=EC=A2=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VolHandler.cs (신규, prefix=vol): - Windows Core Audio API COM 직접 호출 (IAudioEndpointVolume) - vol 50 설정, vol up/down ±10%, vol mute 토글 - AudioEndpoint 래퍼 클래스 + IDisposable 해제 - 볼륨 바 시각화 (20칸 블록), NuGet 불필요 QrHandler.cs (신규, prefix=qr): - QRCoder 1.6.0 NuGet 추가 (순수 C#, ~150KB) - qr {텍스트} → PNG 클립보드 복사 - qr save {텍스트} → TEMP 저장 후 탐색기 열기 - 바이트 길이·URL 자동 감지 표시 MeetHandler.cs (신규, prefix=meet): - %APPDATA%\AxCopilot\meet.json 로컬 저장 - meet add/del CRUD, 이름·URL·서비스 검색 - 서비스 자동 감지 (Zoom/Teams/Google Meet/Webex/Discord/Slack) - Enter → UseShellExecute URI 열기 BrightHandler.cs (신규, prefix=bright): - WMI PowerShell subprocess (Get-CimInstance WmiMonitorBrightness) - bright 70 설정, bright up/down ±10% - 노트북 내장 디스플레이 전용, 외장 모니터 미지원 안내 - 밝기 바 시각화 (20칸 블록) PasteHandler.cs (신규, prefix=paste): - ClipboardHistoryService 의존성 주입 - paste 3 1 5 → 3번→1번→5번 순서대로 Ctrl+V - paste all → 최근 20개 항목 전체 순차 붙여넣기 - SendInput P/Invoke + AttachThreadInput 포커스 전환 - Raycast "Paste Sequentially" 대응 App.xaml.cs: L27 핸들러 5개 등록 (Vol/Qr/Meet/Bright/Paste) AxCopilot.csproj: QRCoder 1.6.0 패키지 참조 추가 LAUNCHER_ROADMAP.md: Phase L27 ✅ 완료 + 벤치마킹 공백 해소 표시 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Opus 4.6 --- docs/LAUNCHER_ROADMAP.md | 35 ++-- src/AxCopilot/App.xaml.cs | 13 ++ src/AxCopilot/AxCopilot.csproj | 1 + src/AxCopilot/Handlers/BrightHandler.cs | 153 ++++++++++++++++ src/AxCopilot/Handlers/MeetHandler.cs | 231 ++++++++++++++++++++++++ src/AxCopilot/Handlers/PasteHandler.cs | 217 ++++++++++++++++++++++ src/AxCopilot/Handlers/QrHandler.cs | 127 +++++++++++++ src/AxCopilot/Handlers/VolHandler.cs | 211 ++++++++++++++++++++++ 8 files changed, 970 insertions(+), 18 deletions(-) create mode 100644 src/AxCopilot/Handlers/BrightHandler.cs create mode 100644 src/AxCopilot/Handlers/MeetHandler.cs create mode 100644 src/AxCopilot/Handlers/PasteHandler.cs create mode 100644 src/AxCopilot/Handlers/QrHandler.cs create mode 100644 src/AxCopilot/Handlers/VolHandler.cs diff --git a/docs/LAUNCHER_ROADMAP.md b/docs/LAUNCHER_ROADMAP.md index 19161da..1a44413 100644 --- a/docs/LAUNCHER_ROADMAP.md +++ b/docs/LAUNCHER_ROADMAP.md @@ -2,7 +2,7 @@ ## 현재 상태 (v2.2.0) -### 핵심 기능 (114개 핸들러, L26 완료 / L27~L29 계획 중) +### 핵심 기능 (119개 핸들러, L27 완료 / L28~L29 계획 중) - 퍼지 검색 + 한글 초성 검색 (FuzzyEngine) + **최근 실행 지수 감소 랭킹 (30일 decay)** - 110개+ 프리픽스 명령 (계산기·이모지·웹검색·스니펫·클립보드·프로세스·데이터·네트워크·업무양식 등) - 10가지 테마 + 커스텀 테마 @@ -39,17 +39,17 @@ | **에이전트 코딩 루프** | ✅ 실구현 | 선언만 | ❌ | ❌ | ❌ | ❌ | | **사내 보안 AI 게이트** | ✅ **독점** | ❌ | ❌ | ❌ | ❌ | ❌ | | **클립보드 히스토리** | ✅ 핀·분류·이미지 | ✅ | ✅ | △ 플러그인 | ❌ | ❌ | -| **순차 붙여넣기** | ❌ **공백** | ✅ | ❌ | ❌ | ❌ | ❌ | +| **순차 붙여넣기** | ✅ (L27) | ✅ | ❌ | ❌ | ❌ | ❌ | | **스니펫 확장** | ✅ | ✅ 동적 플레이스홀더 | ✅ | △ 플러그인 | ❌ | ❌ | | **창 관리** | ✅ 22 레이아웃 | ✅ 70+ (Win 베타 탑재) | ❌ | ❌ | △ FancyZones 연동 | ❌ | | **파일 탐색기 인라인** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ **독점** | | **파일 미리보기** | △ 텍스트 6줄 | ❌ | ✅ Grid View | ❌ | ❌ | △ 사이드바 | | **브라우저 북마크 검색** | ❌ **공백** | ✅ 확장 연동 | ✅ | △ 플러그인 | ✅ 내장 | ❌ | | **브라우저 탭 AI 전달** | ❌ **공백** | ✅ {browser-tab} | ❌ | ❌ | ❌ | ❌ | -| **시스템 볼륨 제어** | ❌ **공백** | ✅ | ✅ | ❌ | ❌ | ❌ | -| **화면 밝기 제어** | ❌ **공백** | ✅ | ❌ | ❌ | ❌ | ❌ | -| **QR 코드 생성** | ❌ **공백** | ✅ | ✅ | △ 플러그인 | ❌ | ❌ | -| **회의 링크 빠른 열기** | △ QuickLink 수동 | ✅ Calendar 연동 | ✅ | ❌ | ❌ | ❌ | +| **시스템 볼륨 제어** | ✅ (L27) | ✅ | ✅ | ❌ | ❌ | ❌ | +| **화면 밝기 제어** | ✅ (L27) | ✅ | ❌ | ❌ | ❌ | ❌ | +| **QR 코드 생성** | ✅ (L27) | ✅ | ✅ | △ 플러그인 | ❌ | ❌ | +| **회의 링크 빠른 열기** | ✅ (L27) | ✅ Calendar 연동 | ✅ | ❌ | ❌ | ❌ | | **winget 앱 설치** | ❌ **공백** | ✅ Win 베타 | ❌ | △ 플러그인 | ❌ | ✅ 내장 | | **노코드 워크플로우** | ❌ **공백** | △ AI Ext 베타 | ✅ 완전 지원 | ❌ | ❌ | ❌ | | **스크립트 명령 실행** | ✅ ^ 프리픽스 | ✅ Script Commands | ✅ | ✅ | ✅ | ❌ | @@ -583,23 +583,22 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label --- -## Phase L27 — 경쟁 공백 즉시 해소: Windows 일상 제어 (v2.3.0) 📋 계획 +## Phase L27 — 경쟁 공백 즉시 해소: Windows 일상 제어 (v2.3.0) ✅ 완료 > **방향**: Raycast·Alfred·PowerToys가 공통 제공하나 AX에 없는 **일상 빈도 최상위 기능** 6종 일괄 구현. > 모두 외부 설치 의존 없이 Windows 네이티브 API + 로컬 파싱으로 구현. -| # | 기능 | 프리픽스 | 구현 방식 | 사무 적합성 | -|---|------|---------|---------|:---:| -| 📋 L27-1 | **브라우저 북마크 검색** | `bm` | Edge: `%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\Bookmarks` JSON 파싱. Chrome 동일 경로. 제목·URL·폴더 검색. Enter → 기본 브라우저로 열기 | ⭐⭐⭐ | -| 📋 L27-2 | **시스템 볼륨 제어** | `vol` | `Windows.Media.Audio` COM / `NAudio.CoreAudioApi` IAudioEndpointVolume. `vol 50` → 50% 설정, `vol up/down` → ±10%, `vol mute` → 토글. 현재 볼륨 실시간 표시 | ⭐⭐⭐ | -| 📋 L27-3 | **QR 코드 생성** | `qr` | QRCoder NuGet (순수 C# 행렬). `qr {텍스트/URL}` → 128×128 BitmapImage 결과 패널 인라인 표시. Enter → PNG 클립보드 복사. `save` → `%TEMP%\qr.png` 저장 후 탐색기 | ⭐⭐ | -| 📋 L27-4 | **회의 링크 전용 관리** | `meet` | `%APPDATA%\AxCopilot\meet.json` 로컬 저장. `meet add {이름} {URL}` 등록. 서비스 자동 감지 (Zoom/Teams/Google Meet/Webex). Enter → URI 실행. 빈 쿼리 → 전체 목록 | ⭐⭐⭐ | -| 📋 L27-5 | **화면 밝기 제어** | `bright` | WMI `WmiMonitorBrightnessMethods.WmiSetBrightness()`. `bright 70` → 70% 설정. `bright up/down` → ±10%. 노트북 내장 모니터 대상. 외장 모니터는 미지원 안내 | ⭐⭐ | -| 📋 L27-6 | **클립보드 순차 붙여넣기** | `paste` | 클립보드 히스토리 항목 N개를 번호 지정해 순서대로 전송 (`paste 3 1 5` → 3번→1번→5번 순). SendKeys 방식. Raycast "Paste Sequentially" 대응 기능 | ⭐⭐ | +| # | 기능 | 프리픽스 | 구현 방식 | +|---|------|---------|---------| +| ✅ L27-1 | **브라우저 북마크 검색** | (퍼지 통합) | `BookmarkHandler.cs` 기구현 — Edge/Chrome Bookmarks JSON 파싱, 5분 캐시 TTL, 제목·URL 검색, Enter → 기본 브라우저로 열기. 프리픽스 없이 일반 검색에 통합 | +| ✅ L27-2 | **시스템 볼륨 제어** | `vol` | `VolHandler.cs` — Windows Core Audio API COM 직접 호출 (IAudioEndpointVolume). `vol 50` 설정, `vol up/down` ±10%, `vol mute` 토글. 볼륨 바 시각화. NuGet 불필요 | +| ✅ L27-3 | **QR 코드 생성** | `qr` | `QrHandler.cs` — QRCoder NuGet (순수 C# QR 행렬). `qr {텍스트}` → Enter: PNG 클립보드 복사. `qr save {텍스트}` → TEMP 저장 후 탐색기 열기 | +| ✅ L27-4 | **회의 링크 전용 관리** | `meet` | `MeetHandler.cs` — `%APPDATA%\AxCopilot\meet.json` 로컬 저장. `meet add/del` CRUD. 서비스 자동 감지 (Zoom/Teams/Google Meet/Webex/Discord/Slack). Enter → URI 열기 | +| ✅ L27-5 | **화면 밝기 제어** | `bright` | `BrightHandler.cs` — WMI PowerShell subprocess (Get-CimInstance WmiMonitorBrightness). 노트북 내장 디스플레이. `bright 70` 설정, `bright up/down` ±10%. 외장 모니터 미지원 안내 | +| ✅ L27-6 | **클립보드 순차 붙여넣기** | `paste` | `PasteHandler.cs` — ClipboardHistoryService 연동. `paste 3 1 5` → 3번→1번→5번 순서대로 Ctrl+V. `paste all` 전체 순차 붙여넣기. SendInput P/Invoke 방식 | -**구현 순서**: L27-1(bm) → L27-4(meet) → L27-3(qr) → L27-2(vol) → L27-5(bright) → L27-6(paste) -**NuGet 추가**: `QRCoder` (순수 C#, 외부 의존 없음, ~150KB DLL) -**예상 핸들러 파일**: BookmarkHandler.cs, MeetHandler.cs, QrHandler.cs, VolHandler.cs, BrightHandler.cs — PasteHandler.cs 또는 ClipboardHistoryHandler 확장 +**NuGet 추가**: `QRCoder 1.6.0` (순수 C#, 외부 의존 없음, ~150KB DLL) +**빌드**: 경고 0, 오류 0 --- diff --git a/src/AxCopilot/App.xaml.cs b/src/AxCopilot/App.xaml.cs index 60a878d..0d4146d 100644 --- a/src/AxCopilot/App.xaml.cs +++ b/src/AxCopilot/App.xaml.cs @@ -372,6 +372,19 @@ public partial class App : System.Windows.Application // L25-4: 오늘 업무 통합 뷰 (prefix=today) commandResolver.RegisterHandler(new TodayHandler()); + // ─── L27: 경쟁 공백 즉시 해소 ──────────────────────────────────────── + // L27-1: 브라우저 북마크 검색 — BookmarkHandler (기구현, prefix=null 퍼지 통합) + // L27-2: 시스템 볼륨 제어 (prefix=vol) + commandResolver.RegisterHandler(new VolHandler()); + // L27-3: QR 코드 생성 (prefix=qr) + commandResolver.RegisterHandler(new QrHandler()); + // L27-4: 회의 링크 관리 (prefix=meet) + commandResolver.RegisterHandler(new MeetHandler()); + // L27-5: 화면 밝기 제어 (prefix=bright) + commandResolver.RegisterHandler(new BrightHandler()); + // L27-6: 클립보드 순차 붙여넣기 (prefix=paste) + commandResolver.RegisterHandler(new PasteHandler(_clipboardHistory)); + // ─── 플러그인 로드 ──────────────────────────────────────────────────── var pluginHost = new PluginHost(settings, commandResolver); pluginHost.LoadAll(); diff --git a/src/AxCopilot/AxCopilot.csproj b/src/AxCopilot/AxCopilot.csproj index 3ade69a..3621692 100644 --- a/src/AxCopilot/AxCopilot.csproj +++ b/src/AxCopilot/AxCopilot.csproj @@ -66,6 +66,7 @@ + diff --git a/src/AxCopilot/Handlers/BrightHandler.cs b/src/AxCopilot/Handlers/BrightHandler.cs new file mode 100644 index 0000000..60665d2 --- /dev/null +++ b/src/AxCopilot/Handlers/BrightHandler.cs @@ -0,0 +1,153 @@ +using System.Diagnostics; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +/// +/// L27-5: 화면 밝기 제어 핸들러. "bright" 프리픽스로 사용합니다. +/// +/// 예: bright → 현재 밝기 표시 +/// bright 70 → 밝기 70% 설정 +/// bright up / down → ±10% 조절 +/// Enter → 해당 밝기로 설정. +/// WMI (WmiMonitorBrightness) 사용 — 노트북 내장 디스플레이 대상. +/// +public class BrightHandler : IActionHandler +{ + public string? Prefix => "bright"; + + public PluginMetadata Metadata => new( + "밝기 제어", + "화면 밝기 조절 — 설정·증감 (노트북)", + "1.0", + "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + var q = query.Trim().ToLowerInvariant(); + var items = new List(); + + // 현재 밝기 읽기 + int current = GetCurrentBrightness(); + if (current < 0) + { + items.Add(new LauncherItem( + "밝기 센서를 찾을 수 없습니다", + "노트북 내장 디스플레이에서만 동작합니다 (외장 모니터 미지원)", + null, null, Symbol: "\uE7BA")); + return Task.FromResult>(items); + } + + var bar = BrightnessBar(current); + + if (string.IsNullOrWhiteSpace(q)) + { + items.Add(new LauncherItem( + $"현재 밝기: {current}%", + $"{bar} · bright 70 / bright up / bright down", + null, null, Symbol: "\uE706")); + + items.Add(new LauncherItem("bright up", "밝기 +10%", null, ("set", Math.Min(current + 10, 100)), Symbol: "\uE706")); + items.Add(new LauncherItem("bright down", "밝기 −10%", null, ("set", Math.Max(current - 10, 0)), Symbol: "\uE706")); + + foreach (var p in new[] { 10, 25, 50, 75, 100 }) + items.Add(new LauncherItem($"bright {p}", $"밝기 {p}%", null, ("set", p), Symbol: "\uE706")); + + return Task.FromResult>(items); + } + + if (q is "up" or "올려" or "+") + { + var target = Math.Min(current + 10, 100); + items.Add(new LauncherItem($"밝기 +10% → {target}%", BrightnessBar(target), null, ("set", target), Symbol: "\uE706")); + } + else if (q is "down" or "내려" or "-") + { + var target = Math.Max(current - 10, 0); + items.Add(new LauncherItem($"밝기 −10% → {target}%", BrightnessBar(target), null, ("set", target), Symbol: "\uE706")); + } + else if (int.TryParse(q, out int val) && val is >= 0 and <= 100) + { + items.Add(new LauncherItem( + $"밝기 {val}% 설정", + $"{BrightnessBar(val)} (현재 {current}%)", + null, ("set", val), Symbol: "\uE706")); + } + else + { + items.Add(new LauncherItem($"'{query}' — 알 수 없는 명령", + "사용법: bright 70 / bright up / bright down", + null, null, Symbol: "\uE7BA")); + } + + return Task.FromResult>(items); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is ("set", int level)) + { + bool ok = SetBrightness(level); + if (ok) + NotificationService.Notify("bright", $"밝기 {level}%"); + else + NotificationService.Notify("bright", "밝기 설정 실패 — 노트북 내장 디스플레이에서만 동작합니다."); + } + return Task.CompletedTask; + } + + private static string BrightnessBar(int pct) + { + int filled = pct / 5; + return "[" + new string('█', filled) + new string('░', 20 - filled) + "]"; + } + + // ─── WMI 밝기 읽기/쓰기 (PowerShell subprocess) ────────────────────────── + + private static int GetCurrentBrightness() + { + try + { + var output = RunPowerShell( + "(Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightness -ErrorAction Stop).CurrentBrightness"); + if (int.TryParse(output.Trim(), out int val)) + return val; + } + catch { } + return -1; + } + + private static bool SetBrightness(int level) + { + try + { + level = Math.Clamp(level, 0, 100); + var cmd = $"$m = Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightnessMethods -ErrorAction Stop; " + + $"Invoke-CimMethod -InputObject $m -MethodName WmiSetBrightness -Arguments @{{Timeout=1; Brightness={level}}}"; + RunPowerShell(cmd); + return true; + } + catch { return false; } + } + + private static string RunPowerShell(string command) + { + using var proc = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = $"-NoProfile -NonInteractive -Command \"{command}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + proc.Start(); + var output = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(3000); + return output; + } +} diff --git a/src/AxCopilot/Handlers/MeetHandler.cs b/src/AxCopilot/Handlers/MeetHandler.cs new file mode 100644 index 0000000..157c680 --- /dev/null +++ b/src/AxCopilot/Handlers/MeetHandler.cs @@ -0,0 +1,231 @@ +using System.IO; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Windows; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +/// +/// L27-4: 회의 링크 전용 관리 핸들러. "meet" 프리픽스로 사용합니다. +/// +/// 예: meet → 전체 회의 목록 +/// meet 스탠드업 → 이름 검색 +/// meet add 스탠드업 https://... → 회의 추가 +/// meet del 스탠드업 → 회의 삭제 +/// Enter → 기본 브라우저로 회의 링크 열기. +/// 저장: %APPDATA%\AxCopilot\meet.json +/// +public class MeetHandler : IActionHandler +{ + public string? Prefix => "meet"; + + public PluginMetadata Metadata => new( + "회의 링크", + "회의 링크 관리 — 추가 · 검색 · 즉시 열기", + "1.0", + "AX"); + + private record MeetEntry( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("service")] string Service); + + private static readonly string DataPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "AxCopilot", "meet.json"); + + private static readonly JsonSerializerOptions JsonOpt = new() + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + var q = query.Trim(); + var items = new List(); + var meets = Load(); + + // ── add 명령 ───────────────────────────────────────────────────────── + if (q.StartsWith("add ", StringComparison.OrdinalIgnoreCase)) + { + var parts = q[4..].Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 2 || !Uri.TryCreate(parts[1], UriKind.Absolute, out _)) + { + items.Add(new LauncherItem("사용법: meet add {이름} {URL}", + "예: meet add 스탠드업 https://teams.microsoft.com/...", + null, null, Symbol: "\uE710")); + } + else + { + var name = parts[0]; + var url = parts[1]; + var svc = DetectService(url); + items.Add(new LauncherItem( + $"회의 추가: {name}", + $"{svc} · {url}", + null, ("add", $"{name}\t{url}\t{svc}"), Symbol: "\uE710")); + } + return Task.FromResult>(items); + } + + // ── del 명령 ───────────────────────────────────────────────────────── + if (q.StartsWith("del ", StringComparison.OrdinalIgnoreCase)) + { + var name = q[4..].Trim(); + var found = meets.FirstOrDefault(m => + m.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + if (found != null) + { + items.Add(new LauncherItem( + $"회의 삭제: {found.Name}", + $"{found.Service} · {found.Url}", + null, ("del", found.Name), Symbol: "\uE74D")); + } + else + { + items.Add(new LauncherItem($"'{name}' 회의를 찾을 수 없습니다", + "meet del {이름}", null, null, Symbol: "\uE783")); + } + return Task.FromResult>(items); + } + + // ── 빈 쿼리 → 전체 목록 ────────────────────────────────────────────── + if (string.IsNullOrWhiteSpace(q)) + { + if (meets.Count == 0) + { + items.Add(new LauncherItem("등록된 회의가 없습니다", + "meet add {이름} {URL} 로 추가하세요", + null, null, Symbol: "\uE8D6")); + return Task.FromResult>(items); + } + + items.Add(new LauncherItem( + $"회의 {meets.Count}개 등록됨", + "Enter: 브라우저로 열기 · meet add/del 로 관리", + null, null, Symbol: "\uE8D6")); + + foreach (var m in meets) + items.Add(MeetItem(m)); + + return Task.FromResult>(items); + } + + // ── 검색 ────────────────────────────────────────────────────────────── + var searched = meets.Where(m => + m.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || + m.Service.Contains(q, StringComparison.OrdinalIgnoreCase) || + m.Url.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (searched.Count == 0) + { + items.Add(new LauncherItem($"'{q}' 회의를 찾을 수 없습니다", + "meet add {이름} {URL} 로 추가하세요", + null, null, Symbol: "\uE783")); + } + else + { + foreach (var m in searched) + items.Add(MeetItem(m)); + } + + return Task.FromResult>(items); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is ("add", string addData)) + { + var parts = addData.Split('\t'); + if (parts.Length >= 3) + { + var meets = Load(); + meets.RemoveAll(m => m.Name.Equals(parts[0], StringComparison.OrdinalIgnoreCase)); + meets.Add(new MeetEntry(parts[0], parts[1], parts[2])); + Save(meets); + NotificationService.Notify("meet", $"'{parts[0]}' 회의가 추가되었습니다."); + } + } + else if (item.Data is ("del", string delName)) + { + var meets = Load(); + int removed = meets.RemoveAll(m => m.Name.Equals(delName, StringComparison.OrdinalIgnoreCase)); + Save(meets); + if (removed > 0) + NotificationService.Notify("meet", $"'{delName}' 회의가 삭제되었습니다."); + } + else if (item.Data is ("open", string url)) + { + try + { + System.Diagnostics.Process.Start( + new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true }); + } + catch (Exception ex) + { + NotificationService.Notify("meet", $"열기 실패: {ex.Message}"); + } + } + + return Task.CompletedTask; + } + + // ─── 헬퍼 ──────────────────────────────────────────────────────────────── + + private static LauncherItem MeetItem(MeetEntry m) + { + var icon = m.Service switch + { + "Zoom" => "\uE774", + "Teams" => "\uE8D6", + "Google Meet" => "\uE774", + "Webex" => "\uE774", + _ => "\uE774" + }; + + return new LauncherItem( + $"{m.Name} [{m.Service}]", + m.Url, + null, ("open", m.Url), Symbol: icon); + } + + private static string DetectService(string url) + { + var lower = url.ToLowerInvariant(); + if (lower.Contains("zoom.us") || lower.Contains("zoom.com")) return "Zoom"; + if (lower.Contains("teams.microsoft.com") || lower.Contains("teams.live.com")) return "Teams"; + if (lower.Contains("meet.google.com")) return "Google Meet"; + if (lower.Contains("webex.com")) return "Webex"; + if (lower.Contains("discord.gg") || lower.Contains("discord.com")) return "Discord"; + if (lower.Contains("slack.com")) return "Slack"; + return "기타"; + } + + // ─── JSON 파일 I/O ─────────────────────────────────────────────────────── + + private static List Load() + { + try + { + if (!File.Exists(DataPath)) return []; + var json = File.ReadAllText(DataPath); + return JsonSerializer.Deserialize>(json) ?? []; + } + catch { return []; } + } + + private static void Save(List list) + { + try + { + var dir = Path.GetDirectoryName(DataPath)!; + if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); + File.WriteAllText(DataPath, JsonSerializer.Serialize(list, JsonOpt)); + } + catch { } + } +} diff --git a/src/AxCopilot/Handlers/PasteHandler.cs b/src/AxCopilot/Handlers/PasteHandler.cs new file mode 100644 index 0000000..4c095a8 --- /dev/null +++ b/src/AxCopilot/Handlers/PasteHandler.cs @@ -0,0 +1,217 @@ +using System.Runtime.InteropServices; +using System.Windows; +using AxCopilot.SDK; +using AxCopilot.Services; +using AxCopilot.Themes; + +namespace AxCopilot.Handlers; + +/// +/// L27-6: 클립보드 순차 붙여넣기 핸들러. "paste" 프리픽스로 사용합니다. +/// +/// 예: paste → 번호 매긴 클립보드 히스토리 목록 +/// paste 3 1 5 → 3번→1번→5번 항목을 순서대로 붙여넣기 +/// paste all → 전체 히스토리를 순서대로 붙여넣기 +/// Enter → 이전 창에서 순서대로 Ctrl+V 실행. +/// Raycast "Paste Sequentially" 대응. +/// +public class PasteHandler : IActionHandler +{ + private readonly ClipboardHistoryService _history; + + public string? Prefix => "paste"; + + public PluginMetadata Metadata => new( + "순차 붙여넣기", + "클립보드 히스토리를 순서대로 붙여넣기 (Paste Sequentially)", + "1.0", + "AX"); + + public PasteHandler(ClipboardHistoryService historyService) + { + _history = historyService; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + var q = query.Trim(); + var items = new List(); + var history = _history.History.Where(e => e.IsText && !string.IsNullOrEmpty(e.Text)).ToList(); + + if (history.Count == 0) + { + items.Add(new LauncherItem( + "클립보드 히스토리가 비어 있습니다", + "텍스트를 복사하면 사용할 수 있습니다", + null, null, Symbol: Symbols.ClipPaste)); + return Task.FromResult>(items); + } + + // ── 번호 시퀀스 파싱 ────────────────────────────────────────────────── + if (!string.IsNullOrWhiteSpace(q) && q != "all") + { + var nums = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var indices = new List(); + foreach (var n in nums) + { + if (int.TryParse(n, out int idx) && idx >= 1 && idx <= history.Count) + indices.Add(idx); + } + + if (indices.Count > 0) + { + var preview = string.Join(" → ", indices.Select(i => $"#{i}")); + var texts = indices.Select(i => history[i - 1].Text ?? "").ToList(); + var totalLen = texts.Sum(t => t.Length); + + items.Add(new LauncherItem( + $"순차 붙여넣기: {preview}", + $"{indices.Count}개 항목 · {totalLen}자 · Enter: 순서대로 붙여넣기", + null, ("seq", texts), Symbol: Symbols.ClipPaste)); + + // 미리보기 + for (int i = 0; i < indices.Count; i++) + { + var entry = history[indices[i] - 1]; + items.Add(new LauncherItem( + $" {i + 1}. #{indices[i]}: {Truncate(entry.Preview, 60)}", + entry.RelativeTime, + null, null, Symbol: Symbols.History)); + } + + return Task.FromResult>(items); + } + } + + // ── all 명령 ────────────────────────────────────────────────────────── + if (q.Equals("all", StringComparison.OrdinalIgnoreCase)) + { + var texts = history.Take(20).Select(e => e.Text ?? "").ToList(); + items.Add(new LauncherItem( + $"전체 순차 붙여넣기 ({texts.Count}개)", + $"Enter: 최근 {texts.Count}개 항목을 순서대로 붙여넣기", + null, ("seq", texts), Symbol: Symbols.ClipPaste)); + return Task.FromResult>(items); + } + + // ── 빈 쿼리 → 번호 매긴 목록 ───────────────────────────────────────── + items.Add(new LauncherItem( + "순차 붙여넣기 — 번호를 입력하세요", + "예: paste 3 1 5 → 3번→1번→5번 순서로 붙여넣기 · paste all → 전체", + null, null, Symbol: Symbols.ClipPaste)); + + for (int i = 0; i < Math.Min(history.Count, 15); i++) + { + var entry = history[i]; + var pinMark = entry.IsPinned ? "\uD83D\uDCCC " : ""; + items.Add(new LauncherItem( + $" #{i + 1} {pinMark}{Truncate(entry.Preview, 50)}", + $"{entry.RelativeTime} · {entry.CopiedAt:MM/dd HH:mm}", + null, ("single", entry.Text ?? ""), Symbol: Symbols.History)); + } + + return Task.FromResult>(items); + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is ("single", string singleText)) + { + // 단일 항목 붙여넣기 + await PasteTexts([singleText], ct); + } + else if (item.Data is ("seq", List texts)) + { + // 순차 붙여넣기 + await PasteTexts(texts, ct); + } + } + + private async Task PasteTexts(List texts, CancellationToken ct) + { + if (texts.Count == 0) return; + + try + { + var prevWindow = WindowTracker.PreviousWindow; + if (prevWindow == IntPtr.Zero) return; + + _history.SuppressNextCapture(); + + // 이전 창 포커스 복원 대기 + await Task.Delay(300, ct); + + var targetThread = GetWindowThreadProcessId(prevWindow, out _); + var currentThread = GetCurrentThreadId(); + AttachThreadInput(currentThread, targetThread, true); + SetForegroundWindow(prevWindow); + AttachThreadInput(currentThread, targetThread, false); + + await Task.Delay(100, ct); + + foreach (var text in texts) + { + if (ct.IsCancellationRequested) break; + + _history.SuppressNextCapture(); + Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text)); + await Task.Delay(50, ct); + + SendCtrlV(); + await Task.Delay(200, ct); // 항목 간 간격 + } + + NotificationService.Notify("paste", $"{texts.Count}개 항목 붙여넣기 완료"); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + NotificationService.Notify("paste", $"붙여넣기 실패: {ex.Message}"); + } + } + + private static string Truncate(string s, int max) + => s.Length <= max ? s : s[..max] + "…"; + + // ─── Ctrl+V 주입 (ClipboardHistoryHandler와 동일 패턴) ──────────────────── + + private static void SendCtrlV() + { + const uint INPUT_KEYBOARD = 1; + const uint KEYEVENTF_KEYUP = 0x0002; + const ushort VK_CONTROL = 0x11; + const ushort VK_V = 0x56; + + var inputs = new INPUT[4]; + inputs[0] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_CONTROL } }; + inputs[1] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_V } }; + inputs[2] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_V, dwFlags = KEYEVENTF_KEYUP } }; + inputs[3] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_CONTROL, dwFlags = KEYEVENTF_KEYUP } }; + SendInput((uint)inputs.Length, inputs, Marshal.SizeOf()); + } + + // ─── P/Invoke ────────────────────────────────────────────────────────────── + + [StructLayout(LayoutKind.Explicit, Size = 40)] + private struct INPUT + { + [FieldOffset(0)] public uint Type; + [FieldOffset(8)] public KEYBDINPUT ki; + } + + [StructLayout(LayoutKind.Sequential)] + private struct KEYBDINPUT + { + public ushort wVk; + public ushort wScan; + public uint dwFlags; + public uint time; + public IntPtr dwExtraInfo; + } + + [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + [DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach); + [DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId(); + [DllImport("user32.dll")] private static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray)] INPUT[] pInputs, int cbSize); +} diff --git a/src/AxCopilot/Handlers/QrHandler.cs b/src/AxCopilot/Handlers/QrHandler.cs new file mode 100644 index 0000000..f6825be --- /dev/null +++ b/src/AxCopilot/Handlers/QrHandler.cs @@ -0,0 +1,127 @@ +using System.IO; +using System.Windows; +using System.Windows.Media.Imaging; +using AxCopilot.SDK; +using AxCopilot.Services; +using QRCoder; + +namespace AxCopilot.Handlers; + +/// +/// L27-3: QR 코드 생성 핸들러. "qr" 프리픽스로 사용합니다. +/// +/// 예: qr https://google.com → QR 코드 생성 (Enter: 클립보드 복사) +/// qr 안녕하세요 → 한국어 텍스트 QR 생성 +/// qr save https://... → QR PNG 파일 저장 +/// Enter → QR 이미지를 클립보드에 복사 (붙여넣기로 사용). +/// +public class QrHandler : IActionHandler +{ + public string? Prefix => "qr"; + + public PluginMetadata Metadata => new( + "QR 코드", + "QR 코드 생성 — 텍스트/URL → 클립보드 PNG 복사", + "1.0", + "AX"); + + private const int PixelsPerModule = 10; + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + var q = query.Trim(); + var items = new List(); + + if (string.IsNullOrWhiteSpace(q)) + { + items.Add(new LauncherItem( + "QR 코드 생성", + "qr {텍스트 또는 URL} → Enter: QR PNG 클립보드 복사", + null, null, Symbol: "\uE8C8")); + return Task.FromResult>(items); + } + + // save 명령 감지 + bool saveToFile = false; + var text = q; + if (q.StartsWith("save ", StringComparison.OrdinalIgnoreCase)) + { + saveToFile = true; + text = q[5..].Trim(); + } + + if (string.IsNullOrWhiteSpace(text)) + { + items.Add(new LauncherItem("QR 생성할 텍스트를 입력하세요", + "qr {텍스트} 또는 qr save {텍스트}", null, null, Symbol: "\uE8C8")); + return Task.FromResult>(items); + } + + int byteLen = System.Text.Encoding.UTF8.GetByteCount(text); + var typeHint = Uri.TryCreate(text, UriKind.Absolute, out _) ? "URL" : "텍스트"; + + items.Add(new LauncherItem( + $"QR 생성: {(text.Length > 60 ? text[..60] + "…" : text)}", + saveToFile + ? $"{typeHint} · {byteLen}바이트 · Enter: PNG 파일 저장" + : $"{typeHint} · {byteLen}바이트 · Enter: PNG 클립보드 복사", + null, (saveToFile ? "save" : "copy", text), Symbol: "\uE8C8")); + + if (!saveToFile) + { + items.Add(new LauncherItem( + "qr save ...", + "PNG 파일로 저장하려면 qr save {텍스트} 입력", + null, null, Symbol: "\uE74E")); + } + + return Task.FromResult>(items); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is not (string action, string text)) return Task.CompletedTask; + + try + { + byte[] png = GenerateQrPng(text); + + if (action == "save") + { + var path = Path.Combine(Path.GetTempPath(), $"qr_{DateTime.Now:yyyyMMdd_HHmmss}.png"); + File.WriteAllBytes(path, png); + // 탐색기에서 파일 선택 상태로 열기 + System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\""); + NotificationService.Notify("qr", $"QR 저장: {path}"); + } + else + { + // PNG → BitmapImage → 클립보드 + using var ms = new MemoryStream(png); + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.StreamSource = ms; + bitmap.EndInit(); + bitmap.Freeze(); + + Application.Current.Dispatcher.Invoke(() => Clipboard.SetImage(bitmap)); + NotificationService.Notify("qr", "QR 이미지가 클립보드에 복사되었습니다."); + } + } + catch (Exception ex) + { + NotificationService.Notify("qr", $"QR 생성 실패: {ex.Message}"); + } + + return Task.CompletedTask; + } + + private static byte[] GenerateQrPng(string text) + { + using var generator = new QRCodeGenerator(); + using var data = generator.CreateQrCode(text, QRCodeGenerator.ECCLevel.M); + using var code = new PngByteQRCode(data); + return code.GetGraphic(PixelsPerModule); + } +} diff --git a/src/AxCopilot/Handlers/VolHandler.cs b/src/AxCopilot/Handlers/VolHandler.cs new file mode 100644 index 0000000..3cfe459 --- /dev/null +++ b/src/AxCopilot/Handlers/VolHandler.cs @@ -0,0 +1,211 @@ +using System.Runtime.InteropServices; +using System.Windows; +using AxCopilot.SDK; +using AxCopilot.Services; +using AxCopilot.Themes; + +namespace AxCopilot.Handlers; + +/// +/// L27-2: 시스템 볼륨 제어 핸들러. "vol" 프리픽스로 사용합니다. +/// +/// 예: vol → 현재 볼륨 표시 +/// vol 50 → 볼륨 50% 설정 +/// vol up / down → ±10% 조절 +/// vol mute → 음소거 토글 +/// Enter → 해당 명령 실행. +/// Windows Core Audio API (IAudioEndpointVolume) COM 인터페이스 사용. +/// +public class VolHandler : IActionHandler +{ + public string? Prefix => "vol"; + + public PluginMetadata Metadata => new( + "볼륨 제어", + "시스템 볼륨 조절 — 설정·증감·음소거", + "1.0", + "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + var q = query.Trim().ToLowerInvariant(); + var items = new List(); + + // 현재 볼륨 읽기 + float curLevel; + bool curMuted; + try + { + using var audio = AudioEndpoint.GetDefault(); + curLevel = audio.GetVolume(); + curMuted = audio.GetMute(); + } + catch + { + items.Add(new LauncherItem( + "오디오 장치를 찾을 수 없습니다", + "기본 재생 장치가 연결되어 있는지 확인하세요", + null, null, Symbol: Symbols.Warning)); + return Task.FromResult>(items); + } + + int pct = (int)Math.Round(curLevel * 100); + var bar = VolumeBar(pct); + var muteLabel = curMuted ? " 🔇 음소거" : ""; + var symbol = curMuted ? Symbols.VolumeMute + : pct == 0 ? Symbols.VolumeMute + : pct < 50 ? Symbols.VolumeDown + : Symbols.VolumeUp; + + if (string.IsNullOrWhiteSpace(q)) + { + items.Add(new LauncherItem( + $"현재 볼륨: {pct}%{muteLabel}", + $"{bar} · vol 50 / vol up / vol down / vol mute", + null, null, Symbol: symbol)); + + items.Add(new LauncherItem("vol up", "볼륨 +10%", null, ("set", Math.Min(pct + 10, 100)), Symbol: Symbols.VolumeUp)); + items.Add(new LauncherItem("vol down", "볼륨 −10%", null, ("set", Math.Max(pct - 10, 0)), Symbol: Symbols.VolumeDown)); + items.Add(new LauncherItem("vol mute", curMuted ? "음소거 해제" : "음소거", null, ("mute", !curMuted), Symbol: Symbols.VolumeMute)); + items.Add(new LauncherItem("vol 0", "볼륨 0% (무음)", null, ("set", 0), Symbol: Symbols.VolumeMute)); + items.Add(new LauncherItem("vol 50", "볼륨 50%", null, ("set", 50), Symbol: Symbols.VolumeDown)); + items.Add(new LauncherItem("vol 100", "볼륨 100%", null, ("set", 100), Symbol: Symbols.VolumeUp)); + return Task.FromResult>(items); + } + + // 명령 파싱 + if (q is "up" or "올려" or "+") + { + var target = Math.Min(pct + 10, 100); + items.Add(new LauncherItem($"볼륨 +10% → {target}%", $"{VolumeBar(target)}", null, ("set", target), Symbol: Symbols.VolumeUp)); + } + else if (q is "down" or "내려" or "-") + { + var target = Math.Max(pct - 10, 0); + items.Add(new LauncherItem($"볼륨 −10% → {target}%", $"{VolumeBar(target)}", null, ("set", target), Symbol: Symbols.VolumeDown)); + } + else if (q is "mute" or "음소거" or "m") + { + items.Add(new LauncherItem( + curMuted ? "음소거 해제" : "음소거 설정", + $"현재: {pct}%{muteLabel}", + null, ("mute", !curMuted), Symbol: Symbols.VolumeMute)); + } + else if (int.TryParse(q, out int val) && val is >= 0 and <= 100) + { + items.Add(new LauncherItem( + $"볼륨 {val}% 설정", + $"{VolumeBar(val)} (현재 {pct}%)", + null, ("set", val), Symbol: val > pct ? Symbols.VolumeUp : Symbols.VolumeDown)); + } + else + { + items.Add(new LauncherItem( + $"'{query}' — 알 수 없는 명령", + "사용법: vol 50 / vol up / vol down / vol mute", + null, null, Symbol: Symbols.Warning)); + } + + return Task.FromResult>(items); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + try + { + using var audio = AudioEndpoint.GetDefault(); + + if (item.Data is ("set", int level)) + { + audio.SetVolume(level / 100f); + if (audio.GetMute()) audio.SetMute(false); + NotificationService.Notify("vol", $"볼륨 {level}%"); + } + else if (item.Data is ("mute", bool mute)) + { + audio.SetMute(mute); + NotificationService.Notify("vol", mute ? "음소거" : "음소거 해제"); + } + } + catch (Exception ex) + { + NotificationService.Notify("vol", $"오류: {ex.Message}"); + } + return Task.CompletedTask; + } + + private static string VolumeBar(int pct) + { + int filled = pct / 5; // 0~20 + return "[" + new string('█', filled) + new string('░', 20 - filled) + "]"; + } + + // ─── Core Audio API COM 래퍼 ───────────────────────────────────────────── + + private sealed class AudioEndpoint : IDisposable + { + private readonly IAudioEndpointVolume _vol; + + private AudioEndpoint(IAudioEndpointVolume vol) => _vol = vol; + + public static AudioEndpoint GetDefault() + { + var enumerator = (IMMDeviceEnumerator)new MMDeviceEnumeratorClass(); + enumerator.GetDefaultAudioEndpoint(0 /*eRender*/, 1 /*eMultimedia*/, out var device); + var iid = typeof(IAudioEndpointVolume).GUID; + device.Activate(ref iid, 1 /*CLSCTX_ALL*/, IntPtr.Zero, out var obj); + return new AudioEndpoint((IAudioEndpointVolume)obj); + } + + public float GetVolume() { _vol.GetMasterVolumeLevelScalar(out float l); return l; } + public bool GetMute() { _vol.GetMute(out bool m); return m; } + public void SetVolume(float level) { var g = Guid.Empty; _vol.SetMasterVolumeLevelScalar(Math.Clamp(level, 0f, 1f), ref g); } + public void SetMute(bool mute) { var g = Guid.Empty; _vol.SetMute(mute, ref g); } + + public void Dispose() + { + if (_vol is IDisposable d) d.Dispose(); + if (_vol != null) Marshal.ReleaseComObject(_vol); + } + } + + // ─── COM 인터페이스 정의 (Windows Core Audio API) ────────────────────── + + [ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] + private class MMDeviceEnumeratorClass { } + + [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IMMDeviceEnumerator + { + int EnumAudioEndpoints(int dataFlow, uint stateMask, out IntPtr devices); + int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice device); + } + + [Guid("D666063F-1587-4E43-81F1-B948E807363F")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IMMDevice + { + int Activate(ref Guid iid, uint clsCtx, IntPtr activationParams, + [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface); + } + + [Guid("5CDF2C82-841E-4546-9722-0CF74078229A")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IAudioEndpointVolume + { + int RegisterControlChangeNotify(IntPtr pNotify); + int UnregisterControlChangeNotify(IntPtr pNotify); + int GetChannelCount(out uint pnChannelCount); + int SetMasterVolumeLevel(float fLevelDB, ref Guid pguidEventContext); + int SetMasterVolumeLevelScalar(float fLevel, ref Guid pguidEventContext); + int GetMasterVolumeLevel(out float pfLevelDB); + int GetMasterVolumeLevelScalar(out float pfLevel); + int SetChannelVolumeLevel(uint nChannel, float fLevelDB, ref Guid pguidEventContext); + int SetChannelVolumeLevelScalar(uint nChannel, float fLevel, ref Guid pguidEventContext); + int GetChannelVolumeLevel(uint nChannel, out float pfLevelDB); + int GetChannelVolumeLevelScalar(uint nChannel, out float pfLevel); + int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, ref Guid pguidEventContext); + int GetMute([MarshalAs(UnmanagedType.Bool)] out bool pbMute); + } +}