[Phase L12] 시스템·네트워크·텍스트 도구 핸들러 4종 추가
HttpTesterHandler.cs (신규, ~170줄, prefix=http): - GET/HEAD/POST 등 HttpClient 기반 직접 요청 - http:// 스키마 자동 추가, 3회 리다이렉트, 10초 타임아웃 - 상태 코드·응답 시간·Content-Type·Server 등 주요 헤더 표시 - InternalModeEnabled: 외부 URL 차단, 내부 IP 허용 패턴 정규식 - Enter → 비동기 요청 실행 + 결과 클립보드 복사 HostsHandler.cs (신규, ~220줄, prefix=hosts): - System32\drivers\etc\hosts RFC 파서 (인라인 주석 처리) - 활성 항목 / 주석 처리된 IP 항목(비활성) 자동 분류 - search 키워드 필터, open 메모장 실행, copy 전체 내용 - IPAddress.TryParse 기반 유효 IP 항목 판별 MorseHandler.cs (신규, ~200줄, prefix=morse): - 56자(영문자·숫자·구두점) TextToMorse 정적 딕셔너리 - 역방향 MorseToText 딕셔너리 static 생성자로 자동 구축 - .-/공백 패턴으로 모스 입력 자동 감지 - / 단어 구분자, SOS/AR/AS/BT/KN/SK 프로사인 키워드 지원 - 클립보드 자동 감지 (비어 있으면 도움말 표시) StartupHandler.cs (신규, ~220줄, prefix=startup): - HKCU + HKLM Run/RunOnce 6개 레지스트리 키 조회 - 시작 폴더(.lnk) 현재 사용자 + 모든 사용자 통합 수집 - 범위별(현재 사용자/모든 사용자) 그룹화 표시 - search 키워드 필터, folder 폴더 열기 서브커맨드 App.xaml.cs: 4개 핸들러 Phase L12 블록 등록 docs/LAUNCHER_ROADMAP.md: Phase L12 완료 섹션 추가 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -282,3 +282,16 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
|
|||||||
| L11-2 | **JWT 디코더** ✅ | `jwt` 프리픽스. 클립보드 또는 인라인 토큰 자동 감지(eyJ 시작). 헤더(alg·typ)·페이로드(claims)·서명 유무 분석. exp/iat/nbf 타임스탬프 → 날짜 변환. 만료 D-day·남은 시간 계산. `jwt header` / `jwt payload` 부분 조회. **서명 검증 미지원(분석 전용)** | 높음 |
|
| L11-2 | **JWT 디코더** ✅ | `jwt` 프리픽스. 클립보드 또는 인라인 토큰 자동 감지(eyJ 시작). 헤더(alg·typ)·페이로드(claims)·서명 유무 분석. exp/iat/nbf 타임스탬프 → 날짜 변환. 만료 D-day·남은 시간 계산. `jwt header` / `jwt payload` 부분 조회. **서명 검증 미지원(분석 전용)** | 높음 |
|
||||||
| L11-3 | **Cron 설명기** ✅ | `cron` 프리픽스. 5필드 표준 cron 표현식 파싱. 한국어 설명 생성(예: "평일 오전 9시 실행"). 다음 5회 실행 시간 계산 + 상대 시간 표시. `@daily/@weekly/@monthly/@hourly` 특수 키워드. 필드별 분석(분·시·일·월·요일). Enter → 표현식 복사 | 중간 |
|
| L11-3 | **Cron 설명기** ✅ | `cron` 프리픽스. 5필드 표준 cron 표현식 파싱. 한국어 설명 생성(예: "평일 오전 9시 실행"). 다음 5회 실행 시간 계산 + 상대 시간 표시. `@daily/@weekly/@monthly/@hourly` 특수 키워드. 필드별 분석(분·시·일·월·요일). Enter → 표현식 복사 | 중간 |
|
||||||
| L11-4 | **유니코드 조회** ✅ | `unicode` 프리픽스. 문자 직접 입력, `U+XXXX`, `0xXXXX`, 10진수 코드포인트 방식 지원. UTF-8·UTF-16 바이트, HTML 엔티티, 카테고리(Lu/Ll/So 등), 블록명 표시. 한글 음절 초·중·종성 분해. 여러 문자 입력 시 코드포인트 범위 요약 | 중간 |
|
| L11-4 | **유니코드 조회** ✅ | `unicode` 프리픽스. 문자 직접 입력, `U+XXXX`, `0xXXXX`, 10진수 코드포인트 방식 지원. UTF-8·UTF-16 바이트, HTML 엔티티, 카테고리(Lu/Ll/So 등), 블록명 표시. 한글 음절 초·중·종성 분해. 여러 문자 입력 시 코드포인트 범위 요약 | 중간 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase L12 — 시스템·네트워크·텍스트 도구 (v2.0.4) ✅ 완료
|
||||||
|
|
||||||
|
> **방향**: 실무 네트워크 진단·시스템 관리·재미있는 텍스트 변환 확충.
|
||||||
|
|
||||||
|
| # | 기능 | 설명 | 우선순위 |
|
||||||
|
|---|------|------|----------|
|
||||||
|
| L12-1 | **HTTP 요청 테스터** ✅ | `http` 프리픽스. GET/HEAD/POST/PUT/DELETE 메서드. http:// 자동 추가. 상태 코드·응답 시간·Content-Type·주요 헤더 표시. 사내 모드에서 외부 URL 차단(내부 IP만 허용). Enter → 요청 실행 + 결과 클립보드 복사 | 높음 |
|
||||||
|
| L12-2 | **hosts 파일 관리** ✅ | `hosts` 프리픽스. C:\Windows\System32\drivers\etc\hosts 파싱. 활성·비활성(주석 처리) 항목 분류. `hosts search` 키워드 필터. `hosts open` 메모장 열기. `hosts copy` 전체 내용 복사. 항목 Enter → 클립보드 복사 | 중간 |
|
||||||
|
| L12-3 | **모스 부호 변환기** ✅ | `morse` 프리픽스. 텍스트 → 모스 부호 (영문자·숫자·구두점 56자 지원). 모스 → 텍스트 역변환 (.-/공백 자동 감지). SOS/AR/AS 프로사인 키워드. 클립보드 자동 감지. 문자별·코드별 대응표 표시 | 낮음 |
|
||||||
|
| L12-4 | **시작 프로그램 조회** ✅ | `startup` 프리픽스. HKCU/HKLM Run·RunOnce 레지스트리 + 시작 폴더(.lnk) 통합 조회. 범위(현재 사용자/모든 사용자) 그룹화. `startup search` 키워드 필터. `startup folder` 시작 폴더 열기. Enter → 명령 경로 클립보드 복사 | 중간 |
|
||||||
|
|||||||
@@ -247,6 +247,16 @@ public partial class App : System.Windows.Application
|
|||||||
// L11-4: 유니코드 문자 조회 (prefix=unicode)
|
// L11-4: 유니코드 문자 조회 (prefix=unicode)
|
||||||
commandResolver.RegisterHandler(new UnicodeHandler());
|
commandResolver.RegisterHandler(new UnicodeHandler());
|
||||||
|
|
||||||
|
// ─── Phase L12 핸들러 ─────────────────────────────────────────────────
|
||||||
|
// L12-1: HTTP 요청 테스터 (prefix=http)
|
||||||
|
commandResolver.RegisterHandler(new HttpTesterHandler());
|
||||||
|
// L12-2: hosts 파일 관리 (prefix=hosts)
|
||||||
|
commandResolver.RegisterHandler(new HostsHandler());
|
||||||
|
// L12-3: 모스 부호 변환기 (prefix=morse)
|
||||||
|
commandResolver.RegisterHandler(new MorseHandler());
|
||||||
|
// L12-4: 시작 프로그램 조회 (prefix=startup)
|
||||||
|
commandResolver.RegisterHandler(new StartupHandler());
|
||||||
|
|
||||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||||
var pluginHost = new PluginHost(settings, commandResolver);
|
var pluginHost = new PluginHost(settings, commandResolver);
|
||||||
pluginHost.LoadAll();
|
pluginHost.LoadAll();
|
||||||
|
|||||||
253
src/AxCopilot/Handlers/HostsHandler.cs
Normal file
253
src/AxCopilot/Handlers/HostsHandler.cs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Themes;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L12-2: Windows hosts 파일 관리 핸들러. "hosts" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 예: hosts → 현재 hosts 파일 항목 목록
|
||||||
|
/// hosts search dev → "dev" 포함 항목 필터
|
||||||
|
/// hosts open → hosts 파일을 메모장으로 열기
|
||||||
|
/// hosts copy → 전체 hosts 내용 클립보드 복사
|
||||||
|
/// Enter → 해당 항목을 클립보드에 복사.
|
||||||
|
/// </summary>
|
||||||
|
public class HostsHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "hosts";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"Hosts",
|
||||||
|
"Windows hosts 파일 뷰어 — 항목 조회 · 검색 · 복사",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
private static readonly string HostsPath =
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System),
|
||||||
|
@"drivers\etc\hosts");
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim();
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
|
||||||
|
var entries = ReadHostsEntries();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(q))
|
||||||
|
{
|
||||||
|
var activeCount = entries.Count(e => !e.IsComment);
|
||||||
|
var commentCount = entries.Count(e => e.IsComment && e.IsDisabledEntry);
|
||||||
|
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
$"hosts 파일 활성 {activeCount}개" + (commentCount > 0 ? $" · 비활성 {commentCount}개" : ""),
|
||||||
|
HostsPath,
|
||||||
|
null,
|
||||||
|
("copy_path", HostsPath),
|
||||||
|
Symbol: "\uE8D2"));
|
||||||
|
|
||||||
|
items.Add(new LauncherItem("파일 열기 (메모장)", HostsPath, null, ("open", HostsPath), Symbol: "\uE8A5"));
|
||||||
|
items.Add(new LauncherItem("전체 내용 복사", $"{entries.Count}줄", null, ("copy_all", ""), Symbol: "\uE8A5"));
|
||||||
|
|
||||||
|
// 유효 항목 목록
|
||||||
|
foreach (var e in entries.Where(e => !e.IsComment).Take(20))
|
||||||
|
items.Add(MakeEntryItem(e));
|
||||||
|
|
||||||
|
// 비활성 항목 (주석 처리된 IP 항목)
|
||||||
|
var disabled = entries.Where(e => e.IsDisabledEntry).Take(5).ToList();
|
||||||
|
if (disabled.Count > 0)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"── 비활성 항목 {disabled.Count}개 ──", "", null, null, Symbol: "\uE8D2"));
|
||||||
|
foreach (var e in disabled)
|
||||||
|
items.Add(MakeEntryItem(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var sub = parts[0].ToLowerInvariant();
|
||||||
|
|
||||||
|
switch (sub)
|
||||||
|
{
|
||||||
|
case "open":
|
||||||
|
items.Add(new LauncherItem("메모장으로 열기", HostsPath, null, ("open", HostsPath), Symbol: "\uE8A5"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "copy":
|
||||||
|
case "copy_all":
|
||||||
|
{
|
||||||
|
var content = ReadHostsRaw();
|
||||||
|
items.Add(new LauncherItem("전체 내용 복사", $"{content.Split('\n').Length}줄 · Enter 복사",
|
||||||
|
null, ("copy", content), Symbol: "\uE8A5"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "search":
|
||||||
|
case "find":
|
||||||
|
{
|
||||||
|
var keyword = parts.Length > 1 ? parts[1].ToLowerInvariant() : "";
|
||||||
|
if (string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("검색어 입력", "예: hosts search dev", null, null, Symbol: "\uE783"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var filtered = entries.Where(e =>
|
||||||
|
e.Hostname.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
e.IpAddress.Contains(keyword, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
|
||||||
|
if (filtered.Count == 0)
|
||||||
|
items.Add(new LauncherItem("결과 없음", $"'{keyword}' 항목 없음", null, null, Symbol: "\uE946"));
|
||||||
|
else
|
||||||
|
foreach (var e in filtered.Take(20))
|
||||||
|
items.Add(MakeEntryItem(e));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
// 검색어로 처리
|
||||||
|
var keyword = q.ToLowerInvariant();
|
||||||
|
var filtered = entries.Where(e =>
|
||||||
|
e.Hostname.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
e.IpAddress.Contains(keyword, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
|
||||||
|
if (filtered.Count == 0)
|
||||||
|
items.Add(new LauncherItem("결과 없음", $"'{q}' 항목 없음", null, null, Symbol: "\uE946"));
|
||||||
|
else
|
||||||
|
foreach (var e in filtered.Take(20))
|
||||||
|
items.Add(MakeEntryItem(e));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Hosts", "클립보드에 복사했습니다.");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ("copy_path", string path):
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(path));
|
||||||
|
NotificationService.Notify("Hosts", "경로가 복사되었습니다.");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ("copy_all", _):
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = ReadHostsRaw();
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(content));
|
||||||
|
NotificationService.Notify("Hosts", $"hosts 파일 내용 복사됨 ({content.Split('\n').Length}줄)");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ("open", string path):
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "notepad.exe",
|
||||||
|
Arguments = $"\"{path}\"",
|
||||||
|
UseShellExecute = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
NotificationService.Notify("Hosts", $"열기 실패: {ex.Message}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── hosts 파일 파서 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private record HostsEntry(string IpAddress, string Hostname, string RawLine,
|
||||||
|
bool IsComment, bool IsDisabledEntry);
|
||||||
|
|
||||||
|
private static List<HostsEntry> ReadHostsEntries()
|
||||||
|
{
|
||||||
|
var result = new List<HostsEntry>();
|
||||||
|
string[] lines;
|
||||||
|
try { lines = File.ReadAllLines(HostsPath, Encoding.UTF8); }
|
||||||
|
catch { return result; }
|
||||||
|
|
||||||
|
foreach (var rawLine in lines)
|
||||||
|
{
|
||||||
|
var line = rawLine.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||||
|
|
||||||
|
// 순수 주석 (# 으로 시작, IP 없음)
|
||||||
|
if (line.StartsWith('#'))
|
||||||
|
{
|
||||||
|
// 비활성 IP 항목인지 확인 (예: "# 127.0.0.1 example.com")
|
||||||
|
var inner = line[1..].Trim();
|
||||||
|
if (TryParseIpEntry(inner, out var disIp, out var disHost))
|
||||||
|
{
|
||||||
|
result.Add(new HostsEntry(disIp, disHost, rawLine, true, true));
|
||||||
|
}
|
||||||
|
// 순수 주석은 목록에서 제외
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP 항목 (인라인 주석 포함 가능)
|
||||||
|
var withoutComment = line.Contains('#') ? line[..line.IndexOf('#')].Trim() : line;
|
||||||
|
if (TryParseIpEntry(withoutComment, out var ip, out var host))
|
||||||
|
{
|
||||||
|
result.Add(new HostsEntry(ip, host, rawLine, false, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseIpEntry(string line, out string ip, out string host)
|
||||||
|
{
|
||||||
|
ip = host = "";
|
||||||
|
var parts = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Length < 2) return false;
|
||||||
|
|
||||||
|
// 첫 토큰이 IP 주소 형태인지 간단히 확인
|
||||||
|
if (!System.Net.IPAddress.TryParse(parts[0], out _)) return false;
|
||||||
|
ip = parts[0];
|
||||||
|
host = parts[1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadHostsRaw()
|
||||||
|
{
|
||||||
|
try { return File.ReadAllText(HostsPath, Encoding.UTF8); }
|
||||||
|
catch { return "(hosts 파일을 읽을 수 없습니다)"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LauncherItem MakeEntryItem(HostsEntry e)
|
||||||
|
{
|
||||||
|
var prefix = e.IsComment ? "# " : "";
|
||||||
|
var icon = e.IsComment ? "\uE946" : "\uE8D2";
|
||||||
|
var subtitle = e.IsComment ? "비활성 항목" : e.IpAddress;
|
||||||
|
|
||||||
|
return new LauncherItem(
|
||||||
|
$"{prefix}{e.Hostname}",
|
||||||
|
subtitle,
|
||||||
|
null,
|
||||||
|
("copy", $"{e.IpAddress}\t{e.Hostname}"),
|
||||||
|
Symbol: icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
191
src/AxCopilot/Handlers/HttpTesterHandler.cs
Normal file
191
src/AxCopilot/Handlers/HttpTesterHandler.cs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Themes;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L12-1: HTTP 요청 테스터 핸들러. "http" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 예: http example.com → GET 요청 (http:// 자동 추가)
|
||||||
|
/// http https://api.example → GET 요청 + 응답 코드·시간
|
||||||
|
/// http head https://example → HEAD 요청
|
||||||
|
/// http post https://example → POST (빈 바디)
|
||||||
|
/// http 192.168.1.1 → 내부 IP GET
|
||||||
|
/// Enter → 응답 요약을 클립보드에 복사.
|
||||||
|
///
|
||||||
|
/// ⚠ 외부 URL: 사내 모드 차단. 내부 IP(10./192.168./172.16-31.)는 허용.
|
||||||
|
/// </summary>
|
||||||
|
public class HttpTesterHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "http";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"HTTP",
|
||||||
|
"HTTP 요청 테스터 — GET · HEAD · POST · 응답 코드",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
private static readonly HttpClient _client = new(new HttpClientHandler
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = true,
|
||||||
|
MaxAutomaticRedirections = 3,
|
||||||
|
ServerCertificateCustomValidationCallback = (_, _, _, _) => true,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromSeconds(10),
|
||||||
|
};
|
||||||
|
|
||||||
|
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("HTTP 요청 테스터",
|
||||||
|
"예: http example.com / http head https://api / http post https://url",
|
||||||
|
null, null, Symbol: "\uE774"));
|
||||||
|
items.Add(new LauncherItem("http localhost", "로컬 서버 GET", null, null, Symbol: "\uE774"));
|
||||||
|
items.Add(new LauncherItem("http 192.168.1.1", "내부 IP GET", null, null, Symbol: "\uE774"));
|
||||||
|
items.Add(new LauncherItem("http head https://…", "HEAD 요청", null, null, Symbol: "\uE774"));
|
||||||
|
items.Add(new LauncherItem("http post https://…", "POST 요청", null, null, Symbol: "\uE774"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
string method, url;
|
||||||
|
|
||||||
|
if (parts[0].ToUpperInvariant() is "GET" or "HEAD" or "POST" or "PUT" or "DELETE" or "OPTIONS")
|
||||||
|
{
|
||||||
|
method = parts[0].ToUpperInvariant();
|
||||||
|
url = parts.Length > 1 ? parts[1].Trim() : "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
method = "GET";
|
||||||
|
url = q;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("URL을 입력하세요", $"예: http {method.ToLower()} https://example.com", null, null, Symbol: "\uE783"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스키마 자동 추가
|
||||||
|
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
url = "http://" + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사내 모드 확인
|
||||||
|
var settings = (System.Windows.Application.Current as App)?.SettingsService?.Settings;
|
||||||
|
var isInternal = settings?.InternalModeEnabled ?? true;
|
||||||
|
if (isInternal && !IsInternalUrl(url))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
"사내 모드 제한",
|
||||||
|
$"외부 URL '{url}'은 차단됩니다. 설정에서 사외 모드를 활성화하세요.",
|
||||||
|
null, null, Symbol: "\uE783"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
$"{method} {TruncateUrl(url)}",
|
||||||
|
"Enter를 눌러 요청 실행",
|
||||||
|
null,
|
||||||
|
("request", $"{method}|{url}"),
|
||||||
|
Symbol: "\uE774"));
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async 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("HTTP", "클립보드에 복사했습니다.");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Data is not ("request", string reqData)) return;
|
||||||
|
|
||||||
|
var idx = reqData.IndexOf('|');
|
||||||
|
var method = reqData[..idx];
|
||||||
|
var url = reqData[(idx + 1)..];
|
||||||
|
|
||||||
|
NotificationService.Notify("HTTP", $"{method} {TruncateUrl(url)} 요청 중…");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
HttpResponseMessage resp;
|
||||||
|
|
||||||
|
using var reqMsg = new HttpRequestMessage(new HttpMethod(method), url);
|
||||||
|
reqMsg.Headers.TryAddWithoutValidation("User-Agent", "AX-Copilot/2.0 HTTP-Tester");
|
||||||
|
|
||||||
|
resp = await _client.SendAsync(reqMsg, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||||
|
sw.Stop();
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"URL: {url}");
|
||||||
|
sb.AppendLine($"메서드: {method}");
|
||||||
|
sb.AppendLine($"상태 코드: {(int)resp.StatusCode} {resp.ReasonPhrase}");
|
||||||
|
sb.AppendLine($"응답 시간: {sw.ElapsedMilliseconds}ms");
|
||||||
|
sb.AppendLine($"Content-Type: {resp.Content.Headers.ContentType}");
|
||||||
|
sb.AppendLine($"Content-Length: {resp.Content.Headers.ContentLength?.ToString() ?? "unknown"}");
|
||||||
|
|
||||||
|
// 주요 헤더
|
||||||
|
foreach (var h in new[] { "Server", "X-Powered-By", "Cache-Control", "ETag", "Last-Modified" })
|
||||||
|
if (resp.Headers.TryGetValues(h, out var vals))
|
||||||
|
sb.AppendLine($"{h}: {string.Join(", ", vals)}");
|
||||||
|
|
||||||
|
var summary = sb.ToString().TrimEnd();
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(summary));
|
||||||
|
|
||||||
|
var status = (int)resp.StatusCode;
|
||||||
|
var emoji = status < 300 ? "✔" : status < 400 ? "↪" : "✘";
|
||||||
|
NotificationService.Notify("HTTP",
|
||||||
|
$"{emoji} {status} {resp.ReasonPhrase} ({sw.ElapsedMilliseconds}ms) · 결과 복사됨");
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
NotificationService.Notify("HTTP", "요청 타임아웃 (10초 초과)");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
NotificationService.Notify("HTTP", $"요청 오류: {ex.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
NotificationService.Notify("HTTP", $"오류: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static bool IsInternalUrl(string url)
|
||||||
|
{
|
||||||
|
var lower = url.ToLowerInvariant();
|
||||||
|
return lower.Contains("://localhost") ||
|
||||||
|
lower.Contains("://127.0.0.1") ||
|
||||||
|
lower.Contains("://192.168.") ||
|
||||||
|
lower.Contains("://10.") ||
|
||||||
|
System.Text.RegularExpressions.Regex.IsMatch(lower,
|
||||||
|
@"://172\.(1[6-9]|2\d|3[01])\.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TruncateUrl(string url) =>
|
||||||
|
url.Length > 60 ? url[..60] + "…" : url;
|
||||||
|
}
|
||||||
251
src/AxCopilot/Handlers/MorseHandler.cs
Normal file
251
src/AxCopilot/Handlers/MorseHandler.cs
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Themes;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L12-3: 모스 부호 변환기 핸들러. "morse" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 예: morse hello → 텍스트 → 모스 부호
|
||||||
|
/// morse .- -... -.-. → 모스 부호 → 텍스트 (공백으로 구분)
|
||||||
|
/// morse SOS → SOS 모스 부호
|
||||||
|
/// morse → 클립보드 자동 감지·변환
|
||||||
|
/// Enter → 결과를 클립보드에 복사.
|
||||||
|
/// </summary>
|
||||||
|
public class MorseHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "morse";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"Morse",
|
||||||
|
"모스 부호 변환기 — 텍스트 ↔ 모스 부호",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
// ── 모스 부호 사전 ────────────────────────────────────────────────────────
|
||||||
|
private static readonly Dictionary<char, string> TextToMorse = new()
|
||||||
|
{
|
||||||
|
['A'] = ".-", ['B'] = "-...", ['C'] = "-.-.", ['D'] = "-..",
|
||||||
|
['E'] = ".", ['F'] = "..-.", ['G'] = "--.", ['H'] = "....",
|
||||||
|
['I'] = "..", ['J'] = ".---", ['K'] = "-.-", ['L'] = ".-..",
|
||||||
|
['M'] = "--", ['N'] = "-.", ['O'] = "---", ['P'] = ".--.",
|
||||||
|
['Q'] = "--.-", ['R'] = ".-.", ['S'] = "...", ['T'] = "-",
|
||||||
|
['U'] = "..-", ['V'] = "...-", ['W'] = ".--", ['X'] = "-..-",
|
||||||
|
['Y'] = "-.--", ['Z'] = "--..",
|
||||||
|
['0'] = "-----", ['1'] = ".----", ['2'] = "..---", ['3'] = "...--",
|
||||||
|
['4'] = "....-", ['5'] = ".....", ['6'] = "-....", ['7'] = "--...",
|
||||||
|
['8'] = "---..", ['9'] = "----.",
|
||||||
|
['.'] = ".-.-.-", [','] = "--..--", ['?'] = "..--..", ['!'] = "-.-.--",
|
||||||
|
['/'] = "-..-.", ['-'] = "-....-", ['('] = "-.--.", [')'] = "-.--.-",
|
||||||
|
['@'] = ".--.-.", ['='] = "-...-", ['+'] = ".-.-.", [':'] = "---...",
|
||||||
|
[';'] = "-.-.-.", ['"'] = ".-..-.", ['\''] = ".----.", ['_'] = "..--.-",
|
||||||
|
[' '] = "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, char> MorseToText;
|
||||||
|
|
||||||
|
// 번개 부호 / 대문자 표
|
||||||
|
private static readonly string[] ProsignCodes = ["SOS", "AR", "AS", "BT", "KN", "SK"];
|
||||||
|
private static readonly Dictionary<string, string> ProsignMorse = new()
|
||||||
|
{
|
||||||
|
["SOS"] = "... --- ...",
|
||||||
|
["AR"] = ".-.-.",
|
||||||
|
["AS"] = ".-...",
|
||||||
|
["BT"] = "-...-",
|
||||||
|
["KN"] = "-.--.",
|
||||||
|
["SK"] = "...-.-",
|
||||||
|
};
|
||||||
|
|
||||||
|
static MorseHandler()
|
||||||
|
{
|
||||||
|
MorseToText = TextToMorse
|
||||||
|
.Where(kv => kv.Key != ' ')
|
||||||
|
.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim();
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(q))
|
||||||
|
{
|
||||||
|
// 클립보드 자동 감지
|
||||||
|
var clip = GetClipboard();
|
||||||
|
if (!string.IsNullOrWhiteSpace(clip))
|
||||||
|
{
|
||||||
|
if (IsMorseCode(clip))
|
||||||
|
items.AddRange(BuildMorseToText(clip));
|
||||||
|
else if (clip.Length <= 100)
|
||||||
|
items.AddRange(BuildTextToMorse(clip));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.Count == 0)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("모스 부호 변환기",
|
||||||
|
"예: morse hello / morse .- -... -.-.",
|
||||||
|
null, null, Symbol: "\uE8C4"));
|
||||||
|
items.Add(new LauncherItem("morse SOS", "SOS 모스 부호", null, null, Symbol: "\uE8C4"));
|
||||||
|
items.Add(new LauncherItem("morse hello", "텍스트 → 모스", null, null, Symbol: "\uE8C4"));
|
||||||
|
items.Add(new LauncherItem("morse .- -...", "모스 → 텍스트", null, null, Symbol: "\uE8C4"));
|
||||||
|
}
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로사인 키워드 우선
|
||||||
|
if (ProsignMorse.TryGetValue(q.ToUpperInvariant(), out var psCode))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
$"{q.ToUpper()} = {psCode}",
|
||||||
|
"모스 부호 프로사인 · Enter 복사",
|
||||||
|
null, ("copy", psCode), Symbol: "\uE8C4"));
|
||||||
|
items.AddRange(BuildTextToMorse(q.ToUpper()));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모스 부호 입력 감지
|
||||||
|
if (IsMorseCode(q))
|
||||||
|
{
|
||||||
|
items.AddRange(BuildMorseToText(q));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
items.AddRange(BuildTextToMorse(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Morse", "클립보드에 복사했습니다.");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 변환 로직 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static IEnumerable<LauncherItem> BuildTextToMorse(string text)
|
||||||
|
{
|
||||||
|
var upper = text.ToUpperInvariant();
|
||||||
|
var words = upper.Split(' ');
|
||||||
|
var morseSb = new StringBuilder();
|
||||||
|
var unknown = new List<char>();
|
||||||
|
|
||||||
|
foreach (var word in words)
|
||||||
|
{
|
||||||
|
if (morseSb.Length > 0) morseSb.Append("/ "); // 단어 구분
|
||||||
|
foreach (var ch in word)
|
||||||
|
{
|
||||||
|
if (TextToMorse.TryGetValue(ch, out var code))
|
||||||
|
morseSb.Append(code + " ");
|
||||||
|
else
|
||||||
|
unknown.Add(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var morseStr = morseSb.ToString().TrimEnd();
|
||||||
|
if (string.IsNullOrEmpty(morseStr))
|
||||||
|
{
|
||||||
|
yield return new LauncherItem("변환 불가", "모스 부호에 없는 문자입니다", null, null, Symbol: "\uE783");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return new LauncherItem(
|
||||||
|
morseStr.Length > 80 ? morseStr[..80] + "…" : morseStr,
|
||||||
|
$"'{text}' → 모스 부호 · Enter 복사",
|
||||||
|
null,
|
||||||
|
("copy", morseStr),
|
||||||
|
Symbol: "\uE8C4");
|
||||||
|
|
||||||
|
if (unknown.Count > 0)
|
||||||
|
yield return new LauncherItem("변환 불가 문자",
|
||||||
|
string.Join(" ", unknown.Distinct()), null, null, Symbol: "\uE946");
|
||||||
|
|
||||||
|
// 문자별 표 (최대 10자)
|
||||||
|
var displayText = upper.Replace(" ", "");
|
||||||
|
foreach (var ch in displayText.Take(10))
|
||||||
|
{
|
||||||
|
if (TextToMorse.TryGetValue(ch, out var code) && ch != ' ')
|
||||||
|
yield return new LauncherItem($"{ch} = {code}", "문자별 코드", null, ("copy", code), Symbol: "\uE8C4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<LauncherItem> BuildMorseToText(string morse)
|
||||||
|
{
|
||||||
|
// "/" 는 단어 구분, 공백은 문자 구분
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var words = morse.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var unknown = new List<string>();
|
||||||
|
|
||||||
|
foreach (var word in words)
|
||||||
|
{
|
||||||
|
var codes = word.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var code in codes)
|
||||||
|
{
|
||||||
|
if (MorseToText.TryGetValue(code, out var ch))
|
||||||
|
sb.Append(ch);
|
||||||
|
else
|
||||||
|
unknown.Add(code);
|
||||||
|
}
|
||||||
|
sb.Append(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = sb.ToString().Trim();
|
||||||
|
if (string.IsNullOrEmpty(result))
|
||||||
|
{
|
||||||
|
yield return new LauncherItem("변환 실패", "인식할 수 없는 모스 부호입니다", null, null, Symbol: "\uE783");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return new LauncherItem(
|
||||||
|
result,
|
||||||
|
$"모스 부호 → '{result}' · Enter 복사",
|
||||||
|
null,
|
||||||
|
("copy", result),
|
||||||
|
Symbol: "\uE8C4");
|
||||||
|
|
||||||
|
if (unknown.Count > 0)
|
||||||
|
yield return new LauncherItem("인식 불가 코드",
|
||||||
|
string.Join(" ", unknown.Distinct()), null, null, Symbol: "\uE946");
|
||||||
|
|
||||||
|
// 코드별 표 (최대 10개)
|
||||||
|
var codes_ = morse.Trim().Split(new[] { ' ', '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var code in codes_.Take(10))
|
||||||
|
{
|
||||||
|
if (MorseToText.TryGetValue(code, out var ch))
|
||||||
|
yield return new LauncherItem($"{code} = {ch}", "코드별 문자", null, ("copy", ch.ToString()), Symbol: "\uE8C4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static bool IsMorseCode(string s)
|
||||||
|
{
|
||||||
|
// .-/ 문자와 공백만으로 구성되어 있으면 모스 부호로 판단
|
||||||
|
return !string.IsNullOrWhiteSpace(s) &&
|
||||||
|
s.All(c => c is '.' or '-' or '/' or ' ') &&
|
||||||
|
(s.Contains('.') || s.Contains('-'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetClipboard()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return System.Windows.Application.Current.Dispatcher.Invoke(
|
||||||
|
() => Clipboard.ContainsText() ? Clipboard.GetText() : "");
|
||||||
|
}
|
||||||
|
catch { return ""; }
|
||||||
|
}
|
||||||
|
}
|
||||||
230
src/AxCopilot/Handlers/StartupHandler.cs
Normal file
230
src/AxCopilot/Handlers/StartupHandler.cs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
using Microsoft.Win32;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Themes;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L12-4: 시작 프로그램 조회 핸들러. "startup" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 예: startup → 전체 시작 프로그램 목록
|
||||||
|
/// startup search ms → "ms" 포함 항목 필터
|
||||||
|
/// startup folder → 시작 프로그램 폴더 열기
|
||||||
|
/// Enter → 항목 경로를 클립보드에 복사.
|
||||||
|
/// </summary>
|
||||||
|
public class StartupHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "startup";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"Startup",
|
||||||
|
"시작 프로그램 조회 — 레지스트리 · 폴더 · 필터",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
// 조회할 레지스트리 키 경로들
|
||||||
|
private static readonly (string Path, string Scope)[] RegKeys =
|
||||||
|
[
|
||||||
|
(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", "현재 사용자"),
|
||||||
|
(@"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce", "현재 사용자 (1회)"),
|
||||||
|
(@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run", "현재 사용자 (32bit)"),
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly (string Path, string Scope)[] RegKeysHKLM =
|
||||||
|
[
|
||||||
|
(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", "모든 사용자"),
|
||||||
|
(@"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce", "모든 사용자 (1회)"),
|
||||||
|
(@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run", "모든 사용자 (32bit)"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim();
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
|
||||||
|
var allEntries = CollectAllEntries();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(q))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
$"시작 프로그램 {allEntries.Count}개",
|
||||||
|
"레지스트리 + 시작 폴더",
|
||||||
|
null, null, Symbol: "\uE7FC"));
|
||||||
|
|
||||||
|
items.Add(new LauncherItem("startup folder", "시작 폴더 열기", null, ("open_folder", ""), Symbol: "\uE7FC"));
|
||||||
|
|
||||||
|
// 그룹별 표시
|
||||||
|
var byScope = allEntries.GroupBy(e => e.Scope).OrderBy(g => g.Key);
|
||||||
|
foreach (var group in byScope)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"── {group.Key} ({group.Count()}개) ──", "", null, null, Symbol: "\uE7FC"));
|
||||||
|
foreach (var e in group.Take(10))
|
||||||
|
items.Add(MakeEntryItem(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var sub = parts[0].ToLowerInvariant();
|
||||||
|
|
||||||
|
switch (sub)
|
||||||
|
{
|
||||||
|
case "folder":
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("시작 폴더 열기 (현재 사용자)", GetStartupFolderUser(),
|
||||||
|
null, ("open_folder", GetStartupFolderUser()), Symbol: "\uE7FC"));
|
||||||
|
items.Add(new LauncherItem("시작 폴더 열기 (모든 사용자)", GetStartupFolderCommon(),
|
||||||
|
null, ("open_folder", GetStartupFolderCommon()), Symbol: "\uE7FC"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "search":
|
||||||
|
case "find":
|
||||||
|
{
|
||||||
|
var keyword = parts.Length > 1 ? parts[1].ToLowerInvariant() : "";
|
||||||
|
if (string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("검색어 입력", "예: startup search teams", null, null, Symbol: "\uE783"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var filtered = allEntries.Where(e =>
|
||||||
|
e.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
e.Command.Contains(keyword, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
|
||||||
|
if (filtered.Count == 0)
|
||||||
|
items.Add(new LauncherItem("결과 없음", $"'{keyword}' 항목 없음", null, null, Symbol: "\uE946"));
|
||||||
|
else
|
||||||
|
foreach (var e in filtered)
|
||||||
|
items.Add(MakeEntryItem(e));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
// 검색어로 처리
|
||||||
|
var keyword = q.ToLowerInvariant();
|
||||||
|
var filtered = allEntries.Where(e =>
|
||||||
|
e.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
e.Command.Contains(keyword, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
|
||||||
|
if (filtered.Count == 0)
|
||||||
|
items.Add(new LauncherItem("결과 없음", $"'{q}' 항목 없음", null, null, Symbol: "\uE946"));
|
||||||
|
else
|
||||||
|
foreach (var e in filtered.Take(15))
|
||||||
|
items.Add(MakeEntryItem(e));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Startup", "클립보드에 복사했습니다.");
|
||||||
|
}
|
||||||
|
catch { /* 비핵심 */ }
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ("open_folder", string path):
|
||||||
|
var folderPath = string.IsNullOrEmpty(path) ? GetStartupFolderUser() : path;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = folderPath,
|
||||||
|
UseShellExecute = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
NotificationService.Notify("Startup", $"폴더 열기 실패: {ex.Message}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 레지스트리 수집 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private record StartupEntry(string Name, string Command, string Scope, string Source);
|
||||||
|
|
||||||
|
private static List<StartupEntry> CollectAllEntries()
|
||||||
|
{
|
||||||
|
var result = new List<StartupEntry>();
|
||||||
|
|
||||||
|
// HKCU
|
||||||
|
foreach (var (regPath, scope) in RegKeys)
|
||||||
|
result.AddRange(ReadRegistryRun(Registry.CurrentUser, regPath, scope));
|
||||||
|
|
||||||
|
// HKLM
|
||||||
|
foreach (var (regPath, scope) in RegKeysHKLM)
|
||||||
|
result.AddRange(ReadRegistryRun(Registry.LocalMachine, regPath, scope));
|
||||||
|
|
||||||
|
// 시작 폴더 (현재 사용자)
|
||||||
|
result.AddRange(ReadStartupFolder(GetStartupFolderUser(), "현재 사용자 (폴더)"));
|
||||||
|
|
||||||
|
// 시작 폴더 (모든 사용자)
|
||||||
|
result.AddRange(ReadStartupFolder(GetStartupFolderCommon(), "모든 사용자 (폴더)"));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<StartupEntry> ReadRegistryRun(RegistryKey hive, string path, string scope)
|
||||||
|
{
|
||||||
|
RegistryKey? key;
|
||||||
|
try { key = hive.OpenSubKey(path, writable: false); }
|
||||||
|
catch { yield break; }
|
||||||
|
|
||||||
|
if (key == null) yield break;
|
||||||
|
|
||||||
|
using (key)
|
||||||
|
{
|
||||||
|
foreach (var name in key.GetValueNames())
|
||||||
|
{
|
||||||
|
var cmd = key.GetValue(name)?.ToString() ?? "";
|
||||||
|
yield return new StartupEntry(name, cmd, scope, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<StartupEntry> ReadStartupFolder(string folderPath, string scope)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(folderPath)) yield break;
|
||||||
|
foreach (var file in Directory.EnumerateFiles(folderPath, "*.lnk"))
|
||||||
|
{
|
||||||
|
yield return new StartupEntry(
|
||||||
|
Path.GetFileNameWithoutExtension(file),
|
||||||
|
file,
|
||||||
|
scope,
|
||||||
|
folderPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetStartupFolderUser() =>
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Startup);
|
||||||
|
|
||||||
|
private static string GetStartupFolderCommon() =>
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartup);
|
||||||
|
|
||||||
|
private static LauncherItem MakeEntryItem(StartupEntry e)
|
||||||
|
{
|
||||||
|
var cmdShort = e.Command.Length > 70 ? e.Command[..70] + "…" : e.Command;
|
||||||
|
return new LauncherItem(
|
||||||
|
e.Name,
|
||||||
|
$"{e.Scope} · {cmdShort}",
|
||||||
|
null,
|
||||||
|
("copy", e.Command),
|
||||||
|
Symbol: "\uE7FC");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user