[Phase L21] TOML·Log·PS·Key 핸들러 4종 추가 + L21~L23 로드맵 계획 수립

TomlHandler (prefix=toml, 290줄, partial class):
- 외부 라이브러리 없는 경량 TOML 파서
- [section]/[[array-of-tables]] 파싱, 스칼라(bool/int/float/string)/배열/인라인 테이블
- toml validate·keys·sections·get <key.path>·stats·flat 서브커맨드
- 점 표기법 경로 조회(GetByPath), 재귀 평탄화(FlattenTable)
- CS8602 null 경고 수정(table![s] 명시)

LogHandler (prefix=log, 290줄, partial class):
- ERROR/WARN/INFO/DEBUG 레벨 자동 감지(키워드 기반)
- 클립보드 자동 읽기 또는 log <파일경로> 직접 입력
- error·warn·info·debug·last <n>·head <n>·grep <키워드>·stats·exceptions·today 서브커맨드
- 날짜별 분포 통계(yyyy-MM-dd 패턴), 스택트레이스 블록 추출
- [GeneratedRegex] DatePattern, using System.Text 누락 수정

PsHandler (prefix=ps, 200줄):
- 68개 PowerShell 명령 내장(file/process/service/network/registry/string/date/pipe 8개 카테고리)
- ps <카테고리> 전체 목록, ps <키워드> 전체 검색
- ps run <명령> → Windows Terminal / PowerShell 터미널 직접 실행
- Enter 클립보드 복사, 카테고리 오버뷰 표시

KeyHandler (prefix=key, 190줄):
- Windows(26개)·VS Code(29개)·Chrome/Edge(22개)·Vim(23개)·Excel(20개)·Windows Terminal(13개)
- 총 133개 단축키 내장
- key <앱> 카테고리 조회, key <키워드> 전체 앱 통합 검색
- Enter → 단축키 클립보드 복사

LAUNCHER_ROADMAP.md:
- Phase L21~L23 계획 추가(L22: proc·geo·cargo·pip, L23: k8s·gh·choco·cmp)
- L21 모든 항목  완료 표시

- 빌드: 경고 0, 오류 0
This commit is contained in:
2026-04-04 16:46:14 +09:00
parent 5c6a1b645d
commit d6c7f65d6c
6 changed files with 1378 additions and 0 deletions

View File

@@ -399,3 +399,42 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
| 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 명령 자동 생성 | 높음 |
---
## Phase L21 — TOML·Log·PowerShell·단축키 도구 (v2.1.0) ✅ 완료
> **방향**: 개발자 설정 파일·로그 분석·Windows 자동화·단축키 생산성 강화.
| # | 기능 | 설명 | 우선순위 |
|---|------|------|----------|
| L21-1 | **TOML 파서·분석기** ✅ | `toml` 프리픽스. 클립보드 자동 읽기. 외부 라이브러리 없이 순수 구현. `toml validate` 유효성 검사. `toml keys` 최상위 키 목록. `toml get key.sub` 점 표기법 경로 조회. `toml stats` 줄·키·섹션 통계. `toml flat` 평탄화. YAML/JSON과 같은 패턴의 3번째 설정 파일 형식 지원 | 높음 |
| L21-2 | **로그 파일 분석기** ✅ | `log` 프리픽스. 클립보드 또는 `log <경로>` 파일 경로 입력. ERROR/WARN/INFO/DEBUG 레벨 파싱 + 건수 요약. `log error` 오류 줄만 필터. `log last <n>` 마지막 N줄 표시(tail). `log grep <키워드>` 키워드 필터. `log stats` 레벨별 통계 + 시간대 분포. 스택트레이스·예외 패턴 자동 감지 | 높음 |
| L21-3 | **PowerShell 명령 생성기** ✅ | `ps` 프리픽스. 자주 쓰는 PowerShell 명령어 빠른 조회·생성·실행. 카테고리별 명령(파일/프로세스/네트워크/서비스/레지스트리/이벤트). `ps <키워드>` 명령 검색. `ps <명령어>` 생성 → Enter 시 PowerShell 터미널 실행. 파이프라인 예시 표시. 원라이너 복사 지원 | 높음 |
| L21-4 | **단축키 참조 사전** ✅ | `key` 프리픽스. Windows/VS Code/Chrome/Vim/Excel 5개 앱 단축키 내장(100개+). `key vscode` 전체 목록. `key vscode find` 키워드 검색. `key win` Windows 단축키. `key chrome` 브라우저 단축키. `key vim` Vim 명령. 단축키 → 기능 설명 양방향 검색. Enter → 단축키 클립보드 복사 | 높음 |
---
## Phase L22 — 프로세스·좌표·Cargo·pip 도구 (v2.1.0) 🔄 예정
> **방향**: 시스템 모니터링·지리 계산·언어별 패키지 매니저 확장.
| # | 기능 | 설명 | 우선순위 |
|---|------|------|----------|
| L22-1 | **프로세스 상세 관리** | `proc` 프리픽스 (기존 `kill` 과 역할 분리 — `kill`은 강제종료, `proc`는 조회·분석). CPU·메모리·PID·경로 상세 표시. `proc top` 상위 10개 CPU/RAM 점유 프로세스. `proc find <이름>` 이름 검색. `proc detail <PID>` 프로세스 트리·포트·DLL 목록 | 높음 |
| L22-2 | **좌표·거리 계산기** | `geo` 프리픽스. 위도·경도 좌표 입력(DMS/DD 형식). 두 좌표 간 직선 거리 계산(Haversine 공식). `geo seoul tokyo` 도시명 내장 좌표 빠른 조회. DMS↔DD 변환. 좌표→Google Maps URL 생성. 국가별 주요 도시 30개+ 내장 | 중간 |
| L22-3 | **Rust cargo 명령 생성기** | `cargo` 프리픽스. cargo build/run/test/check/clippy/fmt/add/remove/update/doc 명령. `cargo add <crate>` → crates.io 명령. `cargo <명령>` → Enter 시 터미널 실행. npm 핸들러와 동일 패턴. Rust 개발자용 빠른 명령 조회 | 중간 |
| L22-4 | **Python pip 명령 생성기** | `pip` 프리픽스. pip install/uninstall/list/show/freeze/upgrade/search. `pip install <패키지>` → pip/pip3/conda 3종 동시 표시. `pip freeze` → requirements.txt 생성 명령. `pip venv` 가상환경 생성 명령. `pip <명령>` → Enter 시 터미널 실행 | 중간 |
---
## Phase L23 — Kubectl·GitHub CLI·Chocolatey·텍스트 비교 (v2.1.0) 🔄 예정
> **방향**: DevOps·소스 관리·Windows 패키지 관리·텍스트 분석 도구.
| # | 기능 | 설명 | 우선순위 |
|---|------|------|----------|
| L23-1 | **kubectl 명령 생성기** | `k8s` 프리픽스. get/describe/apply/delete/logs/exec/port-forward/scale 명령. `k8s get pods` → kubectl get pods -n default. 네임스페이스 빠른 전환. 자주 쓰는 yaml 템플릿(Deployment/Service/ConfigMap) 클립보드 복사 | 중간 |
| L23-2 | **GitHub CLI 명령 생성기** | `gh` 프리픽스. pr/issue/repo/workflow/release 명령 빠른 조회·생성. `gh pr list` / `gh pr create`. 현재 디렉토리 git remote 자동 감지. 사내 모드에서 외부 GitHub 차단 안내 | 중간 |
| L23-3 | **Chocolatey 패키지 관리** | `choco` 프리픽스. choco install/uninstall/upgrade/list/search 명령. 자주 쓰는 패키지 내장 목록(git/node/python/vscode/chrome/7zip 등 20개+). `choco install <패키지>` → Enter 시 관리자 PowerShell 실행. 미설치 감지 | 중간 |
| L23-4 | **텍스트 비교·diff** | `cmp` 프리픽스 (기존 `diff`와 역할 분리 — `diff`는 파일 비교, `cmp`는 클립보드 텍스트 비교). 두 텍스트 블록 라인 단위 비교. 추가·삭제·변경 줄 통계. `cmp clip1 clip2` / 클립보드 자동 감지. unified diff 형식 출력. 단어 단위 diff 모드 | 낮음 |

