using System.Diagnostics; using System.Runtime.InteropServices; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L6-3: 컨텍스트 감지 자동완성 핸들러. "ctx" 프리픽스로 사용합니다. /// /// 현재 포커스된 앱을 감지하여 상황별 런처 명령을 제안합니다. /// 예: ctx → 현재 Chrome 사용 중이면 북마크/웹 검색 명령 추천 /// 현재 VS Code 사용 중이면 파일/스니펫/git 명령 추천 /// public class ContextHandler : IActionHandler { public string? Prefix => "ctx"; public PluginMetadata Metadata => new( "Context", "컨텍스트 명령 제안 — ctx", "1.0", "AX"); // ─── P/Invoke ───────────────────────────────────────────────────────── [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint dwProcessId); // ─── 컨텍스트 규칙 ───────────────────────────────────────────────────── // 프로세스 이름 → (앱 표시명, 추천 명령 목록) private static readonly Dictionary ContextMap = new(new StringArrayEqualityComparer()) { { new[] { "chrome", "msedge", "firefox", "brave", "opera" }, ("웹 브라우저", new[] { new ContextSuggestion("북마크 검색", "북마크를 검색합니다", "bm", Symbols.Favorite), new ContextSuggestion("웹 검색", "기본 검색엔진으로 검색합니다", "?", "\uE721"), new ContextSuggestion("URL 열기", "URL을 런처에서 직접 실행합니다", "url", "\uE71B"), new ContextSuggestion("클립보드 내용 검색", "클립보드 이력을 검색합니다", "#", "\uE8C8"), }) }, { new[] { "code", "devenv", "rider", "idea64", "pycharm64", "webstorm64", "clion64" }, ("코드 편집기", new[] { new ContextSuggestion("파일 검색", "인덱싱된 파일을 빠르게 엽니다", "", "\uE8A5"), new ContextSuggestion("스니펫 입력", "텍스트 스니펫을 확장합니다", ";", "\uE8D2"), new ContextSuggestion("클립보드 이력", "복사한 내용을 검색합니다", "#", "\uE8C8"), new ContextSuggestion("파일 미리보기", "F3으로 파일 내용을 미리봅니다", "", "\uE7C3"), new ContextSuggestion("QuickLook 편집","파일 인라인 편집 (Ctrl+E)", "", "\uE70F"), }) }, { new[] { "excel", "powerpnt", "winword", "onenote", "outlook", "hwp", "hwpx" }, ("오피스", new[] { new ContextSuggestion("클립보드 이력", "복사한 셀·텍스트를 재사용합니다", "#", "\uE8C8"), new ContextSuggestion("계산기", "수식을 빠르게 계산합니다", "=", "\uE8EF"), new ContextSuggestion("날짜 계산", "날짜·기간을 계산합니다", "=today", "\uE787"), new ContextSuggestion("파일 검색", "문서 파일을 빠르게 찾습니다", "", "\uE8A5"), new ContextSuggestion("스니펫", "자주 쓰는 문구를 입력합니다", ";", "\uE8D2"), }) }, { new[] { "explorer" }, ("파일 탐색기", new[] { new ContextSuggestion("파일 태그 검색", "태그로 파일을 찾습니다", "tag", "\uE932"), new ContextSuggestion("배치 이름변경", "여러 파일을 한번에 이름변경합니다", "batchren", "\uE8AC"), new ContextSuggestion("파일 미리보기", "선택 파일을 미리봅니다 (F3)", "", "\uE7C3"), new ContextSuggestion("폴더 즐겨찾기", "즐겨찾기 폴더를 엽니다", "fav", Symbols.Favorite), }) }, { new[] { "slack", "teams", "zoom", "msteams" }, ("커뮤니케이션", new[] { new ContextSuggestion("클립보드 이력", "공유할 내용을 클립보드에서 선택", "#", "\uE8C8"), new ContextSuggestion("스크린 캡처", "화면을 캡처해 공유합니다", "cap", "\uE722"), new ContextSuggestion("스니펫", "자주 쓰는 답변 텍스트를 입력합니다",";", "\uE8D2"), new ContextSuggestion("번역", "텍스트를 번역합니다", "! 번역", "\uF2B7"), }) }, }; // ─── 기본 제안 (앱 미인식) ──────────────────────────────────────────── private static readonly ContextSuggestion[] DefaultSuggestions = { new("파일/앱 검색", "이름으로 파일·앱을 빠르게 검색", "", "\uE721"), new("클립보드 이력", "최근 복사한 내용 검색", "#", "\uE8C8"), new("계산기", "수식 계산", "=", "\uE8EF"), new("스니펫", "텍스트 스니펫 확장", ";", "\uE8D2"), new("스케줄러", "자동화 스케줄 목록", "sched","\uE916"), }; // ─── 항목 목록 ────────────────────────────────────────────────────────── public Task> GetItemsAsync(string query, CancellationToken ct) { var (procName, appDisplayName, suggestions) = GetContextInfo(); var items = new List(); var headerSub = string.IsNullOrEmpty(procName) ? "현재 포그라운드 앱을 인식할 수 없습니다" : $"현재 앱: {appDisplayName} ({procName}) · 상황별 명령 제안"; items.Add(new LauncherItem( "컨텍스트 제안", headerSub, null, null, Symbol: "\uE945")); var q = query.Trim().ToLowerInvariant(); foreach (var s in suggestions) { if (!string.IsNullOrEmpty(q) && !s.Title.Contains(q, StringComparison.OrdinalIgnoreCase) && !s.Subtitle.Contains(q, StringComparison.OrdinalIgnoreCase)) continue; var subtitle = string.IsNullOrEmpty(s.Prefix) ? s.Subtitle : $"{s.Subtitle} · 프리픽스: [{s.Prefix}]"; items.Add(new LauncherItem( s.Title, subtitle, null, s.Prefix, Symbol: s.Symbol)); } return Task.FromResult>(items); } // ─── 실행 ───────────────────────────────────────────────────────────── public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { // 제안 항목 실행 → 런처 입력창에 프리픽스 삽입 // LauncherWindow가 SetInputText를 지원하므로 Application 수준에서 접근 if (item.Data is string prefix && !string.IsNullOrEmpty(prefix)) { var launcher = System.Windows.Application.Current?.Windows .OfType() .FirstOrDefault(); launcher?.SetInputText(prefix + " "); } return Task.CompletedTask; } // ─── 내부 유틸 ──────────────────────────────────────────────────────── private static (string ProcName, string AppName, ContextSuggestion[] Suggestions) GetContextInfo() { try { var hwnd = GetForegroundWindow(); if (hwnd == IntPtr.Zero) return ("", "", DefaultSuggestions); GetWindowThreadProcessId(hwnd, out var pid); if (pid == 0) return ("", "", DefaultSuggestions); var proc = Process.GetProcessById((int)pid); var pName = proc.ProcessName.ToLowerInvariant(); foreach (var kv in ContextMap) { if (kv.Key.Any(k => pName.Contains(k))) return (pName, kv.Value.AppName, kv.Value.Suggestions); } return (pName, pName, DefaultSuggestions); } catch { return ("", "", DefaultSuggestions); } } // ─── 제안 레코드 ───────────────────────────────────────────────────── private record ContextSuggestion(string Title, string Subtitle, string Prefix, string Symbol); // ─── 키 비교기 ─────────────────────────────────────────────────────── private class StringArrayEqualityComparer : IEqualityComparer { public bool Equals(string[]? x, string[]? y) => x != null && y != null && x.SequenceEqual(y); public int GetHashCode(string[] obj) => obj.Aggregate(17, (h, s) => h * 31 + s.GetHashCode()); } }