[Phase L27] 경쟁 공백 즉시 해소 — Windows 일상 제어 도구 5종 구현

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 19:37:52 +09:00
parent 837cc0692b
commit a67cdf574d
8 changed files with 970 additions and 18 deletions

View File

@@ -2,7 +2,7 @@
## 현재 상태 (v2.2.0) ## 현재 상태 (v2.2.0)
### 핵심 기능 (114개 핸들러, L26 완료 / L27~L29 계획 중) ### 핵심 기능 (119개 핸들러, L27 완료 / L28~L29 계획 중)
- 퍼지 검색 + 한글 초성 검색 (FuzzyEngine) + **최근 실행 지수 감소 랭킹 (30일 decay)** - 퍼지 검색 + 한글 초성 검색 (FuzzyEngine) + **최근 실행 지수 감소 랭킹 (30일 decay)**
- 110개+ 프리픽스 명령 (계산기·이모지·웹검색·스니펫·클립보드·프로세스·데이터·네트워크·업무양식 등) - 110개+ 프리픽스 명령 (계산기·이모지·웹검색·스니펫·클립보드·프로세스·데이터·네트워크·업무양식 등)
- 10가지 테마 + 커스텀 테마 - 10가지 테마 + 커스텀 테마
@@ -39,17 +39,17 @@
| **에이전트 코딩 루프** | ✅ 실구현 | 선언만 | ❌ | ❌ | ❌ | ❌ | | **에이전트 코딩 루프** | ✅ 실구현 | 선언만 | ❌ | ❌ | ❌ | ❌ |
| **사내 보안 AI 게이트** | ✅ **독점** | ❌ | ❌ | ❌ | ❌ | ❌ | | **사내 보안 AI 게이트** | ✅ **독점** | ❌ | ❌ | ❌ | ❌ | ❌ |
| **클립보드 히스토리** | ✅ 핀·분류·이미지 | ✅ | ✅ | △ 플러그인 | ❌ | ❌ | | **클립보드 히스토리** | ✅ 핀·분류·이미지 | ✅ | ✅ | △ 플러그인 | ❌ | ❌ |
| **순차 붙여넣기** | **공백** | ✅ | ❌ | ❌ | ❌ | ❌ | | **순차 붙여넣기** | ✅ (L27) | ✅ | ❌ | ❌ | ❌ | ❌ |
| **스니펫 확장** | ✅ | ✅ 동적 플레이스홀더 | ✅ | △ 플러그인 | ❌ | ❌ | | **스니펫 확장** | ✅ | ✅ 동적 플레이스홀더 | ✅ | △ 플러그인 | ❌ | ❌ |
| **창 관리** | ✅ 22 레이아웃 | ✅ 70+ (Win 베타 탑재) | ❌ | ❌ | △ FancyZones 연동 | ❌ | | **창 관리** | ✅ 22 레이아웃 | ✅ 70+ (Win 베타 탑재) | ❌ | ❌ | △ FancyZones 연동 | ❌ |
| **파일 탐색기 인라인** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ **독점** | | **파일 탐색기 인라인** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ **독점** |
| **파일 미리보기** | △ 텍스트 6줄 | ❌ | ✅ Grid View | ❌ | ❌ | △ 사이드바 | | **파일 미리보기** | △ 텍스트 6줄 | ❌ | ✅ Grid View | ❌ | ❌ | △ 사이드바 |
| **브라우저 북마크 검색** | ❌ **공백** | ✅ 확장 연동 | ✅ | △ 플러그인 | ✅ 내장 | ❌ | | **브라우저 북마크 검색** | ❌ **공백** | ✅ 확장 연동 | ✅ | △ 플러그인 | ✅ 내장 | ❌ |
| **브라우저 탭 AI 전달** | ❌ **공백** | ✅ {browser-tab} | ❌ | ❌ | ❌ | ❌ | | **브라우저 탭 AI 전달** | ❌ **공백** | ✅ {browser-tab} | ❌ | ❌ | ❌ | ❌ |
| **시스템 볼륨 제어** | **공백** | ✅ | ✅ | ❌ | ❌ | ❌ | | **시스템 볼륨 제어** | ✅ (L27) | ✅ | ✅ | ❌ | ❌ | ❌ |
| **화면 밝기 제어** | **공백** | ✅ | ❌ | ❌ | ❌ | ❌ | | **화면 밝기 제어** | ✅ (L27) | ✅ | ❌ | ❌ | ❌ | ❌ |
| **QR 코드 생성** | **공백** | ✅ | ✅ | △ 플러그인 | ❌ | ❌ | | **QR 코드 생성** | ✅ (L27) | ✅ | ✅ | △ 플러그인 | ❌ | ❌ |
| **회의 링크 빠른 열기** | △ QuickLink 수동 | ✅ Calendar 연동 | ✅ | ❌ | ❌ | ❌ | | **회의 링크 빠른 열기** | ✅ (L27) | ✅ Calendar 연동 | ✅ | ❌ | ❌ | ❌ |
| **winget 앱 설치** | ❌ **공백** | ✅ Win 베타 | ❌ | △ 플러그인 | ❌ | ✅ 내장 | | **winget 앱 설치** | ❌ **공백** | ✅ Win 베타 | ❌ | △ 플러그인 | ❌ | ✅ 내장 |
| **노코드 워크플로우** | ❌ **공백** | △ AI Ext 베타 | ✅ 완전 지원 | ❌ | ❌ | ❌ | | **노코드 워크플로우** | ❌ **공백** | △ AI Ext 베타 | ✅ 완전 지원 | ❌ | ❌ | ❌ |
| **스크립트 명령 실행** | ✅ ^ 프리픽스 | ✅ Script Commands | ✅ | ✅ | ✅ | ❌ | | **스크립트 명령 실행** | ✅ ^ 프리픽스 | ✅ 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종 일괄 구현. > **방향**: Raycast·Alfred·PowerToys가 공통 제공하나 AX에 없는 **일상 빈도 최상위 기능** 6종 일괄 구현.
> 모두 외부 설치 의존 없이 Windows 네이티브 API + 로컬 파싱으로 구현. > 모두 외부 설치 의존 없이 Windows 네이티브 API + 로컬 파싱으로 구현.
| # | 기능 | 프리픽스 | 구현 방식 | 사무 적합성 | | # | 기능 | 프리픽스 | 구현 방식 |
|---|------|---------|---------|:---:| |---|------|---------|---------|
| 📋 L27-1 | **브라우저 북마크 검색** | `bm` | Edge: `%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\Bookmarks` JSON 파싱. Chrome 동일 경로. 제목·URL·폴더 검색. Enter → 기본 브라우저로 열기 | ⭐⭐⭐ | | L27-1 | **브라우저 북마크 검색** | (퍼지 통합) | `BookmarkHandler.cs` 기구현 — Edge/Chrome Bookmarks JSON 파싱, 5분 캐시 TTL, 제목·URL 검색, Enter → 기본 브라우저로 열기. 프리픽스 없이 일반 검색에 통합 |
| 📋 L27-2 | **시스템 볼륨 제어** | `vol` | `Windows.Media.Audio` COM / `NAudio.CoreAudioApi` IAudioEndpointVolume. `vol 50` → 50% 설정, `vol up/down` ±10%, `vol mute` 토글. 현재 볼륨 실시간 표시 | ⭐⭐⭐ | | L27-2 | **시스템 볼륨 제어** | `vol` | `VolHandler.cs` — Windows Core Audio API COM 직접 호출 (IAudioEndpointVolume). `vol 50` 설정, `vol up/down` ±10%, `vol mute` 토글. 볼륨 바 시각화. NuGet 불필요 |
| 📋 L27-3 | **QR 코드 생성** | `qr` | QRCoder NuGet (순수 C# 행렬). `qr {텍스트/URL}` 128×128 BitmapImage 결과 패널 인라인 표시. Enter PNG 클립보드 복사. `save``%TEMP%\qr.png` 저장 후 탐색기 | ⭐⭐ | | L27-3 | **QR 코드 생성** | `qr` | `QrHandler.cs` QRCoder NuGet (순수 C# QR 행렬). `qr {텍스트}` → Enter: PNG 클립보드 복사. `qr save {텍스트}` → TEMP 저장 후 탐색기 열기 |
| 📋 L27-4 | **회의 링크 전용 관리** | `meet` | `%APPDATA%\AxCopilot\meet.json` 로컬 저장. `meet add {이름} {URL}` 등록. 서비스 자동 감지 (Zoom/Teams/Google Meet/Webex). Enter → URI 실행. 빈 쿼리 → 전체 목록 | ⭐⭐⭐ | | 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` | WMI `WmiMonitorBrightnessMethods.WmiSetBrightness()`. `bright 70` → 70% 설정. `bright up/down` ±10%. 노트북 내장 모니터 대상. 외장 모니터 미지원 안내 | ⭐⭐ | | L27-5 | **화면 밝기 제어** | `bright` | `BrightHandler.cs` — WMI PowerShell subprocess (Get-CimInstance WmiMonitorBrightness). 노트북 내장 디스플레이. `bright 70` 설정, `bright up/down` ±10%. 외장 모니터 미지원 안내 |
| 📋 L27-6 | **클립보드 순차 붙여넣기** | `paste` | 클립보드 히스토리 항목 N개를 번호 지정해 순서대로 전송 (`paste 3 1 5` → 3번→1번→5번 순). SendKeys 방식. Raycast "Paste Sequentially" 대응 기능 | ⭐⭐ | | 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 1.6.0` (순수 C#, 외부 의존 없음, ~150KB DLL)
**NuGet 추가**: `QRCoder` (순수 C#, 외부 의존 없음, ~150KB DLL) **빌드**: 경고 0, 오류 0
**예상 핸들러 파일**: BookmarkHandler.cs, MeetHandler.cs, QrHandler.cs, VolHandler.cs, BrightHandler.cs — PasteHandler.cs 또는 ClipboardHistoryHandler 확장
--- ---

View File

@@ -372,6 +372,19 @@ public partial class App : System.Windows.Application
// L25-4: 오늘 업무 통합 뷰 (prefix=today) // L25-4: 오늘 업무 통합 뷰 (prefix=today)
commandResolver.RegisterHandler(new TodayHandler()); 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); var pluginHost = new PluginHost(settings, commandResolver);
pluginHost.LoadAll(); pluginHost.LoadAll();

View File

@@ -66,6 +66,7 @@
<PackageReference Include="Markdig" Version="0.37.0" /> <PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2903.40" /> <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" /> <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" /> <PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
<PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" /> <PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" />

View File

@@ -0,0 +1,153 @@
using System.Diagnostics;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
/// <summary>
/// L27-5: 화면 밝기 제어 핸들러. "bright" 프리픽스로 사용합니다.
///
/// 예: bright → 현재 밝기 표시
/// bright 70 → 밝기 70% 설정
/// bright up / down → ±10% 조절
/// Enter → 해당 밝기로 설정.
/// WMI (WmiMonitorBrightness) 사용 — 노트북 내장 디스플레이 대상.
/// </summary>
public class BrightHandler : IActionHandler
{
public string? Prefix => "bright";
public PluginMetadata Metadata => new(
"밝기 제어",
"화면 밝기 조절 — 설정·증감 (노트북)",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim().ToLowerInvariant();
var items = new List<LauncherItem>();
// 현재 밝기 읽기
int current = GetCurrentBrightness();
if (current < 0)
{
items.Add(new LauncherItem(
"밝기 센서를 찾을 수 없습니다",
"노트북 내장 디스플레이에서만 동작합니다 (외장 모니터 미지원)",
null, null, Symbol: "\uE7BA"));
return Task.FromResult<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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;
}
}

View File

@@ -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;
/// <summary>
/// L27-4: 회의 링크 전용 관리 핸들러. "meet" 프리픽스로 사용합니다.
///
/// 예: meet → 전체 회의 목록
/// meet 스탠드업 → 이름 검색
/// meet add 스탠드업 https://... → 회의 추가
/// meet del 스탠드업 → 회의 삭제
/// Enter → 기본 브라우저로 회의 링크 열기.
/// 저장: %APPDATA%\AxCopilot\meet.json
/// </summary>
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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
}
// ── 빈 쿼리 → 전체 목록 ──────────────────────────────────────────────
if (string.IsNullOrWhiteSpace(q))
{
if (meets.Count == 0)
{
items.Add(new LauncherItem("등록된 회의가 없습니다",
"meet add {이름} {URL} 로 추가하세요",
null, null, Symbol: "\uE8D6"));
return Task.FromResult<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<MeetEntry> Load()
{
try
{
if (!File.Exists(DataPath)) return [];
var json = File.ReadAllText(DataPath);
return JsonSerializer.Deserialize<List<MeetEntry>>(json) ?? [];
}
catch { return []; }
}
private static void Save(List<MeetEntry> list)
{
try
{
var dir = Path.GetDirectoryName(DataPath)!;
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
File.WriteAllText(DataPath, JsonSerializer.Serialize(list, JsonOpt));
}
catch { }
}
}

View File

@@ -0,0 +1,217 @@
using System.Runtime.InteropServices;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L27-6: 클립보드 순차 붙여넣기 핸들러. "paste" 프리픽스로 사용합니다.
///
/// 예: paste → 번호 매긴 클립보드 히스토리 목록
/// paste 3 1 5 → 3번→1번→5번 항목을 순서대로 붙여넣기
/// paste all → 전체 히스토리를 순서대로 붙여넣기
/// Enter → 이전 창에서 순서대로 Ctrl+V 실행.
/// Raycast "Paste Sequentially" 대응.
/// </summary>
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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
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<IEnumerable<LauncherItem>>(items);
}
// ── 번호 시퀀스 파싱 ──────────────────────────────────────────────────
if (!string.IsNullOrWhiteSpace(q) && q != "all")
{
var nums = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var indices = new List<int>();
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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<string> texts))
{
// 순차 붙여넣기
await PasteTexts(texts, ct);
}
}
private async Task PasteTexts(List<string> 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<INPUT>());
}
// ─── 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);
}