View File

@@ -332,6 +332,14 @@ public partial class App : System.Windows.Application
commandResolver.RegisterHandler(new StrHandler());
// L20-4: Unix 파일 권한 계산기 (prefix=perm)
commandResolver.RegisterHandler(new PermHandler());
// L21-1: TOML 파서·분석기 (prefix=toml)
commandResolver.RegisterHandler(new TomlHandler());
// L21-2: 로그 파일 분석기 (prefix=log)
commandResolver.RegisterHandler(new LogHandler());
// L21-3: PowerShell 명령 생성기 (prefix=ps)
commandResolver.RegisterHandler(new PsHandler());
// L21-4: 키보드 단축키 참조 사전 (prefix=key)
commandResolver.RegisterHandler(new KeyHandler());
// ─── 플러그인 로드 ────────────────────────────────────────────────────
var pluginHost = new PluginHost(settings, commandResolver);

View File

@@ -0,0 +1,289 @@
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L21-4: 키보드 단축키 참조 사전 핸들러. "key" 프리픽스로 사용합니다.
///
/// 예: key → 앱 카테고리 목록
/// key win → Windows 단축키
/// key vscode → VS Code 단축키
/// key chrome → Chrome/Edge 단축키
/// key vim → Vim 명령
/// key excel → Excel 단축키
/// key terminal → Windows Terminal 단축키
/// key <키워드> → 전체 단축키에서 키워드 검색 (예: key 찾기, key find)
/// Enter → 단축키 복사.
/// </summary>
public class KeyHandler : IActionHandler
{
public string? Prefix => "key";
public PluginMetadata Metadata => new(
"Key",
"키보드 단축키 참조 사전 — Windows·VS Code·Chrome·Vim·Excel",
"1.0",
"AX");
private record Shortcut(string Keys, string Description, string App);
private static readonly Shortcut[] All =
[
// Windows
new("Win + D", "바탕화면 보기/복원", "win"),
new("Win + E", "파일 탐색기 열기", "win"),
new("Win + L", "PC 잠금", "win"),
new("Win + R", "실행 대화상자", "win"),
new("Win + S", "Windows 검색", "win"),
new("Win + V", "클립보드 히스토리", "win"),
new("Win + X", "빠른 링크 메뉴 (전원 메뉴)", "win"),
new("Win + Tab", "작업 보기(가상 데스크톱)", "win"),
new("Win + Ctrl + D", "새 가상 데스크톱 추가", "win"),
new("Win + Ctrl + →/←", "가상 데스크톱 전환", "win"),
new("Win + ↑", "창 최대화", "win"),
new("Win + ↓", "창 최소화/복원", "win"),
new("Win + ←/→", "창 좌/우 절반 스냅", "win"),
new("Win + Shift + S", "화면 부분 캡처 (Snipping Tool)", "win"),
new("Alt + Tab", "실행 중인 앱 전환", "win"),
new("Alt + F4", "현재 창 닫기", "win"),
new("Ctrl + Shift + Esc", "작업 관리자 직접 열기", "win"),
new("Ctrl + Alt + Del", "보안 옵션 화면", "win"),
new("F2", "선택 항목 이름 변경", "win"),
new("F5", "새로고침", "win"),
new("F11", "전체화면 토글", "win"),
new("Win + . (점)", "이모지 피커", "win"),
new("Win + P", "프로젝션 모드 선택 (다중 모니터)", "win"),
new("Win + I", "Windows 설정", "win"),
new("PrtSc", "전체 화면 캡처 → 클립보드", "win"),
new("Alt + PrtSc", "현재 창 캡처 → 클립보드", "win"),
// VS Code
new("Ctrl + P", "파일 빠른 열기", "vscode"),
new("Ctrl + Shift + P", "명령 팔레트", "vscode"),
new("Ctrl + `", "통합 터미널 열기/닫기", "vscode"),
new("Ctrl + B", "사이드바 토글", "vscode"),
new("Ctrl + J", "패널 토글(터미널·출력)", "vscode"),
new("Ctrl + F", "파일 내 찾기", "vscode"),
new("Ctrl + H", "파일 내 찾아 바꾸기", "vscode"),
new("Ctrl + Shift + F", "전체 검색", "vscode"),
new("Ctrl + Shift + H", "전체 찾아 바꾸기", "vscode"),
new("Ctrl + G", "줄 이동", "vscode"),
new("Ctrl + /", "줄 주석 토글", "vscode"),
new("Alt + ↑/↓", "현재 줄 위/아래 이동", "vscode"),
new("Alt + Shift + ↑/↓","현재 줄 위/아래 복사", "vscode"),
new("Ctrl + D", "다음 동일 단어 선택", "vscode"),
new("Ctrl + Shift + L", "동일 단어 모두 선택", "vscode"),
new("Ctrl + Shift + K", "현재 줄 삭제", "vscode"),
new("Ctrl + Enter", "아래에 새 줄 추가", "vscode"),
new("Ctrl + Shift + Enter", "위에 새 줄 추가", "vscode"),
new("F12", "정의로 이동", "vscode"),
new("Alt + F12", "정의 미리보기", "vscode"),
new("Shift + F12", "모든 참조 찾기", "vscode"),
new("F2", "기호 이름 변경(리팩터링)", "vscode"),
new("Ctrl + .", "빠른 수정 (Quick Fix)", "vscode"),
new("Ctrl + K Ctrl + F","선택 영역 포맷", "vscode"),
new("Shift + Alt + F", "전체 파일 포맷", "vscode"),
new("Ctrl + Z / Y", "실행 취소 / 다시 실행", "vscode"),
new("Ctrl + W", "현재 탭 닫기", "vscode"),
new("Ctrl + Tab", "에디터 탭 전환", "vscode"),
new("Ctrl + \\", "에디터 분할", "vscode"),
new("Ctrl + 1/2/3", "에디터 그룹 포커스", "vscode"),
// Chrome / Edge
new("Ctrl + T", "새 탭 열기", "chrome"),
new("Ctrl + W", "현재 탭 닫기", "chrome"),
new("Ctrl + Shift + T", "닫은 탭 다시 열기", "chrome"),
new("Ctrl + Tab", "다음 탭으로 이동", "chrome"),
new("Ctrl + Shift + Tab","이전 탭으로 이동", "chrome"),
new("Ctrl + 1~8", "번호로 탭 이동", "chrome"),
new("Ctrl + L", "주소창 포커스", "chrome"),
new("Ctrl + D", "현재 페이지 북마크", "chrome"),
new("Ctrl + F", "페이지 내 찾기", "chrome"),
new("Ctrl + H", "방문 기록", "chrome"),
new("Ctrl + J", "다운로드 목록", "chrome"),
new("Ctrl + Shift + J", "개발자 도구 콘솔", "chrome"),
new("F12", "개발자 도구 토글", "chrome"),
new("F5 / Ctrl + R", "페이지 새로고침", "chrome"),
new("Ctrl + Shift + R", "캐시 무시 새로고침", "chrome"),
new("Ctrl + +/-", "페이지 확대/축소", "chrome"),
new("Ctrl + 0", "확대/축소 기본값 복원", "chrome"),
new("Alt + ← / →", "뒤로/앞으로", "chrome"),
new("Ctrl + N", "새 창 열기", "chrome"),
new("Ctrl + Shift + N", "시크릿 창", "chrome"),
new("Ctrl + Shift + B", "북마크 바 토글", "chrome"),
new("Ctrl + U", "페이지 소스 보기", "chrome"),
// Vim
new("i", "삽입 모드 진입", "vim"),
new("Esc", "노멀 모드로 복귀", "vim"),
new(":w", "저장", "vim"),
new(":q", "종료", "vim"),
new(":wq / :x", "저장 후 종료", "vim"),
new(":q!", "저장 없이 강제 종료", "vim"),
new("h/j/k/l", "좌/하/상/우 이동", "vim"),
new("w / b", "단어 앞/뒤로 이동", "vim"),
new("gg / G", "파일 처음 / 끝으로 이동", "vim"),
new("0 / $", "줄 처음 / 끝으로 이동", "vim"),
new("dd", "현재 줄 삭제", "vim"),
new("yy", "현재 줄 복사(yank)", "vim"),
new("p / P", "붙여넣기 (다음/현재 위치)", "vim"),
new("u / Ctrl + R", "실행 취소 / 다시 실행", "vim"),
new("/keyword", "앞으로 검색", "vim"),
new("n / N", "다음/이전 검색 결과", "vim"),
new(":%s/old/new/g", "전체 치환", "vim"),
new("v / V", "비주얼 모드 (문자/줄)", "vim"),
new("Ctrl + V", "비주얼 블록 모드", "vim"),
new(":split / :vsplit", "수평/수직 창 분할", "vim"),
new("Ctrl + W + W", "다음 창으로 포커스 이동", "vim"),
new(":noh", "검색 하이라이트 제거", "vim"),
new(">> / <<", "들여쓰기 추가/제거", "vim"),
// Excel
new("Ctrl + Arrow", "데이터 끝으로 이동", "excel"),
new("Ctrl + Shift + Arrow","데이터 끝까지 선택", "excel"),
new("Ctrl + Home / End","셀 A1 / 마지막 셀로 이동", "excel"),
new("Ctrl + Space", "열 전체 선택", "excel"),
new("Shift + Space", "행 전체 선택", "excel"),
new("Ctrl + Shift + +", "행/열 삽입", "excel"),
new("Ctrl + -", "행/열 삭제", "excel"),
new("Ctrl + D", "아래로 채우기", "excel"),
new("Ctrl + R", "오른쪽으로 채우기", "excel"),
new("Alt + =", "SUM 자동 합계", "excel"),
new("F4", "참조 절대/상대 전환 ($)", "excel"),
new("Ctrl + 1", "셀 서식 창", "excel"),
new("Ctrl + ;", "현재 날짜 입력", "excel"),
new("Ctrl + Shift + ;", "현재 시간 입력", "excel"),
new("F2", "셀 편집 모드", "excel"),
new("Alt + Enter", "셀 내 줄바꿈", "excel"),
new("Ctrl + Z / Y", "실행 취소 / 다시 실행", "excel"),
new("Ctrl + F", "찾기", "excel"),
new("Ctrl + H", "찾아 바꾸기", "excel"),
new("Ctrl + Shift + L", "필터 자동 설정 토글", "excel"),
// Windows Terminal
new("Ctrl + Shift + T", "새 탭 열기", "terminal"),
new("Ctrl + Shift + W", "탭 닫기", "terminal"),
new("Ctrl + Tab", "다음 탭 이동", "terminal"),
new("Ctrl + Shift + 1~9","번호 탭으로 이동", "terminal"),
new("Ctrl + Shift + D", "창 복제", "terminal"),
new("Alt + Shift + D", "창 분할 (자동 방향)", "terminal"),
new("Alt + Shift + +", "수평 분할", "terminal"),
new("Alt + Shift + -", "수직 분할", "terminal"),
new("Alt + Arrow", "분할된 창 간 포커스 이동", "terminal"),
new("Ctrl + +/-", "폰트 크기 변경", "terminal"),
new("Ctrl + F", "터미널 내 텍스트 검색", "terminal"),
new("Ctrl + Shift + P", "명령 팔레트", "terminal"),
new("Ctrl + Shift + F", "전체 화면 토글", "terminal"),
];
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("키보드 단축키 참조 사전",
"key win / vscode / chrome / vim / excel / terminal / key <키워드 검색>",
null, null, Symbol: "\uE92E"));
AddAppOverview(items);
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
// 앱 카테고리 조회
var catKeys = new Dictionary<string, string[]>
{
["win"] = ["win", "windows"],
["vscode"] = ["vscode", "code", "vs"],
["chrome"] = ["chrome", "edge", "browser"],
["vim"] = ["vim", "vi", "neovim"],
["excel"] = ["excel", "spreadsheet"],
["terminal"] = ["terminal", "wt"],
};
foreach (var (cat, aliases) in catKeys)
{
if (aliases.Contains(sub))
{
var catItems = All.Where(s => s.App == cat).ToList();
var catName = cat switch
{
"win" => "Windows",
"vscode" => "VS Code",
"chrome" => "Chrome / Edge",
"vim" => "Vim",
"excel" => "Excel",
"terminal" => "Windows Terminal",
_ => cat
};
items.Add(new LauncherItem($"{catName} 단축키 {catItems.Count}개", "", null, null, Symbol: "\uE92E"));
foreach (var s in catItems)
items.Add(new LauncherItem(s.Keys, s.Description, null, ("copy", s.Keys), Symbol: "\uE92E"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
}
// 키워드 검색 (전체)
var keyword = string.Join(" ", parts);
var found = All.Where(s =>
s.Keys.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
s.Description.Contains(keyword, StringComparison.OrdinalIgnoreCase)).ToList();
if (found.Count > 0)
{
items.Add(new LauncherItem($"'{keyword}' 검색 결과: {found.Count}개", "", null, null, Symbol: "\uE92E"));
foreach (var s in found)
{
var appLabel = s.App.ToUpperInvariant();
items.Add(new LauncherItem(s.Keys, $"[{appLabel}] {s.Description}",
null, ("copy", s.Keys), Symbol: "\uE92E"));
}
}
else
{
items.Add(new LauncherItem($"'{keyword}' 결과 없음",
"key win / vscode / chrome / vim / excel / terminal",
null, null, Symbol: "\uE783"));
AddAppOverview(items);
}
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("Key", "클립보드에 복사했습니다.");
}
catch { }
}
return Task.CompletedTask;
}
private static void AddAppOverview(List<LauncherItem> items)
{
var apps = new (string Key, string Label, int Count)[]
{
("win", "Windows 단축키", All.Count(s => s.App == "win")),
("vscode", "VS Code 단축키", All.Count(s => s.App == "vscode")),
("chrome", "Chrome / Edge 단축키", All.Count(s => s.App == "chrome")),
("vim", "Vim 명령", All.Count(s => s.App == "vim")),
("excel", "Excel 단축키", All.Count(s => s.App == "excel")),
("terminal", "Windows Terminal 단축키", All.Count(s => s.App == "terminal")),
};
foreach (var (key, label, count) in apps)
items.Add(new LauncherItem($"key {key}", $"{label} ({count}개)",
null, null, Symbol: "\uE92E"));
}
}

