using System.Windows; using AxCopilot.Models; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// 텍스트 스니펫 핸들러. ";" 프리픽스로 사용합니다. /// 예: ;addr → 미리 저장된 주소 텍스트를 클립보드에 복사 후 붙여넣기 /// ;sig → 서명 텍스트 붙여넣기 /// public class SnippetHandler : IActionHandler { private readonly SettingsService _settings; public string? Prefix => ";"; public PluginMetadata Metadata => new( "Snippets", "텍스트 스니펫 — ; 뒤에 키워드 입력", "1.0", "AX"); public SnippetHandler(SettingsService settings) { _settings = settings; } public Task> GetItemsAsync(string query, CancellationToken ct) { var snippets = _settings.Settings.Snippets; if (!snippets.Any()) { return Task.FromResult>( [ new LauncherItem( "등록된 스니펫이 없습니다", "설정 → 스니펫 탭에서 추가하세요", null, null, Symbol: Symbols.Snippet) ]); } var q = query.Trim().ToLowerInvariant(); var results = snippets .Where(s => string.IsNullOrEmpty(q) || s.Key.ToLowerInvariant().Contains(q) || s.Name.ToLowerInvariant().Contains(q)) .Select(s => new LauncherItem( s.Name.Length > 0 ? s.Name : s.Key, TruncatePreview(s.Content), null, s, Symbol: Symbols.Snippet)) .ToList(); if (results.Count == 0) { results.Add(new LauncherItem( $"'{query}'에 해당하는 스니펫 없음", "설정에서 새 스니펫을 추가하세요", null, null, Symbol: Symbols.Snippet)); } return Task.FromResult>(results); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is not SnippetEntry snippet) return Task.CompletedTask; try { // 변수 치환: {date}, {time}, {datetime} var expanded = ExpandVariables(snippet.Content); Clipboard.SetText(expanded); // 100ms 후 Ctrl+V 시뮬레이션 Task.Delay(100, ct).ContinueWith(_ => { System.Windows.Application.Current.Dispatcher.Invoke(() => { var inputs = new[] { new INPUT { type = 1, u = new InputUnion { ki = new KEYBDINPUT { wVk = 0x11 } } }, // Ctrl down new INPUT { type = 1, u = new InputUnion { ki = new KEYBDINPUT { wVk = 0x56 } } }, // V down new INPUT { type = 1, u = new InputUnion { ki = new KEYBDINPUT { wVk = 0x56, dwFlags = 0x0002 } } }, // V up new INPUT { type = 1, u = new InputUnion { ki = new KEYBDINPUT { wVk = 0x11, dwFlags = 0x0002 } } }, // Ctrl up }; SendInput((uint)inputs.Length, inputs, System.Runtime.InteropServices.Marshal.SizeOf()); }); }, ct, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default); } catch (Exception ex) { LogService.Warn($"스니펫 실행 실패: {ex.Message}"); } return Task.CompletedTask; } // ─── 변수 치환 ───────────────────────────────────────────────────────── private static string ExpandVariables(string content) { var now = DateTime.Now; return content .Replace("{date}", now.ToString("yyyy-MM-dd")) .Replace("{time}", now.ToString("HH:mm:ss")) .Replace("{datetime}", now.ToString("yyyy-MM-dd HH:mm:ss")) .Replace("{year}", now.Year.ToString()) .Replace("{month}", now.Month.ToString("D2")) .Replace("{day}", now.Day.ToString("D2")); } private static string TruncatePreview(string content) { var oneLine = content.Replace("\r\n", " ").Replace("\n", " ").Replace("\r", " "); return oneLine.Length > 60 ? oneLine[..57] + "…" : oneLine; } // ─── P/Invoke (SendInput) ────────────────────────────────────────────── [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] private struct INPUT { public uint type; public InputUnion u; } [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit)] private struct InputUnion { [System.Runtime.InteropServices.FieldOffset(0)] public KEYBDINPUT ki; } [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] private struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; } }