View File

@@ -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;
/// <summary>
/// L27-3: QR 코드 생성 핸들러. "qr" 프리픽스로 사용합니다.
///
/// 예: qr https://google.com → QR 코드 생성 (Enter: 클립보드 복사)
/// qr 안녕하세요 → 한국어 텍스트 QR 생성
/// qr save https://... → QR PNG 파일 저장
/// Enter → QR 이미지를 클립보드에 복사 (붙여넣기로 사용).
/// </summary>
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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
if (string.IsNullOrWhiteSpace(q))
{
items.Add(new LauncherItem(
"QR 코드 생성",
"qr {텍스트 또는 URL} → Enter: QR PNG 클립보드 복사",
null, null, Symbol: "\uE8C8"));
return Task.FromResult<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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);
}
}

View File

@@ -0,0 +1,211 @@
using System.Runtime.InteropServices;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L27-2: 시스템 볼륨 제어 핸들러. "vol" 프리픽스로 사용합니다.
///
/// 예: vol → 현재 볼륨 표시
/// vol 50 → 볼륨 50% 설정
/// vol up / down → ±10% 조절
/// vol mute → 음소거 토글
/// Enter → 해당 명령 실행.
/// Windows Core Audio API (IAudioEndpointVolume) COM 인터페이스 사용.
/// </summary>
public class VolHandler : IActionHandler
{
public string? Prefix => "vol";
public PluginMetadata Metadata => new(
"볼륨 제어",
"시스템 볼륨 조절 — 설정·증감·음소거",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim().ToLowerInvariant();
var items = new List<LauncherItem>();
// 현재 볼륨 읽기
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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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);
}
}