using System.IO; using System.Runtime.InteropServices; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// Everything SDK를 이용한 초고속 파일 검색 핸들러. /// "es" 프리픽스로 사용합니다. /// /// 예: es report → "report" 파일명 검색 /// es *.xlsx → 엑셀 파일 검색 /// es 회의록 → 한글 파일명 검색 /// /// Everything이 설치되어 있지 않으면 자동으로 비활성화됩니다. /// public class EverythingHandler : IActionHandler { public string? Prefix => "es"; public PluginMetadata Metadata => new( "EverythingSearch", "Everything 초고속 파일 검색 — es [키워드]", "1.0", "AX"); // ─── Everything SDK P/Invoke ───────────────────────────────────────────── private const int EVERYTHING_OK = 0; private const int EVERYTHING_REQUEST_FILE_NAME = 0x00000001; private const int EVERYTHING_REQUEST_PATH = 0x00000002; private const int EVERYTHING_REQUEST_SIZE = 0x00000010; [DllImport("Everything64.dll", CharSet = CharSet.Unicode)] private static extern uint Everything_SetSearchW(string lpSearchString); [DllImport("Everything64.dll")] private static extern void Everything_SetMax(uint dwMax); [DllImport("Everything64.dll")] private static extern void Everything_SetRequestFlags(uint dwRequestFlags); [DllImport("Everything64.dll")] private static extern bool Everything_QueryW(bool bWait); [DllImport("Everything64.dll")] private static extern uint Everything_GetNumResults(); [DllImport("Everything64.dll")] private static extern uint Everything_GetLastError(); [DllImport("Everything64.dll", CharSet = CharSet.Unicode)] private static extern IntPtr Everything_GetResultFullPathNameW(uint nIndex, IntPtr lpString, uint nMaxCount); [DllImport("Everything64.dll", CharSet = CharSet.Unicode)] private static extern void Everything_GetResultSize(uint nIndex, out long lpFileSize); [DllImport("Everything64.dll")] private static extern uint Everything_GetMajorVersion(); // ─── 상태 ──────────────────────────────────────────────────────────────── private bool? _isAvailable; private bool IsAvailable { get { if (_isAvailable.HasValue) return _isAvailable.Value; try { Everything_GetMajorVersion(); _isAvailable = true; } catch { _isAvailable = false; } return _isAvailable.Value; } } // ─── IActionHandler ────────────────────────────────────────────────────── public Task> GetItemsAsync(string query, CancellationToken ct) { if (!IsAvailable) { return Task.FromResult>(new[] { new LauncherItem( "Everything이 설치되어 있지 않습니다", "voidtools.com에서 Everything을 설치하면 초고속 파일 검색을 사용할 수 있습니다", null, null, Symbol: Symbols.Warning) }); } var q = query.Trim(); if (string.IsNullOrWhiteSpace(q)) { return Task.FromResult>(new[] { new LauncherItem( "Everything 파일 검색", "검색어를 입력하세요 — 파일명, 확장자(*.xlsx), 경로 일부 등", null, null, Symbol: Symbols.Search) }); } try { Everything_SetSearchW(q); Everything_SetMax(30); Everything_SetRequestFlags(EVERYTHING_REQUEST_FILE_NAME | EVERYTHING_REQUEST_PATH | EVERYTHING_REQUEST_SIZE); if (!Everything_QueryW(true)) { return Task.FromResult>(new[] { new LauncherItem( "Everything 검색 실패", $"오류 코드: {Everything_GetLastError()} — Everything 서비스가 실행 중인지 확인하세요", null, null, Symbol: Symbols.Warning) }); } var count = Everything_GetNumResults(); if (count == 0) { return Task.FromResult>(new[] { new LauncherItem( $"검색 결과 없음: {q}", "다른 키워드나 와일드카드(*.xlsx)를 시도해 보세요", null, null, Symbol: Symbols.Info) }); } var items = new List(); var buffer = Marshal.AllocHGlobal(520 * 2); // MAX_PATH * sizeof(wchar_t) try { for (uint i = 0; i < count && i < 30; i++) { Everything_GetResultFullPathNameW(i, buffer, 520); var fullPath = Marshal.PtrToStringUni(buffer) ?? ""; Everything_GetResultSize(i, out var fileSize); var fileName = Path.GetFileName(fullPath); var dirPath = Path.GetDirectoryName(fullPath) ?? ""; var sizeStr = fileSize > 0 ? FormatSize(fileSize) : ""; var subtitle = string.IsNullOrEmpty(sizeStr) ? dirPath : $"{sizeStr} · {dirPath}"; var symbol = Directory.Exists(fullPath) ? Symbols.Folder : GetFileSymbol(fullPath); items.Add(new LauncherItem(fileName, subtitle, null, fullPath, Symbol: symbol)); } } finally { Marshal.FreeHGlobal(buffer); } return Task.FromResult>(items); } catch (Exception ex) { LogService.Warn($"Everything 검색 오류: {ex.Message}"); return Task.FromResult>(new[] { new LauncherItem("Everything 검색 오류", ex.Message, null, null, Symbol: Symbols.Warning) }); } } public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is not string path || string.IsNullOrEmpty(path)) return; try { if (Directory.Exists(path)) { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true, }); } else if (File.Exists(path)) { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true, }); } } catch (Exception ex) { LogService.Warn($"Everything 결과 열기 실패: {ex.Message}"); } await Task.CompletedTask; } // ─── 헬퍼 ──────────────────────────────────────────────────────────────── private static string FormatSize(long bytes) { if (bytes < 1024) return $"{bytes} B"; if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB"; return $"{bytes / (1024.0 * 1024 * 1024):F2} GB"; } private static string GetFileSymbol(string path) { var ext = Path.GetExtension(path).ToLowerInvariant(); return ext switch { ".xlsx" or ".xls" or ".csv" => Symbols.Excel, ".docx" or ".doc" => Symbols.Word, ".pptx" or ".ppt" => Symbols.Slides, ".pdf" => Symbols.Pdf, ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".svg" => Symbols.Image, ".mp4" or ".avi" or ".mkv" or ".mov" => Symbols.Video, ".mp3" or ".wav" or ".flac" => Symbols.Music, ".zip" or ".rar" or ".7z" or ".tar" or ".gz" => Symbols.Archive, ".exe" or ".msi" => Symbols.App, ".cs" or ".py" or ".js" or ".ts" or ".java" or ".cpp" or ".c" or ".go" => Symbols.Code, ".json" or ".xml" or ".yaml" or ".yml" => Symbols.Config, ".txt" or ".md" or ".log" => Symbols.TextFile, ".html" or ".htm" or ".css" => Symbols.Web, _ => Symbols.File, }; } }