Some checks failed
Release Gate / gate (push) Has been cancelled
- 클립보드 히스토리, 클립보드 변환, 순차 붙여넣기 실행 경로에 공통 포커스 복원 helper를 추가했습니다. - 이전 활성 창 복원, 최소 대기, Ctrl+V 주입 순서를 하나로 맞춰 포커스 누락으로 내용이 원래 창에 들어가지 않던 문제를 완화했습니다. - 관련 변경 이력을 README와 DEVELOPMENT 문서에 반영했습니다. 검증: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ - 경고 0 / 오류 0
157 lines
5.5 KiB
C#
157 lines
5.5 KiB
C#
using System.Windows;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Themes;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// paste prefix 핸들러: 클립보드 히스토리를 순서대로 붙여넣습니다.
|
|
/// 예: paste / paste 3 1 5 / paste all
|
|
/// </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);
|
|
}
|
|
}
|
|
|
|
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 · 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();
|
|
|
|
var restored = await ForegroundPasteHelper.RestoreWindowAsync(prevWindow, ct, initialDelayMs: 260);
|
|
if (!restored)
|
|
return;
|
|
|
|
foreach (var text in texts)
|
|
{
|
|
if (ct.IsCancellationRequested) break;
|
|
|
|
_history.SuppressNextCapture();
|
|
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
|
|
await Task.Delay(50, ct);
|
|
|
|
await ForegroundPasteHelper.PasteClipboardAsync(prevWindow, ct, initialDelayMs: 0);
|
|
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] + "…";
|
|
}
|