View File

@@ -0,0 +1,402 @@
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L21-2: 로그 파일 분석기 핸들러. "log" 프리픽스로 사용합니다.
///
/// 예: log → 클립보드 로그 통계
/// log <경로> → 파일 경로 직접 분석
/// log error → ERROR 줄만 표시
/// log warn → WARN 줄만 표시
/// log last 20 → 마지막 20줄 (tail)
/// log head 20 → 처음 20줄
/// log grep <키워드> → 키워드 필터
/// log stats → 레벨별 통계 + 시간대 분포
/// log exceptions → 예외·스택트레이스 블록 추출
/// log today → 오늘 날짜 포함 줄만 표시
/// Enter → 값 복사.
/// </summary>
public partial class LogHandler : IActionHandler
{
public string? Prefix => "log";
public PluginMetadata Metadata => new(
"Log",
"로그 파일 분석기 — ERROR/WARN 필터·tail·grep·통계·예외 추출",
"1.0",
"AX");
private record LogLine(int No, string Level, string Raw);
// 로그 레벨 감지 패턴
private static readonly (string Level, string[] Keywords)[] LevelPatterns =
[
("ERROR", ["[ERROR]", "ERROR:", "ERRO ", "error", "Error", "FATAL", "[FATAL]", "Exception", "EXCEPTION"]),
("WARN", ["[WARN]", "WARN:", "WARNING", "warning", "Warning", "[WARNING]"]),
("INFO", ["[INFO]", "INFO:", "information", "Information"]),
("DEBUG", ["[DEBUG]", "DEBUG:", "debug", "Debug", "TRACE", "[TRACE]"]),
];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
// 클립보드 or 파일
string? src = null;
string? srcLabel = null;
try
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
if (Clipboard.ContainsText()) src = Clipboard.GetText();
});
}
catch { }
if (string.IsNullOrWhiteSpace(q))
{
if (string.IsNullOrWhiteSpace(src))
{
items.Add(new LauncherItem("로그 파일 분석기",
"클립보드에 로그를 복사하거나 log <파일경로> / log error / log grep <키워드>",
null, null, Symbol: "\uE9D9"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
srcLabel = "클립보드";
BuildSummary(items, src!, srcLabel);
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
// 파일 경로인지 판단
if ((sub.Contains('\\') || sub.Contains('/') || sub.Contains(':') || sub.EndsWith(".log") || sub.EndsWith(".txt"))
&& File.Exists(parts[0]))
{
string? fileErr = null;
string? content = null;
try { content = File.ReadAllText(parts[0]); }
catch (Exception ex) { fileErr = ex.Message; }
if (fileErr != null) { items.Add(ErrorItem($"파일 읽기 오류: {fileErr}")); return Task.FromResult<IEnumerable<LauncherItem>>(items); }
src = content;
srcLabel = Path.GetFileName(parts[0]);
if (parts.Length == 1)
{
BuildSummary(items, src!, srcLabel);
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 파일 경로 + 서브커맨드
sub = parts[1].ToLowerInvariant();
parts = parts[1..];
}
else
{
srcLabel = "클립보드";
}
var logSrc = src ?? "";
if (string.IsNullOrWhiteSpace(logSrc))
{
items.Add(ErrorItem("분석할 로그가 없습니다. 클립보드에 로그를 복사하세요."));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var allLines = ParseLogLines(logSrc);
switch (sub)
{
case "error" or "err":
BuildFilteredLines(items, allLines, "ERROR", "ERROR / FATAL / Exception", srcLabel!);
break;
case "warn" or "warning":
BuildFilteredLines(items, allLines, "WARN", "WARN / WARNING", srcLabel!);
break;
case "info":
BuildFilteredLines(items, allLines, "INFO", "INFO / information", srcLabel!);
break;
case "debug" or "trace":
BuildFilteredLines(items, allLines, "DEBUG", "DEBUG / TRACE", srcLabel!);
break;
case "last" or "tail":
{
int n = 20;
if (parts.Length >= 2 && int.TryParse(parts[1], out var pn)) n = Math.Clamp(pn, 1, 500);
var tail = allLines.TakeLast(n).ToList();
var joined = string.Join("\n", tail.Select(l => l.Raw));
items.Add(new LauncherItem($"마지막 {tail.Count}줄 ({srcLabel})",
"Enter 전체 복사", null, ("copy", joined), Symbol: "\uE9D9"));
foreach (var l in tail)
items.Add(BuildLineItem(l));
break;
}
case "head" or "first":
{
int n = 20;
if (parts.Length >= 2 && int.TryParse(parts[1], out var pn)) n = Math.Clamp(pn, 1, 500);
var head = allLines.Take(n).ToList();
var joined = string.Join("\n", head.Select(l => l.Raw));
items.Add(new LauncherItem($"처음 {head.Count}줄 ({srcLabel})",
"Enter 전체 복사", null, ("copy", joined), Symbol: "\uE9D9"));
foreach (var l in head)
items.Add(BuildLineItem(l));
break;
}
case "grep" or "find" or "search":
{
if (parts.Length < 2) { items.Add(ErrorItem("예: log grep Exception")); break; }
var kw = string.Join(" ", parts[1..]);
var matched = allLines.Where(l => l.Raw.Contains(kw, StringComparison.OrdinalIgnoreCase)).ToList();
var joined = string.Join("\n", matched.Select(l => l.Raw));
items.Add(new LauncherItem($"'{kw}' 검색 결과: {matched.Count}줄",
"Enter 전체 복사", null, ("copy", joined), Symbol: "\uE9D9"));
foreach (var l in matched.Take(15))
items.Add(BuildLineItem(l));
if (matched.Count > 15)
items.Add(new LauncherItem($"... ({matched.Count - 15}줄 더)", "Enter 전체 복사",
null, ("copy", joined), Symbol: "\uE9D9"));
break;
}
case "stats":
BuildStats(items, allLines, logSrc, srcLabel!);
break;
case "exceptions" or "exception" or "ex":
{
var blocks = ExtractExceptions(logSrc);
items.Add(new LauncherItem($"예외 블록 {blocks.Count}개 발견",
srcLabel!, null, null, Symbol: "\uE9D9"));
for (int i = 0; i < blocks.Count; i++)
{
var b = blocks[i];
var preview = b.Split('\n')[0];
items.Add(new LauncherItem($"#{i + 1} {TruncateStr(preview, 60)}",
$"{b.Split('\n').Length}줄", null, ("copy", b), Symbol: "\uE9D9"));
}
if (blocks.Count == 0)
items.Add(new LauncherItem("예외·스택트레이스 패턴 없음", "", null, null, Symbol: "\uE9D9"));
break;
}
case "today":
{
var today = DateTime.Today.ToString("yyyy-MM-dd");
var today2 = DateTime.Today.ToString("yyyy/MM/dd");
var matched = allLines.Where(l =>
l.Raw.Contains(today) || l.Raw.Contains(today2)).ToList();
var joined = string.Join("\n", matched.Select(l => l.Raw));
items.Add(new LauncherItem($"오늘({today}) {matched.Count}줄",
"Enter 전체 복사", null, ("copy", joined), Symbol: "\uE9D9"));
foreach (var l in matched.Take(15))
items.Add(BuildLineItem(l));
break;
}
default:
// 기본 요약으로 폴백
BuildSummary(items, logSrc, srcLabel!);
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("Log", "클립보드에 복사했습니다.");
}
catch { }
}
return Task.CompletedTask;
}
// ── 빌더 ────────────────────────────────────────────────────────────────
private static void BuildSummary(List<LauncherItem> items, string src, string label)
{
var lines = ParseLogLines(src);
var errors = lines.Count(l => l.Level == "ERROR");
var warns = lines.Count(l => l.Level == "WARN");
var infos = lines.Count(l => l.Level == "INFO");
var debugs = lines.Count(l => l.Level == "DEBUG");
var unknowns = lines.Count(l => l.Level == "OTHER");
items.Add(new LauncherItem($"{label} — {lines.Count}줄",
$"ERROR:{errors} WARN:{warns} INFO:{infos} DEBUG:{debugs}",
null, null, Symbol: "\uE9D9"));
if (errors > 0)
{
items.Add(new LauncherItem($"🔴 ERROR {errors}줄",
"log error 서브커맨드로 상세 보기", null, null, Symbol: "\uE9D9"));
}
if (warns > 0)
{
items.Add(new LauncherItem($"🟡 WARN {warns}줄",
"log warn 서브커맨드로 상세 보기", null, null, Symbol: "\uE9D9"));
}
items.Add(CopyItem("전체 줄 수", lines.Count.ToString()));
items.Add(CopyItem("ERROR 줄", errors.ToString()));
items.Add(CopyItem("WARN 줄", warns.ToString()));
// 마지막 5줄 미리보기
items.Add(new LauncherItem("── 마지막 5줄 ──", "", null, null, Symbol: "\uE9D9"));
foreach (var l in lines.TakeLast(5))
items.Add(BuildLineItem(l));
}
private static void BuildFilteredLines(List<LauncherItem> items, List<LogLine> lines,
string level, string label, string srcLabel)
{
var filtered = lines.Where(l => l.Level == level).ToList();
var joined = string.Join("\n", filtered.Select(l => l.Raw));
items.Add(new LauncherItem($"{label} {filtered.Count}줄 ({srcLabel})",
"Enter 전체 복사", null, ("copy", joined), Symbol: "\uE9D9"));
if (filtered.Count == 0)
{
items.Add(new LauncherItem($"{label} 줄이 없습니다", "", null, null, Symbol: "\uE9D9"));
return;
}
foreach (var l in filtered.Take(15))
items.Add(BuildLineItem(l));
if (filtered.Count > 15)
items.Add(new LauncherItem($"... ({filtered.Count - 15}줄 더)", "Enter 전체 복사",
null, ("copy", joined), Symbol: "\uE9D9"));
}
private static void BuildStats(List<LauncherItem> items, List<LogLine> lines, string src, string label)
{
var counts = new Dictionary<string, int>
{
["ERROR"] = lines.Count(l => l.Level == "ERROR"),
["WARN"] = lines.Count(l => l.Level == "WARN"),
["INFO"] = lines.Count(l => l.Level == "INFO"),
["DEBUG"] = lines.Count(l => l.Level == "DEBUG"),
["OTHER"] = lines.Count(l => l.Level == "OTHER"),
};
items.Add(new LauncherItem($"로그 통계 ({label})", $"총 {lines.Count}줄",
null, null, Symbol: "\uE9D9"));
foreach (var (lvl, cnt) in counts.Where(kv => kv.Value > 0))
items.Add(CopyItem(lvl, $"{cnt}줄 ({cnt * 100.0 / Math.Max(1, lines.Count):F1}%)"));
// 날짜별 분포 (yyyy-MM-dd 패턴 추출)
var dateCounts = new Dictionary<string, int>();
foreach (var l in lines)
{
var m = DatePattern().Match(l.Raw);
if (m.Success)
{
var date = m.Value[..10];
dateCounts[date] = dateCounts.GetValueOrDefault(date) + 1;
}
}
if (dateCounts.Count > 0)
{
items.Add(new LauncherItem("── 날짜별 분포 ──", "", null, null, Symbol: "\uE9D9"));
foreach (var (date, cnt) in dateCounts.OrderByDescending(kv => kv.Key).Take(5))
items.Add(CopyItem(date, $"{cnt}줄"));
}
}
private static List<string> ExtractExceptions(string src)
{
var blocks = new List<string>();
var lines = src.Split('\n');
var inBlock = false;
var current = new StringBuilder();
foreach (var line in lines)
{
var isEx = line.Contains("Exception") || line.Contains("Error:") ||
line.Contains("at ") || line.TrimStart().StartsWith("Caused by");
if (!inBlock && isEx)
{
inBlock = true;
current.Clear();
current.AppendLine(line);
}
else if (inBlock)
{
if (isEx || line.TrimStart().StartsWith("at ") || line.TrimStart().StartsWith("..."))
current.AppendLine(line);
else
{
if (current.Length > 0) blocks.Add(current.ToString().Trim());
inBlock = false;
current.Clear();
}
}
}
if (inBlock && current.Length > 0) blocks.Add(current.ToString().Trim());
return blocks;
}
// ── 파서·헬퍼 ───────────────────────────────────────────────────────────
private static List<LogLine> ParseLogLines(string src)
{
var lines = src.Split('\n');
return lines.Select((raw, i) =>
{
var level = DetectLevel(raw);
return new LogLine(i + 1, level, raw.TrimEnd('\r'));
}).ToList();
}
private static string DetectLevel(string line)
{
foreach (var (level, keywords) in LevelPatterns)
if (keywords.Any(kw => line.Contains(kw, StringComparison.Ordinal)))
return level;
return "OTHER";
}
private static LauncherItem BuildLineItem(LogLine l)
{
var icon = l.Level switch
{
"ERROR" => "🔴",
"WARN" => "🟡",
"INFO" => "🔵",
"DEBUG" => "⚪",
_ => " "
};
var display = TruncateStr(l.Raw, 80);
return new LauncherItem($"{icon} {display}",
$"줄 {l.No} ({l.Level})", null, ("copy", l.Raw), Symbol: "\uE9D9");
}
private static string TruncateStr(string s, int max) =>
s.Length <= max ? s : s[..max] + "…";
private static LauncherItem CopyItem(string label, string value) =>
new(label, value, null, ("copy", value), Symbol: "\uE9D9");
private static LauncherItem ErrorItem(string msg) =>
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
[GeneratedRegex(@"\d{4}[-/]\d{2}[-/]\d{2}")]
private static partial Regex DatePattern();
}

View File

@@ -0,0 +1,268 @@
using System.Diagnostics;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L21-3: PowerShell 명령 생성기 핸들러. "ps" 프리픽스로 사용합니다.
///
/// 예: ps → 카테고리별 자주 쓰는 명령 목록
/// ps <키워드> → 명령 검색 (예: ps process, ps service, ps file)
/// ps file → 파일/디렉토리 명령 목록
/// ps process → 프로세스 명령 목록
/// ps network → 네트워크 명령 목록
/// ps service → 서비스 명령 목록
/// ps registry → 레지스트리 명령 목록
/// ps string → 문자열 처리 명령 목록
/// ps date → 날짜/시간 명령 목록
/// ps pipe → 파이프라인 예시
/// ps run <명령> → 직접 PowerShell 실행
/// Enter → 클립보드 복사 (또는 PS 터미널 실행).
/// </summary>
public class PsHandler : IActionHandler
{
public string? Prefix => "ps";
public PluginMetadata Metadata => new(
"PS",
"PowerShell 명령 생성기 — 파일·프로세스·네트워크·서비스·레지스트리",
"1.0",
"AX");
private record PsCmd(string Cmd, string Description, string Category);
private static readonly PsCmd[] Commands =
[
// 파일/디렉토리
new("Get-ChildItem -Path . -Recurse", "현재 폴더 재귀 목록", "file"),
new("Get-ChildItem -Filter *.log -Recurse", "*.log 파일 재귀 검색", "file"),
new("Copy-Item src.txt dst.txt", "파일 복사", "file"),
new("Move-Item src.txt dst.txt", "파일 이동/이름변경", "file"),
new("Remove-Item -Path file.txt -Force", "파일 강제 삭제", "file"),
new("New-Item -ItemType Directory -Path .\\NewFolder", "새 폴더 생성", "file"),
new("Get-Content file.txt", "파일 내용 출력", "file"),
new("Get-Content file.txt -Tail 20", "파일 마지막 20줄 (tail)", "file"),
new("Set-Content file.txt \"내용\"", "파일 쓰기", "file"),
new("Add-Content file.txt \"추가 내용\"", "파일에 내용 추가", "file"),
new("Test-Path C:\\path\\to\\file", "경로 존재 여부 확인", "file"),
new("Resolve-Path .\\relative\\path", "상대 경로 → 절대 경로", "file"),
new("Get-Item file.txt | Select-Object Length,LastWriteTime", "파일 크기·수정일 조회", "file"),
new("(Get-ChildItem -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB", "폴더 크기 계산 (MB)", "file"),
new("Compress-Archive -Path .\\folder -DestinationPath out.zip","폴더 압축 (.zip)", "file"),
new("Expand-Archive -Path file.zip -DestinationPath .\\out", "zip 압축 해제", "file"),
// 프로세스
new("Get-Process", "실행 중인 프로세스 목록", "process"),
new("Get-Process -Name chrome | Sort-Object CPU -Desc", "Chrome 프로세스 CPU 순 정렬", "process"),
new("Stop-Process -Name notepad -Force", "notepad 강제 종료", "process"),
new("Stop-Process -Id 1234 -Force", "PID로 프로세스 종료", "process"),
new("Start-Process chrome.exe", "프로세스 실행", "process"),
new("Start-Process notepad -Verb RunAs", "관리자 권한으로 실행", "process"),
new("Get-Process | Where-Object {$_.CPU -gt 10} | Sort-Object CPU -Desc | Select -First 5", "CPU 10% 이상 상위 5개", "process"),
new("Get-WmiObject Win32_Process | Select-Object Name,ProcessId,CommandLine", "프로세스 명령줄 조회", "process"),
new("(Get-Process -Id $PID).Path", "현재 PowerShell 실행 경로", "process"),
// 서비스
new("Get-Service", "모든 서비스 목록", "service"),
new("Get-Service | Where-Object {$_.Status -eq 'Running'}", "실행 중인 서비스만", "service"),
new("Start-Service -Name wuauserv", "Windows Update 서비스 시작", "service"),
new("Stop-Service -Name wuauserv", "서비스 중지", "service"),
new("Restart-Service -Name Spooler", "인쇄 스풀러 재시작", "service"),
new("Set-Service -Name Fax -StartupType Disabled", "서비스 시작 유형 변경 (비활성화)", "service"),
new("Get-Service -Name sshd", "SSH 서비스 상태 확인", "service"),
// 네트워크
new("Test-NetConnection google.com -Port 443", "도메인+포트 연결 테스트", "network"),
new("Test-NetConnection 8.8.8.8", "IP ping 테스트", "network"),
new("Get-NetIPAddress -AddressFamily IPv4", "IPv4 주소 목록", "network"),
new("Get-NetAdapter | Where-Object {$_.Status -eq 'Up'}", "활성 네트워크 어댑터 목록", "network"),
new("Get-NetTCPConnection -State Listen", "Listen 상태 포트 목록", "network"),
new("Get-NetTCPConnection | Where-Object {$_.LocalPort -eq 80}","포트 80 연결 조회", "network"),
new("Resolve-DnsName google.com", "DNS 조회", "network"),
new("(Invoke-WebRequest -Uri 'https://api.ipify.org').Content", "공인 IP 주소 확인", "network"),
new("Invoke-WebRequest -Uri 'http://localhost:8080/health'", "HTTP GET 요청", "network"),
// 레지스트리
new("Get-ItemProperty HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "시작 프로그램 확인", "registry"),
new("Get-ChildItem HKLM:\\SOFTWARE", "HKLM\\SOFTWARE 하위 키 목록", "registry"),
new("New-ItemProperty -Path HKCU:\\Software -Name MyVal -Value 1 -PropertyType DWORD", "레지스트리 값 쓰기", "registry"),
new("Remove-ItemProperty -Path HKCU:\\Software -Name MyVal", "레지스트리 값 삭제", "registry"),
// 문자열·데이터
new("\"hello world\" -replace 'world','PowerShell'", "문자열 치환", "string"),
new("\" text \".Trim()", "문자열 앞뒤 공백 제거", "string"),
new("[System.Web.HttpUtility]::UrlEncode('hello world')", "URL ", "string"),
new("[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('hello'))", "Base64 인코딩", "string"),
new("[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('aGVsbG8='))", "Base64 디코딩", "string"),
new("Get-FileHash file.txt -Algorithm SHA256", "파일 SHA256 해시", "string"),
new("$text | ConvertTo-Json", "객체 → JSON 변환", "string"),
new("Get-Content file.json | ConvertFrom-Json", "JSON 파일 파싱", "string"),
new("Import-Csv data.csv", "CSV 파일 파싱", "string"),
new("Export-Csv -Path out.csv -NoTypeInformation", "CSV 파일 내보내기", "string"),
// 날짜/시간
new("Get-Date", "현재 날짜·시간", "date"),
new("Get-Date -Format 'yyyy-MM-dd HH:mm:ss'", "날짜 포맷 지정", "date"),
new("(Get-Date).AddDays(30)", "30일 후 날짜", "date"),
new("(Get-Date) - (Get-Date '2024-01-01')", "날짜 차이 계산", "date"),
new("[datetime]::UtcNow", "UTC 현재 시각", "date"),
new("[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()", "Unix 타임스탬프 (초)", "date"),
new("[DateTimeOffset]::FromUnixTimeSeconds(1700000000).LocalDateTime", "Unix 타임스탬프 → 날짜", "date"),
// 파이프라인
new("Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 | Format-Table", "상위 10 프로세스 테이블 출력", "pipe"),
new("Get-ChildItem *.txt | ForEach-Object { $_.Name }", "txt 파일 이름만 추출", "pipe"),
new("1..10 | ForEach-Object { $_ * $_ }", "1~10 제곱수 생성", "pipe"),
new("'a','b','c' | Where-Object { $_ -ne 'b' }", "배열 필터링", "pipe"),
new("Get-Content log.txt | Select-String 'ERROR'", "파일에서 ERROR 줄 검색", "pipe"),
new("Get-EventLog -LogName System -Newest 50 | Where-Object {$_.EntryType -eq 'Error'}", "시스템 이벤트 오류 조회", "pipe"),
];
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("PowerShell 명령 생성기",
"ps file · process · network · service · registry · string · date · pipe · run <명령>",
null, null, Symbol: "\uE756"));
AddCategoryOverview(items);
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
// ps run <명령>
if (sub == "run" && parts.Length >= 2)
{
var cmd = parts[1];
items.Add(new LauncherItem(cmd, "Enter → PowerShell에서 실행",
null, ("run", cmd), Symbol: "\uE756"));
items.Add(new LauncherItem("클립보드 복사", cmd, null, ("copy", cmd), Symbol: "\uE756"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 카테고리 조회
var catMatch = Commands.Where(c => c.Category.Equals(sub, StringComparison.OrdinalIgnoreCase)).ToList();
if (catMatch.Count > 0)
{
var catName = sub switch
{
"file" => "파일/디렉토리",
"process" => "프로세스",
"service" => "서비스",
"network" => "네트워크",
"registry" => "레지스트리",
"string" => "문자열·데이터",
"date" => "날짜/시간",
"pipe" => "파이프라인 예시",
_ => sub
};
items.Add(new LauncherItem($"── {catName} ──", $"{catMatch.Count}개 명령", null, null, Symbol: "\uE756"));
foreach (var c in catMatch)
items.Add(new LauncherItem(TruncateStr(c.Cmd, 70), c.Description,
null, ("copy", c.Cmd), Symbol: "\uE756"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 키워드 검색
var keyword = string.Join(" ", parts);
var found = Commands.Where(c =>
c.Cmd.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
c.Description.Contains(keyword, StringComparison.OrdinalIgnoreCase)).ToList();
if (found.Count > 0)
{
items.Add(new LauncherItem($"'{keyword}' 검색 결과: {found.Count}개", "", null, null, Symbol: "\uE756"));
foreach (var c in found)
items.Add(new LauncherItem(TruncateStr(c.Cmd, 70), c.Description,
null, ("copy", c.Cmd), Symbol: "\uE756"));
}
else
{
items.Add(new LauncherItem($"'{keyword}' 결과 없음",
"ps file · process · network · service · registry · string · date · pipe",
null, null, Symbol: "\uE783"));
AddCategoryOverview(items);
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
switch (item.Data)
{
case ("copy", string text):
try
{
System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.SetText(text));
NotificationService.Notify("PS", "클립보드에 복사했습니다.");
}
catch { }
break;
case ("run", string cmd):
RunInPowerShell(cmd);
break;
}
return Task.CompletedTask;
}
// ── 헬퍼 ────────────────────────────────────────────────────────────────
private static void AddCategoryOverview(List<LauncherItem> items)
{
var cats = new (string Key, string Label)[]
{
("file", "파일/디렉토리 — Get-ChildItem · Copy-Item · Remove-Item · Compress-Archive"),
("process", "프로세스 — Get-Process · Stop-Process · Start-Process"),
("service", "서비스 — Get-Service · Start-Service · Stop-Service"),
("network", "네트워크 — Test-NetConnection · Get-NetIPAddress · Resolve-DnsName"),
("registry", "레지스트리 — Get-ItemProperty · New-ItemProperty · Remove-ItemProperty"),
("string", "문자열·데이터 — ConvertTo-Json · Get-FileHash · Import-Csv"),
("date", "날짜/시간 — Get-Date · AddDays · UnixTimeSeconds"),
("pipe", "파이프라인 — Sort-Object · Where-Object · ForEach-Object"),
};
foreach (var (key, label) in cats)
items.Add(new LauncherItem($"ps {key}", label, null, null, Symbol: "\uE756"));
}
private static void RunInPowerShell(string cmd)
{
try
{
// Windows Terminal 우선
var wtPath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
@"Microsoft\WindowsApps\wt.exe");
if (System.IO.File.Exists(wtPath))
{
Process.Start(new ProcessStartInfo
{
FileName = wtPath,
Arguments = $"PowerShell -NoExit -Command \"{cmd.Replace("\"", "\\\"")}\"",
UseShellExecute = true
});
return;
}
Process.Start(new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-NoExit -Command \"{cmd.Replace("\"", "\\\"")}\"",
UseShellExecute = true
});
}
catch { }
}
private static string TruncateStr(string s, int max) =>
s.Length <= max ? s : s[..max] + "…";
}

View File

@@ -0,0 +1,372 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L21-1: TOML 파서·분석기 핸들러. "toml" 프리픽스로 사용합니다.
///
/// 예: toml → 클립보드 TOML 전체 키 목록
/// toml validate → 유효성 검사
/// toml keys → 최상위 키 목록
/// toml get key → 특정 키 값 조회
/// toml get server.port → 점 표기법 중첩 키 조회
/// toml stats → 줄·키·섹션·배열 통계
/// toml flat → 점 표기법 평탄화 (모든 키·값)
/// toml sections → [section] 목록
/// Enter → 값 복사.
/// </summary>
public partial class TomlHandler : IActionHandler
{
public string? Prefix => "toml";
public PluginMetadata Metadata => new(
"TOML",
"TOML 파서·분석기 — 키 조회·유효성 검사·평탄화",
"1.0",
"AX");
// TOML 노드 (경량 표현)
private sealed class TomlTable : Dictionary<string, object?> { }
private sealed class TomlArray : List<object?> { }
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))
{
if (string.IsNullOrWhiteSpace(clipboard))
{
items.Add(new LauncherItem("TOML 파서·분석기",
"클립보드에 TOML을 복사하세요 · toml validate / keys / get / stats / flat",
null, null, Symbol: "\uE8EC"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 기본: 유효성 확인 + 최상위 키
var (tbl, err) = ParseToml(clipboard!);
if (err != null)
{
items.Add(ErrorItem($"TOML 파싱 오류: {err}"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem("TOML 파싱 성공 ✓",
$"최상위 키 {tbl!.Count}개 · toml get / flat / stats", null, null, Symbol: "\uE8EC"));
BuildTopKeys(items, tbl!);
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
var src = parts.Length > 1 && (sub != "get")
? string.Join(" ", parts[1..])
: clipboard ?? "";
// validate
if (sub is "validate" or "check" or "검사")
{
var text = clipboard ?? "";
var (_, verr) = ParseToml(text);
if (verr != null)
items.Add(ErrorItem($"유효성 오류: {verr}"));
else
{
var (vt, _) = ParseToml(text);
items.Add(new LauncherItem("✓ 유효한 TOML", $"최상위 키 {vt!.Count}개", null, null, Symbol: "\uE8EC"));
BuildTopKeys(items, vt!);
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var (table, parseErr) = ParseToml(clipboard ?? "");
if (parseErr != null && sub != "validate")
{
items.Add(ErrorItem($"TOML 파싱 오류: {parseErr}"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
switch (sub)
{
case "keys" or "key":
items.Add(new LauncherItem("최상위 키 목록", $"{table!.Count}개", null, null, Symbol: "\uE8EC"));
BuildTopKeys(items, table!);
break;
case "sections" or "section":
{
var secs = table!.Where(kv => kv.Value is TomlTable).Select(kv => kv.Key).ToList();
items.Add(new LauncherItem($"섹션 {secs.Count}개", "", null, null, Symbol: "\uE8EC"));
foreach (var s in secs)
{
var secCount = table![s] is TomlTable st ? st.Count : 0;
items.Add(new LauncherItem($"[{s}]",
$"{secCount}개 키", null, ("copy", s), Symbol: "\uE8EC"));
}
break;
}
case "get":
{
if (parts.Length < 2) { items.Add(ErrorItem("예: toml get server.port")); break; }
var keyPath = parts[1];
var val = GetByPath(table!, keyPath);
if (val == null)
items.Add(new LauncherItem($"'{keyPath}' 키를 찾을 수 없습니다", "", null, null, Symbol: "\uE8EC"));
else
{
var strVal = TomlValueToString(val);
items.Add(new LauncherItem($"{keyPath} = {TruncateStr(strVal, 60)}",
"Enter 복사", null, ("copy", strVal), Symbol: "\uE8EC"));
items.Add(CopyItem("값", strVal));
items.Add(CopyItem("키 경로", keyPath));
items.Add(CopyItem("타입", GetTomlType(val)));
}
break;
}
case "stats":
{
var flat = new Dictionary<string, object?>();
FlattenTable(table!, "", flat);
var lines = (clipboard ?? "").Split('\n').Length;
var arrays = flat.Values.Count(v => v is TomlArray);
var tables2 = flat.Values.Count(v => v is TomlTable);
var scalars = flat.Count - arrays - tables2;
items.Add(new LauncherItem("TOML 통계", "", null, null, Symbol: "\uE8EC"));
items.Add(CopyItem("전체 줄", lines.ToString()));
items.Add(CopyItem("최상위 키", table!.Count.ToString()));
items.Add(CopyItem("전체 키(flat)", flat.Count.ToString()));
items.Add(CopyItem("스칼라 값", scalars.ToString()));
items.Add(CopyItem("배열", arrays.ToString()));
items.Add(CopyItem("섹션(테이블)", table.Values.Count(v => v is TomlTable).ToString()));
break;
}
case "flat" or "flatten":
{
var flat = new Dictionary<string, object?>();
FlattenTable(table!, "", flat);
var all = string.Join("\n", flat.Select(kv => $"{kv.Key} = {TomlValueToString(kv.Value)}"));
items.Add(new LauncherItem($"평탄화 결과 {flat.Count}개",
"전체 복사 → Enter", null, ("copy", all), Symbol: "\uE8EC"));
foreach (var (k, v) in flat.Take(20))
items.Add(new LauncherItem($"{k} = {TruncateStr(TomlValueToString(v), 50)}",
GetTomlType(v), null, ("copy", TomlValueToString(v)), Symbol: "\uE8EC"));
if (flat.Count > 20)
items.Add(new LauncherItem($"... ({flat.Count - 20}개 더)", "전체는 Enter로 복사",
null, null, Symbol: "\uE8EC"));
break;
}
default:
items.Add(new LauncherItem($"알 수 없는 서브커맨드: '{sub}'",
"validate · keys · sections · get <key> · stats · flat",
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("TOML", "클립보드에 복사했습니다.");
}
catch { }
}
return Task.CompletedTask;
}
// ── 경량 TOML 파서 ────────────────────────────────────────────────────
private static (TomlTable? table, string? error) ParseToml(string src)
{
var root = new TomlTable();
var current = root;
var lines = src.Split('\n');
string? parseError = null;
for (int lineNo = 0; lineNo < lines.Length; lineNo++)
{
var raw = lines[lineNo];
var line = StripComment(raw).Trim();
if (string.IsNullOrWhiteSpace(line)) continue;
// [section] 또는 [[array-of-tables]]
if (line.StartsWith("[["))
{
var name = line.Trim('[', ']').Trim();
// 배열 섹션 처리 (간략화: 마지막 테이블만 유지)
var parts = name.Split('.');
current = root;
foreach (var p in parts)
{
if (!current.TryGetValue(p, out var existing) || existing is not TomlTable child)
{ child = new TomlTable(); current[p] = child; }
current = (TomlTable)current[p]!;
}
}
else if (line.StartsWith("["))
{
var name = line.Trim('[', ']').Trim();
var parts = name.Split('.');
current = root;
foreach (var p in parts)
{
if (!current.TryGetValue(p, out var existing) || existing is not TomlTable child)
{ child = new TomlTable(); current[p] = child; }
current = (TomlTable)current[p]!;
}
}
// key = value
else if (line.Contains('='))
{
var eqIdx = line.IndexOf('=');
var key = line[..eqIdx].Trim().Trim('"');
var val = line[(eqIdx + 1)..].Trim();
try { current[key] = ParseTomlValue(val); }
catch (Exception ex)
{ parseError ??= $"줄 {lineNo + 1}: {ex.Message}"; }
}
}
return parseError != null ? (null, parseError) : (root, null);
}
private static object? ParseTomlValue(string val)
{
if (val.StartsWith('"') || val.StartsWith('\''))
return val.Trim('"', '\'');
if (val.StartsWith('['))
{
var arr = new TomlArray();
var inner = val.Trim('[', ']');
foreach (var item in inner.Split(','))
{
var t = item.Trim();
if (!string.IsNullOrEmpty(t)) arr.Add(ParseTomlValue(t));
}
return arr;
}
if (val.StartsWith('{'))
{
var tbl = new TomlTable();
var inner = val.Trim('{', '}');
foreach (var pair in inner.Split(','))
{
var parts = pair.Split('=', 2);
if (parts.Length == 2)
tbl[parts[0].Trim().Trim('"')] = ParseTomlValue(parts[1].Trim());
}
return tbl;
}
if (val is "true") return true;
if (val is "false") return false;
if (long.TryParse(val, out var lv)) return lv;
if (double.TryParse(val, System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var dv)) return dv;
return val.Trim('"', '\'');
}
private static string StripComment(string line)
{
// '#' 앞에 따옴표가 홀수개인 경우 내부 → 유지, 아니면 제거
bool inStr = false;
char quote = '"';
for (int i = 0; i < line.Length; i++)
{
var c = line[i];
if (!inStr && (c == '"' || c == '\'')) { inStr = true; quote = c; }
else if (inStr && c == quote) inStr = false;
else if (!inStr && c == '#') return line[..i];
}
return line;
}
private static object? GetByPath(TomlTable table, string path)
{
var parts = path.Split('.');
object? cur = table;
foreach (var p in parts)
{
if (cur is TomlTable t && t.TryGetValue(p, out var next)) cur = next;
else return null;
}
return cur;
}
private static void FlattenTable(TomlTable table, string prefix, Dictionary<string, object?> result)
{
foreach (var (k, v) in table)
{
var key = string.IsNullOrEmpty(prefix) ? k : $"{prefix}.{k}";
if (v is TomlTable child) FlattenTable(child, key, result);
else result[key] = v;
}
}
private static void BuildTopKeys(List<LauncherItem> items, TomlTable table)
{
foreach (var (k, v) in table)
{
var type = GetTomlType(v);
var disp = v is TomlTable t ? $"{{ {t.Count}개 키 }}" :
v is TomlArray a ? $"[ {a.Count}개 항목 ]" :
TruncateStr(TomlValueToString(v), 50);
items.Add(new LauncherItem($"{k} = {disp}", type,
null, ("copy", TomlValueToString(v)), Symbol: "\uE8EC"));
}
}
private static string TomlValueToString(object? v) => v switch
{
null => "null",
bool b => b ? "true" : "false",
TomlTable t => "{" + string.Join(", ", t.Select(kv => $"{kv.Key} = {TomlValueToString(kv.Value)}")) + "}",
TomlArray a => "[" + string.Join(", ", a.Select(TomlValueToString)) + "]",
_ => v.ToString() ?? ""
};
private static string GetTomlType(object? v) => v switch
{
null => "null",
bool => "Boolean",
long => "Integer",
double => "Float",
string => "String",
TomlTable => "Table",
TomlArray => "Array",
_ => v.GetType().Name
};
private static string TruncateStr(string s, int max) =>
s.Length <= max ? s : s[..max] + "…";
private static LauncherItem CopyItem(string label, string value) =>
new(label, value, null, ("copy", value), Symbol: "\uE8EC");
private static LauncherItem ErrorItem(string msg) =>
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
}