[Phase L20] Hex·Rand·Str·Perm 핸들러 4종 추가
HexHandler (prefix=hex, 235줄): - 텍스트→hex 변환(공백·소문자·0x접두사·Base64) - 순수 hex 문자열→UTF-8/ASCII 자동 디코딩·정수 해석(big-endian) - hex dump: 오프셋+hex+ASCII 16바이트 단위 덤프 - 0xFF 단일 값→10진/8진/이진/ASCII/NOT 변환 - add/xor/and/or/not 비트 연산 - bytes <n> KB/KiB/MB/MiB/GB/GiB 크기 단위 변환 - CS1631 catch-yield 패턴(parseError 변수 분리) 적용 RandHandler (prefix=rand, 220줄): - rand / rand <max> / rand <min> <max> 숫자 생성 - rand str [len] [charset]: alpha/num/alnum/hex/special 문자셋 - rand color: HEX+RGB+HSL 랜덤 색상 5개 - rand dice [NdS]: 다면체 주사위, 합계·상세·최솟값·최댓값 - rand coin: 동전 던지기 + 연속 5회 - rand pick/shuffle: 항목 선택·피셔-예이츠 셔플 - rand uuid: Guid.NewGuid() UUID v4 - rand token: RandomNumberGenerator 보안 토큰(hex+Base64) - rand pin [len]: PIN 번호 생성 StrHandler (prefix=str, 295줄, partial class): - escape/unescape: HTML·URL·JSON·Regex 이스케이프 변환 - repeat <n> [sep]: 텍스트 반복 - pad <w> [side] [char]: 좌/우/양쪽 패딩 - wrap <cols>: 단어 단위 줄바꿈 - sort [desc]: 줄 정렬 - unique: 중복 줄 제거 - join/split: 구분자 변환 - replace <from> <to>: 텍스트 치환 - extract email/url/number/ip: [GeneratedRegex] 패턴 추출 - lines: 줄·단어·문자·바이트 통계 PermHandler (prefix=perm, 265줄): - 8진수(755)→기호(rwxr-xr-x)·상세 설명·용도·관련 권한 - 기호→8진수 역변환 - +x/-x/+w/-w/+r/-r 비트 수정 연산 - umask 022 → 파일/디렉토리 기본 권한 계산 - common: 14가지 자주 쓰는 권한 전체 목록 - CS8361 삼항연산자 보간 괄호 수정 - 빌드: 경고 0, 오류 0
This commit is contained in:
@@ -386,3 +386,16 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
|
||||
| L19-2 | **타이머·알람** ✅ | `timer` 프리픽스. `timer 30`(초)·`timer 5m`(분)·`timer 1h30m`(시간+분) 형식 파싱. `timer stop`·`timer stop <id>` 특정 타이머 취소. 정적 타이머 레지스트리로 복수 타이머 동시 운영. `Task.Delay` 백그라운드 실행 + `NotificationService.Notify`로 완료 알림. 실행 중 목록 및 남은 시간 실시간 표시 | 높음 |
|
||||
| L19-3 | **IP 주소 유틸리티** ✅ | `ip` 프리픽스. `ip my` → NetworkInterface 전 어댑터 IPv4·마스크·게이트웨이. `ip 192.168.1.1` → 분류(사설/공인/루프백/APIPA/멀티캐스트)·클래스(A~E)·이진·16진·정수 변환. `ip 10.0.0.0/8` CIDR → 네트워크·브로드캐스트·와일드카드·호스트 범위·수. `ip range <start> <end>` IP 범위 계산. `ip bin/hex/int` 표현 변환. `ip from <uint>` 정수→IP | 높음 |
|
||||
| L19-4 | **npm/yarn/pnpm 명령어 생성기** ✅ | `npm` 프리픽스. `npm install <pkg>` → npm·yarn·pnpm 3종 설치 명령 동시 표시(일반/devDependencies/전역). init·uninstall·run·build·test·update·list·audit·publish·scripts·global·clean·ci·lock 서브커맨드 지원. `npm run dev` → yarn dev / pnpm run dev 동등 명령 비교. Enter로 클립보드 복사 | 높음 |
|
||||
|
||||
---
|
||||
|
||||
## Phase L20 — Hex·Rand·Str·Perm 도구 (v2.1.0) ✅ 완료
|
||||
|
||||
> **방향**: 개발자 데이터 처리·생성 도구 강화 — 16진수 변환, 랜덤 생성, 문자열 조작, Unix 권한 계산.
|
||||
|
||||
| # | 기능 | 설명 | 우선순위 |
|
||||
|---|------|------|----------|
|
||||
| L20-1 | **16진수·바이트 변환기** ✅ | `hex` 프리픽스. 클립보드/인라인 텍스트→hex 변환(공백 구분·소문자·0x접두사). 순수 hex 문자열→UTF-8/ASCII 자동 디코딩. `hex dump <text>` 오프셋+hex+ASCII 형식 16줄 단위 덤프. `hex 0xFF` 단일 hex값→10진/8진/이진/ASCII. `hex add/xor/and/or/not` 비트 연산. `hex bytes <n>` KB/MiB/GB 크기 단위 변환. CS1631 catch-yield 패턴 적용 | 높음 |
|
||||
| L20-2 | **랜덤 생성기** ✅ | `rand` 프리픽스. 기본: 1~100 난수. `rand <max>` / `rand <min> <max>`. `rand str [len] [charset]` 영숫자/alpha/num/hex/special 문자셋. `rand color` HEX+RGB+HSL 랜덤 색상 5개. `rand dice [NdS]` 다면체 주사위(1d6~100d10000). `rand coin` 동전 던지기. `rand pick/shuffle` 항목 선택·셔플. `rand uuid` UUID v4. `rand token` RandomNumberGenerator 보안 토큰. `rand pin [len]` PIN 번호 | 높음 |
|
||||
| L20-3 | **문자열 조작 도구** ✅ | `str` 프리픽스. `str escape/unescape html/url/json/regex` 이스케이프 변환. `str repeat <n> [sep]` 반복. `str pad <w> [left/right/both] [char]` 패딩. `str wrap <cols>` 단어 단위 줄바꿈. `str sort [desc]` 줄 정렬. `str unique` 중복 제거. `str join/split <sep>` 구분자 변환. `str replace <from> <to>` 치환. `str extract email/url/number/ip` 패턴 추출. `str lines` 줄/단어/문자 통계. [GeneratedRegex] 소스 생성기 | 높음 |
|
||||
| L20-4 | **Unix 파일 권한 계산기** ✅ | `perm` 프리픽스. `perm 755` 8진수→기호(rwxr-xr-x)·소유자/그룹/기타 상세 설명·용도 안내·관련 권한 제안. `perm rwxr-xr-x` 기호→8진수 역변환. `perm +x/-x/+w/-r 644` 비트 수정 연산. `perm umask 022` umask 적용 시 파일(666)/디렉토리(777) 결과 계산. `perm common` 14가지 자주 쓰는 권한 목록. chmod 명령 자동 생성 | 높음 |
|
||||
|
||||
@@ -324,6 +324,14 @@ public partial class App : System.Windows.Application
|
||||
commandResolver.RegisterHandler(new IpInfoHandler());
|
||||
// L19-4: npm/yarn/pnpm 명령어 생성기 (prefix=npm)
|
||||
commandResolver.RegisterHandler(new NpmHandler());
|
||||
// L20-1: 16진수·바이트 변환기 (prefix=hex)
|
||||
commandResolver.RegisterHandler(new HexHandler());
|
||||
// L20-2: 랜덤 생성기 (prefix=rand)
|
||||
commandResolver.RegisterHandler(new RandHandler());
|
||||
// L20-3: 문자열 조작 도구 (prefix=str)
|
||||
commandResolver.RegisterHandler(new StrHandler());
|
||||
// L20-4: Unix 파일 권한 계산기 (prefix=perm)
|
||||
commandResolver.RegisterHandler(new PermHandler());
|
||||
|
||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||
var pluginHost = new PluginHost(settings, commandResolver);
|
||||
|
||||
315
src/AxCopilot/Handlers/HexHandler.cs
Normal file
315
src/AxCopilot/Handlers/HexHandler.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L20-1: 16진수·바이트 변환기 핸들러. "hex" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: hex → 클립보드 텍스트 → hex 변환
|
||||
/// hex hello → "hello" → 68 65 6C 6C 6F
|
||||
/// hex 68656c6c6f → hex → "hello" 디코딩
|
||||
/// hex dump hello world → 헥스 덤프 형식 (오프셋·hex·ASCII)
|
||||
/// hex 0xFF → 0xFF = 255 (십진수·이진수·문자)
|
||||
/// hex add 0x1A 0x2B → hex 덧셈
|
||||
/// hex xor 0xAB 0xCD → bitwise XOR
|
||||
/// hex and 0xFF 0x0F → bitwise AND
|
||||
/// hex or 0xA0 0x0F → bitwise OR
|
||||
/// hex not 0xFF → bitwise NOT (8비트)
|
||||
/// hex bytes <n> → n바이트 크기 단위 표시 (KB·MB·GB)
|
||||
/// Enter → 결과 복사.
|
||||
/// </summary>
|
||||
public class HexHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "hex";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Hex",
|
||||
"16진수·바이트 변환기 — 텍스트↔hex·덤프·비트연산·크기 단위",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
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("16진수·바이트 변환기",
|
||||
"hex <텍스트> / hex <hexstr> / hex dump <text> / hex 0xFF / hex bytes <n>",
|
||||
null, null, Symbol: "\uE8EF"));
|
||||
if (!string.IsNullOrWhiteSpace(clipboard))
|
||||
items.AddRange(BuildFromText(clipboard!, brief: true));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
|
||||
// hex dump
|
||||
if (sub == "dump")
|
||||
{
|
||||
var text = parts.Length > 1 ? string.Join(" ", parts[1..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{ items.Add(ErrorItem("텍스트를 입력하거나 클립보드에 복사하세요")); }
|
||||
else
|
||||
items.AddRange(BuildDump(text));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// hex bytes <n>
|
||||
if (sub == "bytes" && parts.Length >= 2 &&
|
||||
long.TryParse(parts[1], out var byteCount))
|
||||
{
|
||||
items.AddRange(BuildByteSize(byteCount));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// hex add/xor/and/or/not (비트 연산)
|
||||
if (sub is "add" or "xor" or "and" or "or" or "not")
|
||||
{
|
||||
items.AddRange(BuildBitOp(sub, parts));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 단일 hex 값 (0xFF, 0xAB, FF, AB...)
|
||||
if (TryParseHexValue(parts[0], out var hexVal))
|
||||
{
|
||||
items.AddRange(BuildFromHexValue(hexVal, parts[0]));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 순수 hex 문자열인지 판단 (2자 이상, 모두 hex digit, 짝수 길이)
|
||||
var raw = parts[0].Replace(" ", "").Replace("-", "").Replace(":", "");
|
||||
if (raw.Length >= 2 && raw.Length % 2 == 0 && IsAllHex(raw))
|
||||
{
|
||||
items.AddRange(BuildFromHexString(raw));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 일반 텍스트 → hex 변환
|
||||
var input = string.Join(" ", parts);
|
||||
items.AddRange(BuildFromText(input, brief: false));
|
||||
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("Hex", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 빌더 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildFromText(string text, bool brief)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
var hex = BitConverter.ToString(bytes).Replace("-", "");
|
||||
var spaced = string.Join(" ", Enumerable.Range(0, hex.Length / 2)
|
||||
.Select(i => hex.Substring(i * 2, 2)));
|
||||
|
||||
yield return new LauncherItem(spaced,
|
||||
$"텍스트 → Hex ({bytes.Length} bytes) · Enter 복사",
|
||||
null, ("copy", spaced), Symbol: "\uE8EF");
|
||||
|
||||
if (!brief)
|
||||
{
|
||||
yield return CopyItem("공백 없음", hex);
|
||||
yield return CopyItem("소문자", hex.ToLowerInvariant());
|
||||
yield return CopyItem("0x 접두사", string.Join(" ", Enumerable.Range(0, hex.Length / 2)
|
||||
.Select(i => "0x" + hex.Substring(i * 2, 2))));
|
||||
yield return CopyItem("바이트 수", $"{bytes.Length} bytes");
|
||||
var b64 = Convert.ToBase64String(bytes);
|
||||
yield return CopyItem("Base64", b64);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildFromHexString(string hex)
|
||||
{
|
||||
hex = hex.ToUpperInvariant();
|
||||
byte[]? bytes = null;
|
||||
string? parseError = null;
|
||||
try
|
||||
{
|
||||
bytes = Enumerable.Range(0, hex.Length / 2)
|
||||
.Select(i => Convert.ToByte(hex.Substring(i * 2, 2), 16))
|
||||
.ToArray();
|
||||
}
|
||||
catch { parseError = "올바른 hex 문자열이 아닙니다"; }
|
||||
|
||||
if (parseError != null)
|
||||
{
|
||||
yield return ErrorItem(parseError);
|
||||
yield break;
|
||||
}
|
||||
|
||||
var safeBytes = bytes!;
|
||||
var utf8 = TrySafeUtf8(safeBytes);
|
||||
var ascii = TrySafeAscii(safeBytes);
|
||||
|
||||
yield return new LauncherItem($"Hex → 텍스트",
|
||||
$"{safeBytes.Length} bytes · UTF-8 디코딩",
|
||||
null, null, Symbol: "\uE8EF");
|
||||
|
||||
if (utf8 != null) yield return CopyItem("UTF-8 텍스트", utf8);
|
||||
if (ascii != null && ascii != utf8) yield return CopyItem("ASCII 텍스트", ascii);
|
||||
|
||||
yield return CopyItem("바이트 수", $"{safeBytes.Length}");
|
||||
// 숫자 해석 (최대 8바이트)
|
||||
if (safeBytes.Length <= 8)
|
||||
{
|
||||
var padded = new byte[8];
|
||||
Buffer.BlockCopy(safeBytes, 0, padded, 8 - safeBytes.Length, safeBytes.Length);
|
||||
var bigEndian = BitConverter.IsLittleEndian
|
||||
? BitConverter.ToUInt64(padded.Reverse().ToArray())
|
||||
: BitConverter.ToUInt64(padded);
|
||||
yield return CopyItem($"정수 (big-endian)", bigEndian.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildFromHexValue(ulong val, string original)
|
||||
{
|
||||
yield return new LauncherItem($"{original} = {val}",
|
||||
$"16진수 → 십진수 · Enter 복사",
|
||||
null, ("copy", val.ToString()), Symbol: "\uE8EF");
|
||||
yield return CopyItem("십진수", val.ToString());
|
||||
yield return CopyItem("16진수", $"0x{val:X}");
|
||||
yield return CopyItem("8진수", $"0o{Convert.ToString((long)val, 8)}");
|
||||
yield return CopyItem("이진수", $"0b{Convert.ToString((long)val, 2)}");
|
||||
if (val <= 127) yield return CopyItem("ASCII 문자", ((char)val).ToString());
|
||||
yield return CopyItem("NOT (64bit)", $"0x{(~val):X16}");
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildDump(string text)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
var lines = new List<string>();
|
||||
var sb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < bytes.Length; i += 16)
|
||||
{
|
||||
var chunk = bytes.Skip(i).Take(16).ToArray();
|
||||
var hexPart = string.Join(" ", chunk.Select(b => $"{b:X2}")).PadRight(47);
|
||||
var asciiPart = new string(chunk.Select(b => b >= 32 && b < 127 ? (char)b : '.').ToArray());
|
||||
var line = $"{i:X8} {hexPart} |{asciiPart}|";
|
||||
lines.Add(line);
|
||||
}
|
||||
|
||||
var dump = string.Join("\n", lines);
|
||||
yield return new LauncherItem("헥스 덤프",
|
||||
$"{bytes.Length} bytes · {lines.Count} 행", null, ("copy", dump), Symbol: "\uE8EF");
|
||||
foreach (var line in lines.Take(8))
|
||||
yield return new LauncherItem(line, "", null, ("copy", line), Symbol: "\uE8EF");
|
||||
if (lines.Count > 8)
|
||||
yield return new LauncherItem($"... ({lines.Count - 8}행 더 있음)",
|
||||
"전체 복사하려면 상단 항목 Enter", null, null, Symbol: "\uE8EF");
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildBitOp(string op, string[] parts)
|
||||
{
|
||||
if (op == "not")
|
||||
{
|
||||
if (parts.Length < 2 || !TryParseHexValue(parts[1], out var v))
|
||||
{ yield return ErrorItem("예: hex not 0xFF"); yield break; }
|
||||
var r8 = (byte)(~(byte)v);
|
||||
var r16 = (ushort)(~(ushort)v);
|
||||
yield return new LauncherItem($"NOT 0x{v:X} = 0x{r8:X2} (8bit) / 0x{r16:X4} (16bit)",
|
||||
"비트 반전", null, ("copy", $"0x{r8:X2}"), Symbol: "\uE8EF");
|
||||
yield return CopyItem("NOT 8bit", $"0x{r8:X2} ({r8})");
|
||||
yield return CopyItem("NOT 16bit", $"0x{r16:X4} ({r16})");
|
||||
yield return CopyItem("NOT 64bit", $"0x{(~v):X16}");
|
||||
yield break;
|
||||
}
|
||||
if (parts.Length < 3 || !TryParseHexValue(parts[1], out var a) || !TryParseHexValue(parts[2], out var b))
|
||||
{ yield return ErrorItem($"예: hex {op} 0xAB 0xCD"); yield break; }
|
||||
|
||||
ulong result = op switch
|
||||
{
|
||||
"add" => a + b,
|
||||
"xor" => a ^ b,
|
||||
"and" => a & b,
|
||||
"or" => a | b,
|
||||
_ => 0
|
||||
};
|
||||
var symbol = op switch { "add" => "+", "xor" => "^", "and" => "&", "or" => "|", _ => "?" };
|
||||
yield return new LauncherItem($"0x{a:X} {symbol} 0x{b:X} = 0x{result:X}",
|
||||
$"{a} {symbol} {b} = {result} · Enter 복사",
|
||||
null, ("copy", $"0x{result:X}"), Symbol: "\uE8EF");
|
||||
yield return CopyItem("16진수", $"0x{result:X}");
|
||||
yield return CopyItem("십진수", result.ToString());
|
||||
yield return CopyItem("이진수", $"0b{Convert.ToString((long)result, 2)}");
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildByteSize(long bytes)
|
||||
{
|
||||
yield return new LauncherItem($"{bytes:N0} bytes",
|
||||
"크기 단위 변환 · Enter 복사", null, ("copy", bytes.ToString()), Symbol: "\uE8EF");
|
||||
yield return CopyItem("Bytes", $"{bytes:N0}");
|
||||
yield return CopyItem("KB (1000)", $"{bytes / 1000.0:F3}");
|
||||
yield return CopyItem("KiB (1024)",$"{bytes / 1024.0:F3}");
|
||||
yield return CopyItem("MB (1000)", $"{bytes / 1_000_000.0:F3}");
|
||||
yield return CopyItem("MiB (1024)",$"{bytes / 1_048_576.0:F3}");
|
||||
yield return CopyItem("GB (1000)", $"{bytes / 1_000_000_000.0:F4}");
|
||||
yield return CopyItem("GiB (1024)",$"{bytes / 1_073_741_824.0:F4}");
|
||||
if (bytes >= 1_000_000_000_000L)
|
||||
yield return CopyItem("TB (1000)", $"{bytes / 1_000_000_000_000.0:F4}");
|
||||
}
|
||||
|
||||
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static bool TryParseHexValue(string s, out ulong val)
|
||||
{
|
||||
val = 0;
|
||||
s = s.Trim();
|
||||
if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
return ulong.TryParse(s[2..], System.Globalization.NumberStyles.HexNumber, null, out val);
|
||||
if (s.StartsWith("0X", StringComparison.OrdinalIgnoreCase))
|
||||
return ulong.TryParse(s[2..], System.Globalization.NumberStyles.HexNumber, null, out val);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsAllHex(string s) =>
|
||||
s.All(c => c is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F');
|
||||
|
||||
private static string? TrySafeUtf8(byte[] b)
|
||||
{
|
||||
try { var s = Encoding.UTF8.GetString(b); return s; }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static string? TrySafeAscii(byte[] b)
|
||||
{
|
||||
try { return Encoding.ASCII.GetString(b); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static LauncherItem CopyItem(string label, string value) =>
|
||||
new(label, value, null, ("copy", value), Symbol: "\uE8EF");
|
||||
|
||||
private static LauncherItem ErrorItem(string msg) =>
|
||||
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
|
||||
}
|
||||
277
src/AxCopilot/Handlers/PermHandler.cs
Normal file
277
src/AxCopilot/Handlers/PermHandler.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L20-4: Unix 파일 권한 계산기 핸들러. "perm" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: perm → 주요 권한 목록
|
||||
/// perm 755 → rwxr-xr-x 상세 설명
|
||||
/// perm 644 → rw-r--r-- 상세 설명
|
||||
/// perm rwxr-xr-x → 기호 → 숫자 변환
|
||||
/// perm rw-r--r-- → 기호 → 숫자 변환
|
||||
/// perm +x 644 → 실행 비트 추가
|
||||
/// perm -x 755 → 실행 비트 제거
|
||||
/// perm +w 444 → 쓰기 비트 추가
|
||||
/// perm umask 022 → umask 적용 결과
|
||||
/// perm common → 자주 쓰는 권한 목록
|
||||
/// Enter → 값 복사.
|
||||
/// </summary>
|
||||
public class PermHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "perm";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Perm",
|
||||
"Unix 파일 권한 계산기 — chmod 숫자↔기호·umask·권한 설명",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
private record CommonPerm(string Octal, string Symbol, string Description, string UseCase);
|
||||
|
||||
private static readonly CommonPerm[] Common =
|
||||
[
|
||||
new("777", "rwxrwxrwx", "모든 사용자 완전 권한", "임시 스크립트 (보안 주의)"),
|
||||
new("755", "rwxr-xr-x", "소유자 완전·그룹/기타 읽기+실행", "실행 파일, 디렉토리"),
|
||||
new("750", "rwxr-x---", "소유자 완전·그룹 읽기+실행·기타 없음", "그룹 공유 스크립트"),
|
||||
new("700", "rwx------", "소유자만 완전 권한", "개인 스크립트"),
|
||||
new("644", "rw-r--r--", "소유자 읽기+쓰기·그룹/기타 읽기만", "일반 파일"),
|
||||
new("640", "rw-r-----", "소유자 읽기+쓰기·그룹 읽기·기타 없음", "설정 파일"),
|
||||
new("600", "rw-------", "소유자만 읽기+쓰기", "개인 설정·SSH 키"),
|
||||
new("444", "r--r--r--", "모든 사용자 읽기만", "공유 읽기 전용"),
|
||||
new("400", "r--------", "소유자만 읽기", "SSL 인증서·개인 키"),
|
||||
new("666", "rw-rw-rw-", "모든 사용자 읽기+쓰기 (실행 없음)", "임시 파일"),
|
||||
new("664", "rw-rw-r--", "소유자+그룹 읽기+쓰기·기타 읽기", "그룹 협업 파일"),
|
||||
new("660", "rw-rw----", "소유자+그룹 읽기+쓰기·기타 없음", "그룹 협업 파일"),
|
||||
new("775", "rwxrwxr-x", "소유자+그룹 완전·기타 읽기+실행", "그룹 협업 디렉토리"),
|
||||
new("770", "rwxrwx---", "소유자+그룹 완전·기타 없음", "그룹 전용 디렉토리"),
|
||||
];
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
items.Add(new LauncherItem("Unix 파일 권한 계산기",
|
||||
"perm 755 / perm rwxr-xr-x / perm +x 644 / perm umask 022 / perm common",
|
||||
null, null, Symbol: "\uE8A5"));
|
||||
foreach (var p in Common.Take(6))
|
||||
items.Add(new LauncherItem($"{p.Octal} {p.Symbol}",
|
||||
$"{p.Description} ({p.UseCase})",
|
||||
null, ("copy", p.Octal), Symbol: "\uE8A5"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
|
||||
// common
|
||||
if (sub is "common" or "list" or "목록")
|
||||
{
|
||||
items.Add(new LauncherItem("자주 쓰는 파일 권한 목록", "", null, null, Symbol: "\uE8A5"));
|
||||
foreach (var p in Common)
|
||||
items.Add(new LauncherItem($"{p.Octal} {p.Symbol}",
|
||||
$"{p.Description} · {p.UseCase}",
|
||||
null, ("copy", p.Octal), Symbol: "\uE8A5"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// umask
|
||||
if (sub == "umask" && parts.Length >= 2)
|
||||
{
|
||||
if (TryParseOctal(parts[1], out var umaskVal))
|
||||
items.AddRange(BuildUmask(umaskVal));
|
||||
else
|
||||
items.Add(ErrorItem("umask 값은 3자리 8진수입니다 (예: 022)"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// +x / -x / +w / -w / +r / -r (비트 수정)
|
||||
if (sub.Length == 2 && sub[0] is '+' or '-' && sub[1] is 'r' or 'w' or 'x')
|
||||
{
|
||||
if (parts.Length >= 2 && TryParseOctal(parts[1], out var baseVal))
|
||||
items.AddRange(BuildBitModify(sub[0] == '+', sub[1], baseVal));
|
||||
else
|
||||
items.Add(ErrorItem($"예: perm {sub} 644"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 기호 표기 (rwxr-xr-x)
|
||||
if (sub.Length == 9 && sub.All(c => c is 'r' or 'w' or 'x' or '-'))
|
||||
{
|
||||
items.AddRange(BuildFromSymbol(sub));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 8진수 표기 (755, 644...)
|
||||
if (TryParseOctal(sub, out var octal))
|
||||
{
|
||||
items.AddRange(BuildFromOctal(octal));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
items.Add(new LauncherItem($"인식할 수 없는 입력: '{sub}'",
|
||||
"perm 755 / perm rwxr-xr-x / perm +x 644 / perm umask 022",
|
||||
null, null, Symbol: "\uE783"));
|
||||
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("Perm", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 빌더 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildFromOctal(int octal)
|
||||
{
|
||||
var sym = OctalToSymbol(octal);
|
||||
var desc = DescribeOctal(octal);
|
||||
var preset = Common.FirstOrDefault(p => p.Octal == octal.ToString("D3"));
|
||||
|
||||
yield return new LauncherItem($"{octal:D3} → {sym}",
|
||||
preset?.Description ?? desc,
|
||||
null, ("copy", octal.ToString("D3")), Symbol: "\uE8A5");
|
||||
|
||||
yield return CopyItem("8진수", octal.ToString("D3"));
|
||||
yield return CopyItem("기호 표기", sym);
|
||||
yield return CopyItem("chmod 명령", $"chmod {octal:D3} <파일>");
|
||||
|
||||
var parts = OctalToParts(octal);
|
||||
yield return new LauncherItem("소유자 (Owner)",
|
||||
$"{OctetToSymbol(parts[0])} ({BitsDesc(parts[0])})",
|
||||
null, null, Symbol: "\uE8A5");
|
||||
yield return new LauncherItem("그룹 (Group)",
|
||||
$"{OctetToSymbol(parts[1])} ({BitsDesc(parts[1])})",
|
||||
null, null, Symbol: "\uE8A5");
|
||||
yield return new LauncherItem("기타 (Others)",
|
||||
$"{OctetToSymbol(parts[2])} ({BitsDesc(parts[2])})",
|
||||
null, null, Symbol: "\uE8A5");
|
||||
|
||||
if (preset != null)
|
||||
yield return new LauncherItem("용도", preset.UseCase, null, null, Symbol: "\uE8A5");
|
||||
|
||||
// 관련 권한 제안
|
||||
yield return new LauncherItem("── 관련 권한 ──", "", null, null, Symbol: "\uE8A5");
|
||||
var related = Common.Where(p => Math.Abs(int.Parse(p.Octal) - octal) <= 11)
|
||||
.Take(4).ToList();
|
||||
foreach (var r in related)
|
||||
yield return new LauncherItem($"{r.Octal} {r.Symbol}", r.Description,
|
||||
null, ("copy", r.Octal), Symbol: "\uE8A5");
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildFromSymbol(string sym)
|
||||
{
|
||||
var octal = SymbolToOctal(sym);
|
||||
return BuildFromOctal(octal);
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildUmask(int umask)
|
||||
{
|
||||
var fileDefault = 0666;
|
||||
var dirDefault = 0777;
|
||||
var fileResult = fileDefault & ~umask;
|
||||
var dirResult = dirDefault & ~umask;
|
||||
|
||||
yield return new LauncherItem($"umask {umask:D3} → 파일: {fileResult:D3} 디렉토리: {dirResult:D3}",
|
||||
"umask 적용 결과", null, null, Symbol: "\uE8A5");
|
||||
yield return CopyItem("umask 값", umask.ToString("D3"));
|
||||
yield return CopyItem("기본 파일 권한", fileResult.ToString("D3"));
|
||||
yield return CopyItem("기본 디렉토리 권한", dirResult.ToString("D3"));
|
||||
yield return CopyItem("파일 기호", OctalToSymbol(fileResult));
|
||||
yield return CopyItem("디렉토리 기호", OctalToSymbol(dirResult));
|
||||
yield return new LauncherItem("설명",
|
||||
$"umask {umask:D3}: 파일={OctalToSymbol(fileResult)}, 디렉토리={OctalToSymbol(dirResult)}",
|
||||
null, null, Symbol: "\uE8A5");
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildBitModify(bool add, char bit, int baseOctal)
|
||||
{
|
||||
var mask = bit switch
|
||||
{
|
||||
'r' => 0444,
|
||||
'w' => 0222,
|
||||
'x' => 0111,
|
||||
_ => 0
|
||||
};
|
||||
var result = add ? (baseOctal | mask) : (baseOctal & ~mask);
|
||||
result &= 0777;
|
||||
|
||||
var label = add ? $"+{bit}" : $"-{bit}";
|
||||
yield return new LauncherItem($"{baseOctal:D3} {label} → {result:D3} ({OctalToSymbol(result)})",
|
||||
"Enter 복사", null, ("copy", result.ToString("D3")), Symbol: "\uE8A5");
|
||||
yield return CopyItem("변경 전", baseOctal.ToString("D3"));
|
||||
yield return CopyItem("변경 후", result.ToString("D3"));
|
||||
yield return CopyItem("기호", OctalToSymbol(result));
|
||||
yield return CopyItem("chmod", $"chmod {label} <파일> (또는 chmod {result:D3} <파일>)");
|
||||
}
|
||||
|
||||
// ── 변환 헬퍼 ────────────────────────────────────────────────────────────
|
||||
|
||||
private static bool TryParseOctal(string s, out int val)
|
||||
{
|
||||
val = 0;
|
||||
s = s.TrimStart('0');
|
||||
if (string.IsNullOrEmpty(s)) { val = 0; return true; }
|
||||
if (s.Length > 4) return false;
|
||||
try { val = Convert.ToInt32(s, 8); return val <= 0777; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static int[] OctalToParts(int octal) =>
|
||||
[(octal >> 6) & 7, (octal >> 3) & 7, octal & 7];
|
||||
|
||||
private static string OctetToSymbol(int v) =>
|
||||
$"{((v & 4) != 0 ? 'r' : '-')}{((v & 2) != 0 ? 'w' : '-')}{((v & 1) != 0 ? 'x' : '-')}";
|
||||
|
||||
private static string OctalToSymbol(int octal)
|
||||
{
|
||||
var p = OctalToParts(octal);
|
||||
return OctetToSymbol(p[0]) + OctetToSymbol(p[1]) + OctetToSymbol(p[2]);
|
||||
}
|
||||
|
||||
private static int SymbolToOctal(string sym)
|
||||
{
|
||||
int TriBit(int off) =>
|
||||
((sym[off] == 'r' ? 4 : 0) |
|
||||
(sym[off + 1] == 'w' ? 2 : 0) |
|
||||
(sym[off + 2] == 'x' ? 1 : 0));
|
||||
return (TriBit(0) << 6) | (TriBit(3) << 3) | TriBit(6);
|
||||
}
|
||||
|
||||
private static string BitsDesc(int v)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if ((v & 4) != 0) parts.Add("읽기");
|
||||
if ((v & 2) != 0) parts.Add("쓰기");
|
||||
if ((v & 1) != 0) parts.Add("실행");
|
||||
return parts.Count == 0 ? "없음" : string.Join("·", parts);
|
||||
}
|
||||
|
||||
private static string DescribeOctal(int octal)
|
||||
{
|
||||
var p = OctalToParts(octal);
|
||||
return $"소유자:{BitsDesc(p[0])} 그룹:{BitsDesc(p[1])} 기타:{BitsDesc(p[2])}";
|
||||
}
|
||||
|
||||
private static LauncherItem CopyItem(string label, string value) =>
|
||||
new(label, value, null, ("copy", value), Symbol: "\uE8A5");
|
||||
|
||||
private static LauncherItem ErrorItem(string msg) =>
|
||||
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
|
||||
}
|
||||
324
src/AxCopilot/Handlers/RandHandler.cs
Normal file
324
src/AxCopilot/Handlers/RandHandler.cs
Normal file
@@ -0,0 +1,324 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L20-2: 랜덤 생성기 핸들러. "rand" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: rand → 사용법 목록
|
||||
/// rand → 1~100 기본 난수
|
||||
/// rand 50 → 1~50 난수
|
||||
/// rand 10 99 → 10~99 난수
|
||||
/// rand str → 랜덤 문자열 (12자, 영숫자)
|
||||
/// rand str 20 → 20자 랜덤 문자열
|
||||
/// rand str 16 alpha → 16자 알파벳만
|
||||
/// rand str 8 num → 8자 숫자만
|
||||
/// rand str 12 hex → 12자 hex 문자열
|
||||
/// rand str 16 special → 특수문자 포함
|
||||
/// rand color → 랜덤 HEX 색상
|
||||
/// rand pick 항목1 항목2 → 목록에서 무작위 선택
|
||||
/// rand dice → 주사위 1d6
|
||||
/// rand dice 2d6 → 2개 주사위
|
||||
/// rand dice 1d20 → 20면체 주사위
|
||||
/// rand coin → 동전 던지기
|
||||
/// rand shuffle a b c d → 목록 셔플
|
||||
/// rand uuid → UUID v4
|
||||
/// rand token → 보안 토큰 (32자 hex)
|
||||
/// rand pin → 6자리 PIN
|
||||
/// rand pin 4 → 4자리 PIN
|
||||
/// Enter → 결과 복사.
|
||||
/// </summary>
|
||||
public class RandHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "rand";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Rand",
|
||||
"랜덤 생성기 — 숫자·문자열·색상·주사위·UUID·토큰·PIN·셔플",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
private static readonly Random _rng = Random.Shared;
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
// 기본: 1~100 난수 + 사용법
|
||||
var n = _rng.Next(1, 101);
|
||||
items.Add(new LauncherItem($"{n}",
|
||||
"1~100 랜덤 숫자 · Enter 복사", null, ("copy", n.ToString()), Symbol: "\uE8D0"));
|
||||
items.Add(new LauncherItem("사용법",
|
||||
"rand <max> / rand <min> <max> / rand str / rand color / rand dice / rand pick …",
|
||||
null, null, Symbol: "\uE8D0"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
|
||||
switch (sub)
|
||||
{
|
||||
// rand str [length] [charset]
|
||||
case "str" or "string" or "문자열":
|
||||
{
|
||||
int len = 12;
|
||||
if (parts.Length >= 2 && int.TryParse(parts[1], out var l)) len = Math.Clamp(l, 1, 256);
|
||||
var charset = parts.Length >= 3 ? parts[2].ToLowerInvariant() : "alnum";
|
||||
if (parts.Length == 2 && !int.TryParse(parts[1], out _)) charset = parts[1].ToLowerInvariant();
|
||||
var cs = GetCharset(charset);
|
||||
if (string.IsNullOrEmpty(cs)) { items.Add(ErrorItem("charset: alpha/num/alnum/hex/special")); break; }
|
||||
var result = RandStr(len, cs);
|
||||
items.Add(new LauncherItem(result, $"{len}자 랜덤 문자열 ({charset}) · Enter 복사",
|
||||
null, ("copy", result), Symbol: "\uE8D0"));
|
||||
// 다른 자릿수 미리 생성
|
||||
foreach (var altLen in new[] { 8, 16, 32 }.Where(x => x != len))
|
||||
items.Add(new LauncherItem(RandStr(altLen, cs), $"{altLen}자 ({charset})",
|
||||
null, ("copy", RandStr(altLen, cs)), Symbol: "\uE8D0"));
|
||||
break;
|
||||
}
|
||||
|
||||
// rand color
|
||||
case "color" or "색상" or "colour":
|
||||
{
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var r = _rng.Next(256); var g = _rng.Next(256); var b = _rng.Next(256);
|
||||
var hex = $"#{r:X2}{g:X2}{b:X2}";
|
||||
var rgb = $"rgb({r}, {g}, {b})";
|
||||
var hsl = RgbToHsl(r, g, b);
|
||||
items.Add(new LauncherItem(hex, $"{rgb} {hsl} · Enter 복사",
|
||||
null, ("copy", hex), Symbol: "\uE8D0"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// rand dice [NdS]
|
||||
case "dice" or "주사위":
|
||||
{
|
||||
var spec = parts.Length >= 2 ? parts[1].ToLowerInvariant() : "1d6";
|
||||
if (!TryParseDice(spec, out var dCount, out var dSides))
|
||||
{ items.Add(ErrorItem("형식: 1d6 / 2d10 / 1d20")); break; }
|
||||
var rolls = Enumerable.Range(0, dCount).Select(_ => _rng.Next(1, dSides + 1)).ToList();
|
||||
var total = rolls.Sum();
|
||||
var detail = string.Join(" + ", rolls);
|
||||
items.Add(new LauncherItem($"{dCount}d{dSides} → {total}",
|
||||
$"각 주사위: {detail} · Enter 복사", null, ("copy", total.ToString()), Symbol: "\uE8D0"));
|
||||
if (dCount > 1)
|
||||
{
|
||||
items.Add(CopyItem("합계", total.ToString()));
|
||||
items.Add(CopyItem("상세", detail));
|
||||
items.Add(CopyItem("최솟값", rolls.Min().ToString()));
|
||||
items.Add(CopyItem("최댓값", rolls.Max().ToString()));
|
||||
}
|
||||
// 다시 굴리기 미리보기
|
||||
items.Add(new LauncherItem("── 다시 굴리기 (미리보기) ──", "", null, null, Symbol: "\uE8D0"));
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var r2 = Enumerable.Range(0, dCount).Select(_ => _rng.Next(1, dSides + 1)).ToList();
|
||||
items.Add(new LauncherItem($"{r2.Sum()}", string.Join(" + ", r2),
|
||||
null, ("copy", r2.Sum().ToString()), Symbol: "\uE8D0"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// rand coin
|
||||
case "coin" or "동전":
|
||||
{
|
||||
var result = _rng.Next(2) == 0 ? "앞면 (Head)" : "뒷면 (Tail)";
|
||||
items.Add(new LauncherItem(result, "동전 던지기 · Enter 복사",
|
||||
null, ("copy", result), Symbol: "\uE8D0"));
|
||||
// 연속 5회
|
||||
items.Add(new LauncherItem("── 연속 5회 ──", "", null, null, Symbol: "\uE8D0"));
|
||||
for (int i = 0; i < 5; i++)
|
||||
items.Add(new LauncherItem(_rng.Next(2) == 0 ? "앞면 ⬤" : "뒷면 ○",
|
||||
$"#{i + 1}", null, null, Symbol: "\uE8D0"));
|
||||
break;
|
||||
}
|
||||
|
||||
// rand pick item1 item2 ...
|
||||
case "pick" or "선택":
|
||||
{
|
||||
if (parts.Length < 2) { items.Add(ErrorItem("예: rand pick 치킨 피자 짜장면")); break; }
|
||||
var pool = parts[1..];
|
||||
var picked = pool[_rng.Next(pool.Length)];
|
||||
items.Add(new LauncherItem(picked,
|
||||
$"{pool.Length}개 중 선택 · Enter 복사",
|
||||
null, ("copy", picked), Symbol: "\uE8D0"));
|
||||
items.Add(new LauncherItem("── 다시 선택 ──", "", null, null, Symbol: "\uE8D0"));
|
||||
for (int i = 0; i < Math.Min(3, pool.Length); i++)
|
||||
items.Add(new LauncherItem(pool[_rng.Next(pool.Length)],
|
||||
$"후보 {i + 1}", null, null, Symbol: "\uE8D0"));
|
||||
break;
|
||||
}
|
||||
|
||||
// rand shuffle item1 item2 ...
|
||||
case "shuffle" or "섞기":
|
||||
{
|
||||
if (parts.Length < 2) { items.Add(ErrorItem("예: rand shuffle a b c d e")); break; }
|
||||
var list = parts[1..].ToList();
|
||||
for (int i = list.Count - 1; i > 0; i--)
|
||||
{
|
||||
var j = _rng.Next(i + 1);
|
||||
(list[i], list[j]) = (list[j], list[i]);
|
||||
}
|
||||
var joined = string.Join(" ", list);
|
||||
items.Add(new LauncherItem(joined,
|
||||
$"{list.Count}개 셔플 · Enter 복사",
|
||||
null, ("copy", joined), Symbol: "\uE8D0"));
|
||||
items.Add(CopyItem("쉼표 구분", string.Join(", ", list)));
|
||||
for (int r = 0; r < list.Count; r++)
|
||||
items.Add(new LauncherItem($"#{r + 1} {list[r]}", "", null, null, Symbol: "\uE8D0"));
|
||||
break;
|
||||
}
|
||||
|
||||
// rand uuid
|
||||
case "uuid":
|
||||
{
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var guid = Guid.NewGuid().ToString();
|
||||
items.Add(new LauncherItem(guid, $"UUID v4 · Enter 복사",
|
||||
null, ("copy", guid), Symbol: "\uE8D0"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// rand token
|
||||
case "token" or "secret" or "key":
|
||||
{
|
||||
int tLen = 32;
|
||||
if (parts.Length >= 2 && int.TryParse(parts[1], out var tl)) tLen = Math.Clamp(tl, 8, 128);
|
||||
var tokenBytes = new byte[tLen];
|
||||
System.Security.Cryptography.RandomNumberGenerator.Fill(tokenBytes);
|
||||
var tokenHex = BitConverter.ToString(tokenBytes).Replace("-", "").ToLowerInvariant();
|
||||
var tokenB64 = Convert.ToBase64String(tokenBytes);
|
||||
items.Add(new LauncherItem(tokenHex, $"{tLen}바이트 보안 토큰 (hex) · Enter 복사",
|
||||
null, ("copy", tokenHex), Symbol: "\uE8D0"));
|
||||
items.Add(CopyItem("Hex", tokenHex));
|
||||
items.Add(CopyItem("Base64", tokenB64));
|
||||
break;
|
||||
}
|
||||
|
||||
// rand pin [length]
|
||||
case "pin":
|
||||
{
|
||||
int pLen = 6;
|
||||
if (parts.Length >= 2 && int.TryParse(parts[1], out var pl)) pLen = Math.Clamp(pl, 4, 12);
|
||||
var pin = string.Concat(Enumerable.Range(0, pLen).Select(_ => _rng.Next(10)));
|
||||
items.Add(new LauncherItem(pin, $"{pLen}자리 PIN · Enter 복사",
|
||||
null, ("copy", pin), Symbol: "\uE8D0"));
|
||||
items.Add(new LauncherItem("── 추가 PIN ──", "", null, null, Symbol: "\uE8D0"));
|
||||
for (int i = 0; i < 4; i++)
|
||||
items.Add(new LauncherItem(
|
||||
string.Concat(Enumerable.Range(0, pLen).Select(_ => _rng.Next(10))),
|
||||
$"{pLen}자리", null, null, Symbol: "\uE8D0"));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
// rand <max> 또는 rand <min> <max>
|
||||
if (int.TryParse(sub, out var max))
|
||||
{
|
||||
int min = 1;
|
||||
if (parts.Length >= 2 && int.TryParse(parts[1], out var mx)) { min = max; max = mx; }
|
||||
if (min > max) (min, max) = (max, min);
|
||||
var n = _rng.Next(min, max + 1);
|
||||
items.Add(new LauncherItem($"{n}",
|
||||
$"{min}~{max} 랜덤 숫자 · Enter 복사",
|
||||
null, ("copy", n.ToString()), Symbol: "\uE8D0"));
|
||||
items.Add(new LauncherItem("── 추가 결과 ──", "", null, null, Symbol: "\uE8D0"));
|
||||
for (int i = 0; i < 4; i++)
|
||||
items.Add(new LauncherItem(_rng.Next(min, max + 1).ToString(),
|
||||
$"{min}~{max}", null, null, Symbol: "\uE8D0"));
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(new LauncherItem($"알 수 없는 서브커맨드: '{sub}'",
|
||||
"rand / rand <n> / rand str / rand color / rand dice / rand pick / rand uuid / rand token / rand pin",
|
||||
null, null, Symbol: "\uE783"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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("Rand", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static string GetCharset(string name) => name switch
|
||||
{
|
||||
"alpha" or "알파" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
"num" or "숫자" => "0123456789",
|
||||
"alnum" or "영숫자" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||
"hex" => "0123456789abcdef",
|
||||
"lower" => "abcdefghijklmnopqrstuvwxyz",
|
||||
"upper" => "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
"special" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*",
|
||||
_ => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
};
|
||||
|
||||
private static string RandStr(int len, string charset)
|
||||
{
|
||||
var sb = new StringBuilder(len);
|
||||
for (int i = 0; i < len; i++) sb.Append(charset[_rng.Next(charset.Length)]);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static bool TryParseDice(string s, out int count, out int sides)
|
||||
{
|
||||
count = sides = 0;
|
||||
var d = s.IndexOf('d');
|
||||
if (d < 0) return false;
|
||||
var left = d == 0 ? "1" : s[..d];
|
||||
var right = s[(d + 1)..];
|
||||
return int.TryParse(left, out count) && count >= 1 && count <= 100 &&
|
||||
int.TryParse(right, out sides) && sides >= 2 && sides <= 10000;
|
||||
}
|
||||
|
||||
private static string RgbToHsl(int r, int g, int b)
|
||||
{
|
||||
var rf = r / 255.0; var gf = g / 255.0; var bf = b / 255.0;
|
||||
var max = Math.Max(rf, Math.Max(gf, bf));
|
||||
var min = Math.Min(rf, Math.Min(gf, bf));
|
||||
var l = (max + min) / 2;
|
||||
if (max == min) return $"hsl(0, 0%, {l:P0})";
|
||||
var d = max - min;
|
||||
var s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
var h = max == rf ? (gf - bf) / d + (gf < bf ? 6 : 0)
|
||||
: max == gf ? (bf - rf) / d + 2
|
||||
: (rf - gf) / d + 4;
|
||||
h /= 6;
|
||||
return $"hsl({h * 360:F0}, {s:P0}, {l:P0})";
|
||||
}
|
||||
|
||||
private static LauncherItem CopyItem(string label, string value) =>
|
||||
new(label, value, null, ("copy", value), Symbol: "\uE8D0");
|
||||
|
||||
private static LauncherItem ErrorItem(string msg) =>
|
||||
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
|
||||
}
|
||||
459
src/AxCopilot/Handlers/StrHandler.cs
Normal file
459
src/AxCopilot/Handlers/StrHandler.cs
Normal file
@@ -0,0 +1,459 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L20-3: 문자열 조작 도구 핸들러. "str" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: str → 클립보드 텍스트 조작 메뉴
|
||||
/// str escape html → HTML 특수문자 이스케이프
|
||||
/// str unescape html → HTML 이스케이프 해제
|
||||
/// str escape url → URL 인코딩 (퍼센트)
|
||||
/// str unescape url → URL 디코딩
|
||||
/// str escape json → JSON 문자열 이스케이프
|
||||
/// str escape regex → 정규식 이스케이프
|
||||
/// str repeat 3 → 클립보드 텍스트 3회 반복
|
||||
/// str repeat 5 , → 쉼표 구분 5회 반복
|
||||
/// str pad 20 → 20자 우측 공백 패딩
|
||||
/// str pad 20 left → 좌측 패딩
|
||||
/// str pad 20 * right → 지정 문자로 우측 패딩
|
||||
/// str wrap 80 → 80자 줄바꿈
|
||||
/// str lines → 줄 수·단어·문자 통계
|
||||
/// str sort → 줄 정렬 (오름차순)
|
||||
/// str sort desc → 줄 정렬 (내림차순)
|
||||
/// str unique → 중복 줄 제거
|
||||
/// str join , → 여러 줄 → 쉼표 구분 한 줄
|
||||
/// str split , → 쉼표 구분 → 여러 줄
|
||||
/// str replace a b → 텍스트 내 a를 b로 교체
|
||||
/// str extract email → 이메일 주소 추출
|
||||
/// str extract url → URL 추출
|
||||
/// str extract number → 숫자 추출
|
||||
/// Enter → 결과 복사.
|
||||
/// </summary>
|
||||
public partial class StrHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "str";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Str",
|
||||
"문자열 조작 도구 — HTML/URL/JSON 이스케이프·반복·패딩·줄 정렬·추출",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
items.Add(new LauncherItem("문자열 조작 도구",
|
||||
"str escape/unescape / str repeat / str pad / str sort / str extract …",
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
if (!string.IsNullOrWhiteSpace(clipboard))
|
||||
{
|
||||
var preview = clipboard!.Length > 40 ? clipboard[..40] + "…" : clipboard;
|
||||
items.Add(new LauncherItem($"클립보드: \"{preview}\"",
|
||||
$"{clipboard.Length}자 · 아래 서브커맨드로 조작", null, null, Symbol: "\uE8AB"));
|
||||
BuildQuickMenu(items, clipboard!);
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(new LauncherItem("클립보드가 비어 있습니다", "텍스트를 복사한 뒤 사용하세요",
|
||||
null, null, Symbol: "\uE946"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
var text = parts.Length > 1
|
||||
? string.Join(" ", parts[1..]) // 인라인 텍스트 (없으면 클립보드)
|
||||
: clipboard ?? "";
|
||||
|
||||
// escape / unescape
|
||||
if (sub is "escape" or "esc" or "unescape" or "unesc")
|
||||
{
|
||||
var isEscape = sub is "escape" or "esc";
|
||||
var target = parts.Length >= 2 ? parts[1].ToLowerInvariant() : "html";
|
||||
var src = parts.Length >= 3 ? string.Join(" ", parts[2..]) : clipboard ?? "";
|
||||
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("처리할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var (html, url, json, reg) = (
|
||||
isEscape ? HtmlEncode(src) : HtmlDecode(src),
|
||||
isEscape ? Uri.EscapeDataString(src) : Uri.UnescapeDataString(src),
|
||||
isEscape ? JsonEscape(src) : JsonUnescape(src),
|
||||
isEscape ? RegexEscape(src) : src
|
||||
);
|
||||
var label = isEscape ? "이스케이프" : "이스케이프 해제";
|
||||
items.Add(new LauncherItem($"── {label} ──", "", null, null, Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("HTML", isEscape ? HtmlEncode(src) : HtmlDecode(src)));
|
||||
items.Add(CopyItem("URL", isEscape ? Uri.EscapeDataString(src) : Uri.UnescapeDataString(src)));
|
||||
items.Add(CopyItem("JSON", isEscape ? JsonEscape(src) : JsonUnescape(src)));
|
||||
if (isEscape) items.Add(CopyItem("Regex", RegexEscape(src)));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// repeat
|
||||
if (sub == "repeat")
|
||||
{
|
||||
int count = 3;
|
||||
string sep = "";
|
||||
string src = clipboard ?? "";
|
||||
|
||||
if (parts.Length >= 2 && int.TryParse(parts[1], out var n)) count = Math.Clamp(n, 1, 100);
|
||||
if (parts.Length >= 3) sep = parts[2] == "\\n" ? "\n" : parts[2] == "\\t" ? "\t" : parts[2];
|
||||
if (parts.Length >= 4) src = string.Join(" ", parts[3..]);
|
||||
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("반복할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var result = string.Join(sep, Enumerable.Repeat(src, count));
|
||||
items.Add(new LauncherItem(result.Length > 60 ? result[..60] + "…" : result,
|
||||
$"{count}회 반복 · Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("결과", result));
|
||||
items.Add(CopyItem("결과 길이", $"{result.Length}자"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// pad
|
||||
if (sub == "pad")
|
||||
{
|
||||
if (parts.Length < 2 || !int.TryParse(parts[1], out var width))
|
||||
{ items.Add(ErrorItem("예: str pad 20 / str pad 20 left / str pad 20 * right")); }
|
||||
else
|
||||
{
|
||||
var side = parts.Length >= 3 ? parts[2].ToLowerInvariant() : "right";
|
||||
var padChar = ' ';
|
||||
if (parts.Length >= 4 && parts[2].Length == 1) { padChar = parts[2][0]; side = parts[3].ToLowerInvariant(); }
|
||||
if (parts.Length >= 3 && parts[2].Length == 1 && !"left right both".Contains(parts[2])) padChar = parts[2][0];
|
||||
|
||||
var src = clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("클립보드에 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var result = side switch
|
||||
{
|
||||
"left" => src.PadLeft(width, padChar),
|
||||
"both" => src.PadLeft((src.Length + width) / 2, padChar).PadRight(width, padChar),
|
||||
_ => src.PadRight(width, padChar)
|
||||
};
|
||||
items.Add(new LauncherItem($"\"{result}\"", $"{side} 패딩 {width}자 · Enter 복사",
|
||||
null, ("copy", result), Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("결과", result));
|
||||
}
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// wrap
|
||||
if (sub == "wrap")
|
||||
{
|
||||
if (parts.Length < 2 || !int.TryParse(parts[1], out var cols))
|
||||
{ items.Add(ErrorItem("예: str wrap 80")); }
|
||||
else
|
||||
{
|
||||
var src = parts.Length >= 3 ? string.Join(" ", parts[2..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("줄바꿈할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var result = WordWrap(src, cols);
|
||||
var preview = result.Split('\n').Take(5);
|
||||
items.Add(new LauncherItem($"{cols}자 줄바꿈",
|
||||
$"{result.Split('\n').Length}줄 · Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
foreach (var line in preview)
|
||||
items.Add(new LauncherItem(line.Length > 60 ? line[..60] + "…" : line, "",
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
}
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// sort
|
||||
if (sub == "sort")
|
||||
{
|
||||
var src = parts.Length >= 3 ? string.Join(" ", parts[2..]) : clipboard ?? "";
|
||||
var desc = parts.Length >= 2 && parts[1].ToLowerInvariant() is "desc" or "d";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("정렬할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var lines = src.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sorted = desc ? lines.OrderByDescending(l => l).ToArray()
|
||||
: lines.OrderBy(l => l).ToArray();
|
||||
var result = string.Join("\n", sorted);
|
||||
items.Add(new LauncherItem($"{sorted.Length}줄 {(desc ? "내림차순" : "오름차순")} 정렬",
|
||||
"Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
foreach (var line in sorted.Take(6))
|
||||
items.Add(new LauncherItem(line.Length > 60 ? line[..60] : line, "",
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// unique
|
||||
if (sub is "unique" or "dedup")
|
||||
{
|
||||
var src = parts.Length >= 2 ? string.Join(" ", parts[1..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("중복 제거할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var lines = src.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var unique = lines.Distinct(StringComparer.Ordinal).ToArray();
|
||||
var result = string.Join("\n", unique);
|
||||
items.Add(new LauncherItem($"{lines.Length}줄 → {unique.Length}줄 (중복 {lines.Length - unique.Length}개 제거)",
|
||||
"Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
foreach (var line in unique.Take(6))
|
||||
items.Add(new LauncherItem(line.Length > 60 ? line[..60] : line, "",
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// join
|
||||
if (sub == "join")
|
||||
{
|
||||
var sep = parts.Length >= 2 ? parts[1] : ",";
|
||||
if (sep == "\\n") sep = "\n"; else if (sep == "\\t") sep = "\t";
|
||||
var src = clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("연결할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var lines = src.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var result = string.Join(sep, lines);
|
||||
items.Add(new LauncherItem(result.Length > 60 ? result[..60] + "…" : result,
|
||||
$"{lines.Length}줄 연결 · Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("결과", result));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// split
|
||||
if (sub == "split")
|
||||
{
|
||||
var sep = parts.Length >= 2 ? parts[1] : ",";
|
||||
if (sep == "\\n") sep = "\n"; else if (sep == "\\t") sep = "\t";
|
||||
var src = parts.Length >= 3 ? string.Join(" ", parts[2..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("분리할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var splitted = src.Split(sep, StringSplitOptions.None);
|
||||
var result = string.Join("\n", splitted);
|
||||
items.Add(new LauncherItem($"'{sep}'로 분리 → {splitted.Length}개",
|
||||
"Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
foreach (var item in splitted.Take(8))
|
||||
items.Add(new LauncherItem(item.Length > 60 ? item[..60] : item, "",
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// replace
|
||||
if (sub == "replace")
|
||||
{
|
||||
if (parts.Length < 3) { items.Add(ErrorItem("예: str replace 찾을텍스트 바꿀텍스트")); }
|
||||
else
|
||||
{
|
||||
var from = parts[1];
|
||||
var to = parts[2];
|
||||
var src = parts.Length >= 4 ? string.Join(" ", parts[3..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("처리할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var count = 0;
|
||||
var result = ReplaceCount(src, from, to, out count);
|
||||
items.Add(new LauncherItem($"'{from}' → '{to}' ({count}개 교체)",
|
||||
"Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("결과", result));
|
||||
}
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// extract
|
||||
if (sub == "extract")
|
||||
{
|
||||
var target = parts.Length >= 2 ? parts[1].ToLowerInvariant() : "url";
|
||||
var src = parts.Length >= 3 ? string.Join(" ", parts[2..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("추출할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var matches = target switch
|
||||
{
|
||||
"email" => EmailRegex().Matches(src).Select(m => m.Value).Distinct().ToList(),
|
||||
"url" => UrlRegex().Matches(src).Select(m => m.Value).Distinct().ToList(),
|
||||
"num" or "number" or "숫자" =>
|
||||
NumberRegex().Matches(src).Select(m => m.Value).ToList(),
|
||||
"ip" => IpRegex().Matches(src).Select(m => m.Value).Distinct().ToList(),
|
||||
_ => new List<string>()
|
||||
};
|
||||
if (matches.Count == 0)
|
||||
items.Add(new LauncherItem($"'{target}' 패턴 없음", src.Length > 40 ? src[..40] : src,
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
else
|
||||
{
|
||||
items.Add(new LauncherItem($"{target} {matches.Count}개 추출",
|
||||
"Enter로 전체 복사", null, ("copy", string.Join("\n", matches)), Symbol: "\uE8AB"));
|
||||
foreach (var m in matches.Take(10))
|
||||
items.Add(new LauncherItem(m, "", null, ("copy", m), Symbol: "\uE8AB"));
|
||||
}
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// lines
|
||||
if (sub is "lines" or "info" or "count")
|
||||
{
|
||||
var src = clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("분석할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var lineArr = src.Split('\n');
|
||||
var words = src.Split(new[] {' ','\t','\n','\r'}, StringSplitOptions.RemoveEmptyEntries);
|
||||
items.Add(new LauncherItem($"{lineArr.Length}줄 · {words.Length}단어 · {src.Length}자",
|
||||
"텍스트 분석", null, null, Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("전체 줄 수", lineArr.Length.ToString()));
|
||||
items.Add(CopyItem("빈 줄 수", lineArr.Count(l => string.IsNullOrWhiteSpace(l)).ToString()));
|
||||
items.Add(CopyItem("단어 수", words.Length.ToString()));
|
||||
items.Add(CopyItem("전체 문자 수", src.Length.ToString()));
|
||||
items.Add(CopyItem("공백 제외 문자", src.Count(c => !char.IsWhiteSpace(c)).ToString()));
|
||||
items.Add(CopyItem("바이트 (UTF-8)", Encoding.UTF8.GetByteCount(src).ToString()));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 알 수 없는 커맨드
|
||||
items.Add(new LauncherItem($"알 수 없는 서브커맨드: '{sub}'",
|
||||
"escape · unescape · repeat · pad · wrap · sort · unique · join · split · replace · extract · lines",
|
||||
null, null, Symbol: "\uE783"));
|
||||
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("Str", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static void BuildQuickMenu(List<LauncherItem> items, string src)
|
||||
{
|
||||
items.Add(CopyItem("HTML 이스케이프", HtmlEncode(src)));
|
||||
items.Add(CopyItem("URL 인코딩", Uri.EscapeDataString(src)));
|
||||
items.Add(CopyItem("JSON 이스케이프", JsonEscape(src)));
|
||||
}
|
||||
|
||||
private static string HtmlEncode(string s) => s
|
||||
.Replace("&", "&").Replace("<", "<").Replace(">", ">")
|
||||
.Replace("\"", """).Replace("'", "'");
|
||||
|
||||
private static string HtmlDecode(string s) =>
|
||||
HttpUtility.HtmlDecode(s);
|
||||
|
||||
private static string JsonEscape(string s)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var c in s)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"': sb.Append("\\\""); break;
|
||||
case '\\': sb.Append("\\\\"); break;
|
||||
case '\n': sb.Append("\\n"); break;
|
||||
case '\r': sb.Append("\\r"); break;
|
||||
case '\t': sb.Append("\\t"); break;
|
||||
default:
|
||||
if (c < 0x20) sb.Append($"\\u{(int)c:X4}");
|
||||
else sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string JsonUnescape(string s) =>
|
||||
s.Replace("\\\"", "\"").Replace("\\\\", "\\")
|
||||
.Replace("\\n", "\n").Replace("\\r", "\r").Replace("\\t", "\t");
|
||||
|
||||
private static string RegexEscape(string s) => Regex.Escape(s);
|
||||
|
||||
private static string WordWrap(string text, int cols)
|
||||
{
|
||||
var words = text.Split(' ');
|
||||
var sb = new StringBuilder();
|
||||
int colPos = 0;
|
||||
foreach (var word in words)
|
||||
{
|
||||
if (colPos + word.Length + 1 > cols && colPos > 0) { sb.Append('\n'); colPos = 0; }
|
||||
else if (colPos > 0) { sb.Append(' '); colPos++; }
|
||||
sb.Append(word);
|
||||
colPos += word.Length;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string ReplaceCount(string src, string from, string to, out int count)
|
||||
{
|
||||
count = 0;
|
||||
var sb = new StringBuilder();
|
||||
int pos = 0;
|
||||
while (true)
|
||||
{
|
||||
var idx = src.IndexOf(from, pos, StringComparison.Ordinal);
|
||||
if (idx < 0) { sb.Append(src[pos..]); break; }
|
||||
sb.Append(src[pos..idx]);
|
||||
sb.Append(to);
|
||||
count++;
|
||||
pos = idx + from.Length;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static LauncherItem CopyItem(string label, string value) =>
|
||||
new(label, value, null, ("copy", value), Symbol: "\uE8AB");
|
||||
|
||||
private static LauncherItem ErrorItem(string msg) =>
|
||||
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
|
||||
|
||||
[GeneratedRegex(@"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}")]
|
||||
private static partial Regex EmailRegex();
|
||||
|
||||
[GeneratedRegex(@"https?://[^\s""'<>]+")]
|
||||
private static partial Regex UrlRegex();
|
||||
|
||||
[GeneratedRegex(@"-?\d+(\.\d+)?")]
|
||||
private static partial Regex NumberRegex();
|
||||
|
||||
[GeneratedRegex(@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")]
|
||||
private static partial Regex IpRegex();
|
||||
}
|
||||
Reference in New Issue
Block a user