변경 목적: 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개를 확인했습니다.
260 lines
10 KiB
C#
260 lines
10 KiB
C#
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Windows;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Themes;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// L18-2: 텍스트 케이스 변환 핸들러. "text" 프리픽스로 사용합니다.
|
|
///
|
|
/// 예: text → 클립보드 텍스트 모든 케이스 변환 목록
|
|
/// text camel → camelCase
|
|
/// text pascal → PascalCase
|
|
/// text snake → snake_case
|
|
/// text kebab → kebab-case
|
|
/// text slug → url-slug (소문자 + 하이픈)
|
|
/// text upper → UPPER CASE
|
|
/// text lower → lower case
|
|
/// text title → Title Case
|
|
/// text sentence → Sentence case
|
|
/// text const → SCREAMING_SNAKE_CASE
|
|
/// text dot → dot.case
|
|
/// text reverse → 문자 순서 뒤집기
|
|
/// text trim → 앞뒤 공백·줄바꿈 제거
|
|
/// Enter → 결과 복사.
|
|
/// </summary>
|
|
public partial class TextCaseHandler : IActionHandler
|
|
{
|
|
public string? Prefix => "text";
|
|
|
|
public PluginMetadata Metadata => new(
|
|
"Text",
|
|
"텍스트 케이스 변환 — camelCase · snake_case · PascalCase · slug 등",
|
|
"1.0",
|
|
"AX");
|
|
|
|
private record CaseItem(string Name, string Key, Func<string, string> Convert);
|
|
|
|
private static readonly CaseItem[] Cases =
|
|
[
|
|
new("camelCase", "camel", ToCamel),
|
|
new("PascalCase", "pascal", ToPascal),
|
|
new("snake_case", "snake", ToSnake),
|
|
new("SCREAMING_SNAKE_CASE", "const", ToConst),
|
|
new("kebab-case", "kebab", ToKebab),
|
|
new("URL slug", "slug", ToSlug),
|
|
new("dot.case", "dot", ToDot),
|
|
new("UPPER CASE", "upper", s => s.ToUpperInvariant()),
|
|
new("lower case", "lower", s => s.ToLowerInvariant()),
|
|
new("Title Case", "title", ToTitle),
|
|
new("Sentence case", "sentence", ToSentence),
|
|
new("뒤집기 (reverse)", "reverse", s => new string(s.Reverse().ToArray())),
|
|
new("공백 정리 (trim)", "trim", s => s.Trim()),
|
|
];
|
|
|
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
var q = query.Trim();
|
|
var items = new List<LauncherItem>();
|
|
|
|
string? clipboard = null;
|
|
try
|
|
{
|
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
if (Clipboard.ContainsText())
|
|
clipboard = Clipboard.GetText().Trim();
|
|
});
|
|
}
|
|
catch { }
|
|
|
|
if (string.IsNullOrWhiteSpace(q))
|
|
{
|
|
items.Add(new LauncherItem("텍스트 케이스 변환기",
|
|
"클립보드 텍스트를 다양한 케이스로 변환 · text camel / snake / pascal / kebab…",
|
|
null, null, Symbol: "\uE8AB"));
|
|
|
|
if (string.IsNullOrWhiteSpace(clipboard))
|
|
{
|
|
items.Add(new LauncherItem("클립보드가 비어 있습니다",
|
|
"텍스트를 복사한 뒤 사용하세요", null, null, Symbol: "\uE946"));
|
|
// 케이스 목록만 안내
|
|
foreach (var c in Cases)
|
|
items.Add(new LauncherItem($"text {c.Key}", c.Name, null, null, Symbol: "\uE8AB"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// 클립보드 텍스트 → 모든 케이스 변환 목록
|
|
var preview = clipboard.Length > 30 ? clipboard[..30] + "…" : clipboard;
|
|
items.Add(new LauncherItem($"입력: \"{preview}\"", $"{clipboard.Length}자 · 아래에서 선택",
|
|
null, null, Symbol: "\uE8AB"));
|
|
|
|
foreach (var c in Cases)
|
|
{
|
|
var result = TrySafeConvert(c.Convert, clipboard);
|
|
items.Add(new LauncherItem(result, c.Name,
|
|
null, ("copy", result), Symbol: "\uE8AB"));
|
|
}
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// 서브커맨드로 특정 케이스 변환
|
|
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
|
var sub = parts[0].ToLowerInvariant();
|
|
// 인라인 텍스트 입력 지원: text camel hello world → helloWorld
|
|
var inlineText = parts.Length > 1 ? parts[1] : clipboard;
|
|
|
|
if (string.IsNullOrWhiteSpace(inlineText))
|
|
{
|
|
items.Add(new LauncherItem("텍스트가 없습니다",
|
|
"클립보드에 텍스트를 복사하거나 text camel <직접입력> 형식으로 사용하세요",
|
|
null, null, Symbol: "\uE946"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
var caseItem = Cases.FirstOrDefault(c =>
|
|
c.Key.Equals(sub, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (caseItem != null)
|
|
{
|
|
var result = TrySafeConvert(caseItem.Convert, inlineText);
|
|
var sourceLabel = parts.Length > 1 ? $"입력: \"{inlineText}\"" : $"클립보드: \"{(inlineText.Length > 30 ? inlineText[..30] + "…" : inlineText)}\"";
|
|
items.Add(new LauncherItem(result,
|
|
$"{caseItem.Name} · Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
|
items.Add(new LauncherItem(sourceLabel, "원본", null, ("copy", inlineText), Symbol: "\uE8AB"));
|
|
|
|
// 다른 케이스도 함께 표시
|
|
items.Add(new LauncherItem("── 다른 케이스 ──", "", null, null, Symbol: "\uE8AB"));
|
|
foreach (var c in Cases.Where(c => c.Key != sub))
|
|
{
|
|
var r = TrySafeConvert(c.Convert, inlineText);
|
|
if (r != result)
|
|
items.Add(new LauncherItem(r, c.Name, null, ("copy", r), Symbol: "\uE8AB"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 알 수 없는 서브커맨드 → 모든 케이스 변환
|
|
items.Add(new LauncherItem($"알 수 없는 케이스: '{sub}'",
|
|
"camel · pascal · snake · const · kebab · slug · dot · upper · lower · title · sentence · reverse · trim",
|
|
null, null, Symbol: "\uE783"));
|
|
|
|
if (!string.IsNullOrWhiteSpace(clipboard))
|
|
{
|
|
foreach (var c in Cases)
|
|
{
|
|
var r = TrySafeConvert(c.Convert, clipboard);
|
|
items.Add(new LauncherItem(r, c.Name, null, ("copy", r), Symbol: "\uE8AB"));
|
|
}
|
|
}
|
|
}
|
|
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
|
{
|
|
if (item.Data is ("copy", string text))
|
|
{
|
|
try
|
|
{
|
|
System.Windows.Application.Current.Dispatcher.Invoke(
|
|
() => Clipboard.SetText(text));
|
|
NotificationService.Notify("Text", "클립보드에 복사했습니다.");
|
|
}
|
|
catch { }
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ── 변환 함수 ─────────────────────────────────────────────────────────────
|
|
|
|
private static string TrySafeConvert(Func<string, string> fn, string input)
|
|
{
|
|
try { return fn(input); }
|
|
catch { return input; }
|
|
}
|
|
|
|
/// <summary>입력을 단어 토큰 배열로 분리 (공백, 언더스코어, 하이픈, 대문자 경계)</summary>
|
|
private static string[] Tokenize(string s)
|
|
{
|
|
// camelCase/PascalCase 분리
|
|
var withSpaces = CamelBoundaryRegex().Replace(s, "$1 $2");
|
|
// 구분자 → 공백
|
|
var normalized = SeparatorRegex().Replace(withSpaces, " ");
|
|
return normalized.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
}
|
|
|
|
private static string ToCamel(string s)
|
|
{
|
|
var words = Tokenize(s);
|
|
if (words.Length == 0) return s;
|
|
var sb = new StringBuilder(words[0].ToLowerInvariant());
|
|
for (var i = 1; i < words.Length; i++)
|
|
sb.Append(char.ToUpperInvariant(words[i][0]) + words[i][1..].ToLowerInvariant());
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string ToPascal(string s)
|
|
{
|
|
var words = Tokenize(s);
|
|
var sb = new StringBuilder();
|
|
foreach (var w in words)
|
|
sb.Append(char.ToUpperInvariant(w[0]) + w[1..].ToLowerInvariant());
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string ToSnake(string s) =>
|
|
string.Join("_", Tokenize(s).Select(w => w.ToLowerInvariant()));
|
|
|
|
private static string ToConst(string s) =>
|
|
string.Join("_", Tokenize(s).Select(w => w.ToUpperInvariant()));
|
|
|
|
private static string ToKebab(string s) =>
|
|
string.Join("-", Tokenize(s).Select(w => w.ToLowerInvariant()));
|
|
|
|
private static string ToSlug(string s)
|
|
{
|
|
var normalized = s.Normalize(NormalizationForm.FormD);
|
|
var ascii = new StringBuilder();
|
|
foreach (var c in normalized)
|
|
if (c < 128) ascii.Append(c);
|
|
var slug = SeparatorRegex().Replace(ascii.ToString().ToLowerInvariant(), "-");
|
|
slug = NonSlugRegex().Replace(slug, "");
|
|
slug = MultipleDashRegex().Replace(slug, "-");
|
|
return slug.Trim('-');
|
|
}
|
|
|
|
private static string ToDot(string s) =>
|
|
string.Join(".", Tokenize(s).Select(w => w.ToLowerInvariant()));
|
|
|
|
private static string ToTitle(string s)
|
|
{
|
|
var words = s.Split(' ');
|
|
return string.Join(" ", words.Select(w =>
|
|
w.Length == 0 ? w : char.ToUpperInvariant(w[0]) + w[1..].ToLowerInvariant()));
|
|
}
|
|
|
|
private static string ToSentence(string s)
|
|
{
|
|
if (string.IsNullOrEmpty(s)) return s;
|
|
var lower = s.ToLowerInvariant();
|
|
return char.ToUpperInvariant(lower[0]) + lower[1..];
|
|
}
|
|
|
|
[GeneratedRegex(@"([a-z])([A-Z])")]
|
|
private static partial Regex CamelBoundaryRegex();
|
|
|
|
[GeneratedRegex(@"[\s\-_./\\]+")]
|
|
private static partial Regex SeparatorRegex();
|
|
|
|
[GeneratedRegex(@"[^a-z0-9\-]")]
|
|
private static partial Regex NonSlugRegex();
|
|
|
|
[GeneratedRegex(@"-{2,}")]
|
|
private static partial Regex MultipleDashRegex();
|
|
}
|