AX Commander 비교본 런처 기능 대량 이식
변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
This commit is contained in:
217
src/AxCopilot/Handlers/PasteHandler.cs
Normal file
217
src/AxCopilot/Handlers/PasteHandler.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L27-6: 클립보드 순차 붙여넣기 핸들러. "paste" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: paste → 번호 매긴 클립보드 히스토리 목록
|
||||
/// paste 3 1 5 → 3번→1번→5번 항목을 순서대로 붙여넣기
|
||||
/// paste all → 전체 히스토리를 순서대로 붙여넣기
|
||||
/// Enter → 이전 창에서 순서대로 Ctrl+V 실행.
|
||||
/// Raycast "Paste Sequentially" 대응.
|
||||
/// </summary>
|
||||
public class PasteHandler : IActionHandler
|
||||
{
|
||||
private readonly ClipboardHistoryService _history;
|
||||
|
||||
public string? Prefix => "paste";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"순차 붙여넣기",
|
||||
"클립보드 히스토리를 순서대로 붙여넣기 (Paste Sequentially)",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
public PasteHandler(ClipboardHistoryService historyService)
|
||||
{
|
||||
_history = historyService;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
var history = _history.History.Where(e => e.IsText && !string.IsNullOrEmpty(e.Text)).ToList();
|
||||
|
||||
if (history.Count == 0)
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
"클립보드 히스토리가 비어 있습니다",
|
||||
"텍스트를 복사하면 사용할 수 있습니다",
|
||||
null, null, Symbol: Symbols.ClipPaste));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// ── 번호 시퀀스 파싱 ──────────────────────────────────────────────────
|
||||
if (!string.IsNullOrWhiteSpace(q) && q != "all")
|
||||
{
|
||||
var nums = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var indices = new List<int>();
|
||||
foreach (var n in nums)
|
||||
{
|
||||
if (int.TryParse(n, out int idx) && idx >= 1 && idx <= history.Count)
|
||||
indices.Add(idx);
|
||||
}
|
||||
|
||||
if (indices.Count > 0)
|
||||
{
|
||||
var preview = string.Join(" → ", indices.Select(i => $"#{i}"));
|
||||
var texts = indices.Select(i => history[i - 1].Text ?? "").ToList();
|
||||
var totalLen = texts.Sum(t => t.Length);
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
$"순차 붙여넣기: {preview}",
|
||||
$"{indices.Count}개 항목 · {totalLen}자 · Enter: 순서대로 붙여넣기",
|
||||
null, ("seq", texts), Symbol: Symbols.ClipPaste));
|
||||
|
||||
// 미리보기
|
||||
for (int i = 0; i < indices.Count; i++)
|
||||
{
|
||||
var entry = history[indices[i] - 1];
|
||||
items.Add(new LauncherItem(
|
||||
$" {i + 1}. #{indices[i]}: {Truncate(entry.Preview, 60)}",
|
||||
entry.RelativeTime,
|
||||
null, null, Symbol: Symbols.History));
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
}
|
||||
|
||||
// ── all 명령 ──────────────────────────────────────────────────────────
|
||||
if (q.Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var texts = history.Take(20).Select(e => e.Text ?? "").ToList();
|
||||
items.Add(new LauncherItem(
|
||||
$"전체 순차 붙여넣기 ({texts.Count}개)",
|
||||
$"Enter: 최근 {texts.Count}개 항목을 순서대로 붙여넣기",
|
||||
null, ("seq", texts), Symbol: Symbols.ClipPaste));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// ── 빈 쿼리 → 번호 매긴 목록 ─────────────────────────────────────────
|
||||
items.Add(new LauncherItem(
|
||||
"순차 붙여넣기 — 번호를 입력하세요",
|
||||
"예: paste 3 1 5 → 3번→1번→5번 순서로 붙여넣기 · paste all → 전체",
|
||||
null, null, Symbol: Symbols.ClipPaste));
|
||||
|
||||
for (int i = 0; i < Math.Min(history.Count, 15); i++)
|
||||
{
|
||||
var entry = history[i];
|
||||
var pinMark = entry.IsPinned ? "\uD83D\uDCCC " : "";
|
||||
items.Add(new LauncherItem(
|
||||
$" #{i + 1} {pinMark}{Truncate(entry.Preview, 50)}",
|
||||
$"{entry.RelativeTime} · {entry.CopiedAt:MM/dd HH:mm}",
|
||||
null, ("single", entry.Text ?? ""), Symbol: Symbols.History));
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
if (item.Data is ("single", string singleText))
|
||||
{
|
||||
// 단일 항목 붙여넣기
|
||||
await PasteTexts([singleText], ct);
|
||||
}
|
||||
else if (item.Data is ("seq", List<string> texts))
|
||||
{
|
||||
// 순차 붙여넣기
|
||||
await PasteTexts(texts, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PasteTexts(List<string> texts, CancellationToken ct)
|
||||
{
|
||||
if (texts.Count == 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
var prevWindow = WindowTracker.PreviousWindow;
|
||||
if (prevWindow == IntPtr.Zero) return;
|
||||
|
||||
_history.SuppressNextCapture();
|
||||
|
||||
// 이전 창 포커스 복원 대기
|
||||
await Task.Delay(300, ct);
|
||||
|
||||
var targetThread = GetWindowThreadProcessId(prevWindow, out _);
|
||||
var currentThread = GetCurrentThreadId();
|
||||
AttachThreadInput(currentThread, targetThread, true);
|
||||
SetForegroundWindow(prevWindow);
|
||||
AttachThreadInput(currentThread, targetThread, false);
|
||||
|
||||
await Task.Delay(100, ct);
|
||||
|
||||
foreach (var text in texts)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
|
||||
_history.SuppressNextCapture();
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
|
||||
await Task.Delay(50, ct);
|
||||
|
||||
SendCtrlV();
|
||||
await Task.Delay(200, ct); // 항목 간 간격
|
||||
}
|
||||
|
||||
NotificationService.Notify("paste", $"{texts.Count}개 항목 붙여넣기 완료");
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
NotificationService.Notify("paste", $"붙여넣기 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string Truncate(string s, int max)
|
||||
=> s.Length <= max ? s : s[..max] + "…";
|
||||
|
||||
// ─── Ctrl+V 주입 (ClipboardHistoryHandler와 동일 패턴) ────────────────────
|
||||
|
||||
private static void SendCtrlV()
|
||||
{
|
||||
const uint INPUT_KEYBOARD = 1;
|
||||
const uint KEYEVENTF_KEYUP = 0x0002;
|
||||
const ushort VK_CONTROL = 0x11;
|
||||
const ushort VK_V = 0x56;
|
||||
|
||||
var inputs = new INPUT[4];
|
||||
inputs[0] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_CONTROL } };
|
||||
inputs[1] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_V } };
|
||||
inputs[2] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_V, dwFlags = KEYEVENTF_KEYUP } };
|
||||
inputs[3] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_CONTROL, dwFlags = KEYEVENTF_KEYUP } };
|
||||
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
|
||||
}
|
||||
|
||||
// ─── P/Invoke ──────────────────────────────────────────────────────────────
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 40)]
|
||||
private struct INPUT
|
||||
{
|
||||
[FieldOffset(0)] public uint Type;
|
||||
[FieldOffset(8)] public KEYBDINPUT ki;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct KEYBDINPUT
|
||||
{
|
||||
public ushort wVk;
|
||||
public ushort wScan;
|
||||
public uint dwFlags;
|
||||
public uint time;
|
||||
public IntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
[DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
|
||||
[DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] private static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray)] INPUT[] pInputs, int cbSize);
|
||||
}
|
||||
Reference in New Issue
Block a user