using System.IO;
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
///
/// L12-2: Windows hosts 파일 관리 핸들러. "hosts" 프리픽스로 사용합니다.
///
/// 예: hosts → 현재 hosts 파일 항목 목록
/// hosts search dev → "dev" 포함 항목 필터
/// hosts open → hosts 파일을 메모장으로 열기
/// hosts copy → 전체 hosts 내용 클립보드 복사
/// Enter → 해당 항목을 클립보드에 복사.
///
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> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List();
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>(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>(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 ReadHostsEntries()
{
var result = new List();
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);
}
}