using System.Runtime.InteropServices; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// 미디어 컨트롤 핸들러. "media" 프리픽스로 사용합니다. /// 예: media play → 재생/일시정지 /// media next → 다음 트랙 /// media prev → 이전 트랙 /// media vol+ → 볼륨 올리기 /// media vol- → 볼륨 낮추기 /// media mute → 음소거 토글 /// public class MediaHandler : IActionHandler { public string? Prefix => "media"; public PluginMetadata Metadata => new( "MediaControl", "미디어 컨트롤 — media 뒤에 명령어 입력", "1.0", "AX"); // Windows 미디어/볼륨 가상 키 코드 private const byte VK_MEDIA_PLAY_PAUSE = 0xB3; private const byte VK_MEDIA_NEXT_TRACK = 0xB0; private const byte VK_MEDIA_PREV_TRACK = 0xB1; private const byte VK_VOLUME_UP = 0xAF; private const byte VK_VOLUME_DOWN = 0xAE; private const byte VK_VOLUME_MUTE = 0xAD; // KEYEVENTF flags private const uint KEYEVENTF_EXTENDEDKEY = 0x0001; private const uint KEYEVENTF_KEYUP = 0x0002; [DllImport("user32.dll", SetLastError = false)] private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, nuint dwExtraInfo); // 명령어 → (제목, 설명, VK코드, 심볼) private static readonly List<(string[] Keys, string Title, string Subtitle, byte Vk, string Symbol)> _commands = [ (["play", "pause", "pp"], "재생 / 일시정지", "현재 미디어 재생 또는 일시정지", VK_MEDIA_PLAY_PAUSE, Symbols.MediaPlay), (["next", ">>"], "다음 트랙", "다음 곡으로 이동", VK_MEDIA_NEXT_TRACK, Symbols.MediaNext), (["prev", "previous", "<<"], "이전 트랙", "이전 곡으로 이동", VK_MEDIA_PREV_TRACK, Symbols.MediaPrev), (["vol+", "volup", "up"], "볼륨 올리기", "시스템 볼륨 증가", VK_VOLUME_UP, Symbols.VolumeUp), (["vol-", "voldown", "down"], "볼륨 낮추기", "시스템 볼륨 감소", VK_VOLUME_DOWN, Symbols.VolumeDown), (["mute", "음소거"], "음소거 토글", "볼륨 음소거 / 해제", VK_VOLUME_MUTE, Symbols.VolumeMute), ]; public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim().ToLowerInvariant(); IEnumerable<(string[] Keys, string Title, string Subtitle, byte Vk, string Symbol)> matches; if (string.IsNullOrEmpty(q)) { // 쿼리 없으면 전체 목록 표시 matches = _commands; } else { // 입력어와 일치하는 명령 필터 matches = _commands.Where(c => c.Keys.Any(k => k.StartsWith(q, StringComparison.OrdinalIgnoreCase)) || c.Title.Contains(q, StringComparison.OrdinalIgnoreCase)); } var items = matches .Select(c => new LauncherItem(c.Title, c.Subtitle, null, new MediaKeyData(c.Vk), Symbol: c.Symbol)) .ToList(); if (items.Count == 0) { items.Add(new LauncherItem( "알 수 없는 명령어", "play · next · prev · vol+ · vol- · mute 중 하나를 입력하세요", null, null, Symbol: Symbols.Warning)); } return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is not MediaKeyData data) return Task.CompletedTask; try { // 키 누름 → 키 뗌 keybd_event(data.Vk, 0, KEYEVENTF_EXTENDEDKEY, 0); keybd_event(data.Vk, 0, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0); LogService.Info($"미디어 키 전송: VK=0x{data.Vk:X2}"); } catch (Exception ex) { LogService.Warn($"미디어 키 전송 실패: {ex.Message}"); } return Task.CompletedTask; } private record MediaKeyData(byte Vk); }