[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:
@@ -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 모드 | 낮음 |
|
||||
|
||||
@@ -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);
|
||||
|
||||
289
src/AxCopilot/Handlers/KeyHandler.cs
Normal file
289
src/AxCopilot/Handlers/KeyHandler.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
402
src/AxCopilot/Handlers/LogHandler.cs
Normal file
402
src/AxCopilot/Handlers/LogHandler.cs
Normal 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();
|
||||
}
|
||||
268
src/AxCopilot/Handlers/PsHandler.cs
Normal file
268
src/AxCopilot/Handlers/PsHandler.cs
Normal 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] + "…";
|
||||
}
|
||||
372
src/AxCopilot/Handlers/TomlHandler.cs
Normal file
372
src/AxCopilot/Handlers/TomlHandler.cs
Normal 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");
|
||||
}
|
||||
Reference in New Issue
Block a user