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);
+ }
+}