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; }
}