using System.Runtime.InteropServices; using System.Text; using System.Windows; using AxCopilot.Services; namespace AxCopilot.Core; /// /// 모든 앱에서 ';키워드 + Space/Enter' 패턴을 감지해 스니펫을 자동 확장합니다. /// InputListener.KeyFilter에 을 등록하여 사용합니다. /// public class SnippetExpander { private readonly SettingsService _settings; private readonly StringBuilder _buffer = new(); private bool _tracking; // VK 상수 private const ushort VK_BACK = 0x08; private const int VK_ESCAPE = 0x1B; private const int VK_SPACE = 0x20; private const int VK_RETURN = 0x0D; private const int VK_OEM_1 = 0xBA; // ; (US QWERTY) private const int VK_SHIFT = 0x10; private const int VK_CONTROL = 0x11; private const int VK_MENU = 0x12; private const ushort VK_CTRL_US = 0x11; // 방향키 / 기능키 — 이 키가 오면 버퍼 초기화 private static readonly HashSet ClearKeys = new() { 0x21, 0x22, 0x23, 0x24, // PgUp, PgDn, End, Home 0x25, 0x26, 0x27, 0x28, // ←↑→↓ 0x2E, // Delete }; public SnippetExpander(SettingsService settings) { _settings = settings; } /// /// InputListener.KeyFilter에 등록할 메서드. /// true → 해당 키 이벤트 소비(차단), false → 통과. /// 훅 스레드에서 호출되므로 신속히 처리해야 합니다. /// public bool HandleKey(int vkCode) { // 자동 확장 비활성화 시 즉시 통과 if (!_settings.Settings.Launcher.SnippetAutoExpand) return false; // 추적 중이 아닐 때는 ';' 시작 키만 검사해서 훅 경로 부담을 최소화합니다. if (!_tracking) { if (vkCode != VK_OEM_1) return false; if ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0) return false; if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) return false; if ((GetAsyncKeyState(VK_MENU) & 0x8000) != 0) return false; _tracking = true; _buffer.Clear(); _buffer.Append(';'); return false; } // Ctrl/Alt 조합은 무시 (단축키와 충돌 방지) if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) { _tracking = false; _buffer.Clear(); return false; } if ((GetAsyncKeyState(VK_MENU) & 0x8000) != 0) { _tracking = false; _buffer.Clear(); return false; } // ─── 영문자/숫자 — 버퍼에 추가 ───────────────────────────────────── if ((vkCode >= 0x41 && vkCode <= 0x5A) || // A-Z (vkCode >= 0x30 && vkCode <= 0x39) || // 0-9 (vkCode >= 0x60 && vkCode <= 0x69) || // Numpad 0-9 vkCode == 0xBD) // - { bool shifted = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; char c = VkToChar(vkCode, shifted); if (c != '\0') _buffer.Append(char.ToLowerInvariant(c)); return false; } // ─── BackSpace — 버퍼에서 한 글자 제거 ────────────────────────────── if (vkCode == VK_BACK) { if (_buffer.Length > 1) _buffer.Remove(_buffer.Length - 1, 1); else { _tracking = false; _buffer.Clear(); } return false; } // ─── Space / Enter — 스니펫 확장 시도 ─────────────────────────────── if (vkCode == VK_SPACE || vkCode == VK_RETURN) { if (_buffer.Length > 1) // ';' 이후 한 글자 이상 { var keyword = _buffer.ToString(1, _buffer.Length - 1); _tracking = false; _buffer.Clear(); var snippet = _settings.Settings.Snippets.FirstOrDefault( s => s.Key.Equals(keyword, StringComparison.OrdinalIgnoreCase)); if (snippet != null) { var expanded = ExpandVariables(snippet.Content); var deleteCount = keyword.Length + 1; // ';' + keyword // 트리거 키(Space/Enter) 소비 후 UI 스레드에서 확장 처리 Application.Current.Dispatcher.BeginInvoke(() => PasteExpansion(expanded, deleteCount)); return true; // 트리거 키 소비 } } _tracking = false; _buffer.Clear(); return false; } // ─── Escape / 방향키 / 기능키 — 추적 중단 ─────────────────────────── if (vkCode == VK_ESCAPE || ClearKeys.Contains(vkCode) || vkCode >= 0x70) { _tracking = false; _buffer.Clear(); } return false; } // ─── 확장 실행 (UI 스레드) ─────────────────────────────────────────────── private static void PasteExpansion(string text, int deleteCount) { try { // 1. Backspace × deleteCount var inputs = new INPUT[deleteCount * 2]; for (int i = 0; i < deleteCount; i++) { inputs[i * 2] = MakeKeyInput(VK_BACK, false); inputs[i * 2 + 1] = MakeKeyInput(VK_BACK, true); } SendInput((uint)inputs.Length, inputs, Marshal.SizeOf()); // 2. 클립보드 → Ctrl+V Clipboard.SetText(text); var paste = new[] { MakeKeyInput(VK_CTRL_US, false), MakeKeyInput(0x56, false), // V MakeKeyInput(0x56, true), MakeKeyInput(VK_CTRL_US, true), }; SendInput((uint)paste.Length, paste, Marshal.SizeOf()); LogService.Info($"스니펫 확장 완료: {deleteCount}자 삭제 후 붙여넣기"); } catch (Exception ex) { LogService.Warn($"스니펫 확장 실패: {ex.Message}"); } } private static INPUT MakeKeyInput(ushort vk, bool keyUp) { var input = new INPUT { type = 1 }; input.u.ki.wVk = vk; input.u.ki.dwFlags = keyUp ? 0x0002u : 0u; // KEYEVENTF_KEYUP return input; } // ─── 변수 치환 ──────────────────────────────────────────────────────────── 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")); } // ─── VK → Char 매핑 (US QWERTY 기준) ──────────────────────────────────── private static char VkToChar(int vk, bool shifted) { if (vk >= 0x41 && vk <= 0x5A) return shifted ? (char)vk : char.ToLowerInvariant((char)vk); if (vk >= 0x30 && vk <= 0x39) return shifted ? ")!@#$%^&*("[vk - 0x30] : (char)vk; if (vk >= 0x60 && vk <= 0x69) return (char)('0' + (vk - 0x60)); if (vk == 0xBD) return shifted ? '_' : '-'; return '\0'; } // ─── P/Invoke ──────────────────────────────────────────────────────────── [DllImport("user32.dll")] private static extern short GetAsyncKeyState(int vKey); [DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); [StructLayout(LayoutKind.Sequential)] private struct INPUT { public int type; public InputUnion u; } [StructLayout(LayoutKind.Explicit)] private struct InputUnion { [FieldOffset(0)] public MOUSEINPUT mi; [FieldOffset(0)] public KEYBDINPUT ki; [FieldOffset(0)] public HARDWAREINPUT hi; } [StructLayout(LayoutKind.Sequential)] private struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct MOUSEINPUT { public int dx; public int dy; public uint mouseData; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct HARDWAREINPUT { public uint uMsg; public ushort wParamL; public ushort wParamH; } }