Initial commit to new repository

This commit is contained in:
2026-04-03 18:23:52 +09:00
commit deffb33cf9
5248 changed files with 267762 additions and 0 deletions

View File

@@ -0,0 +1,228 @@
using Microsoft.Win32;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 앱 제거 핸들러. "uninstall" 프리픽스로 사용합니다.
/// 예: uninstall → 설치된 앱 전체 목록
/// uninstall chrome → "chrome" 포함 앱 필터
/// Enter 시 해당 앱의 제거 프로그램 실행.
/// </summary>
public class UninstallHandler : IActionHandler
{
public string? Prefix => "uninstall";
public PluginMetadata Metadata => new(
"Uninstall",
"앱 제거 — uninstall 뒤에 앱 이름 입력",
"1.0",
"AX");
// 레지스트리 언인스톨 키 경로
private static readonly string[] _regPaths =
{
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
};
// 캐시: 30초
private static (DateTime At, List<InstalledApp> Apps)? _cache;
private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(30);
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
if (string.IsNullOrWhiteSpace(q))
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"제거할 앱 이름을 입력하세요",
"예: uninstall chrome · uninstall office",
null, null, Symbol: Symbols.Uninstall)
]);
}
var apps = GetInstalledApps();
var filtered = apps
.Where(a => a.Name.Contains(q, StringComparison.OrdinalIgnoreCase) ||
(a.Publisher?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false))
.OrderBy(a => a.Name)
.Take(12)
.ToList();
if (!filtered.Any())
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"검색 결과 없음",
$"'{q}'에 해당하는 설치된 앱이 없습니다",
null, null, Symbol: Symbols.Info)
]);
}
var items = filtered.Select(a => new LauncherItem(
a.Name,
BuildSubtitle(a),
null,
a,
Symbol: Symbols.Uninstall)).ToList();
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not InstalledApp app) return Task.CompletedTask;
if (string.IsNullOrWhiteSpace(app.UninstallString))
{
LogService.Warn($"제거 문자열 없음: {app.Name}");
return Task.CompletedTask;
}
try
{
// UninstallString이 "msiexec /x {GUID}" 또는 경로+args 형태
var uninstall = app.UninstallString.Trim();
if (uninstall.StartsWith("msiexec", StringComparison.OrdinalIgnoreCase))
{
// msiexec 직접 실행
var args = uninstall.Substring("msiexec".Length).Trim();
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(
"msiexec.exe", args) { UseShellExecute = true });
}
else
{
// 따옴표로 둘러싸인 경로 + 나머지 인수 분리
string exe, arguments;
if (uninstall.StartsWith('"'))
{
var close = uninstall.IndexOf('"', 1);
exe = close > 0 ? uninstall[1..close] : uninstall;
arguments = close > 0 ? uninstall[(close + 1)..].Trim() : "";
}
else
{
var spaceIdx = uninstall.IndexOf(' ');
if (spaceIdx > 0)
{
exe = uninstall[..spaceIdx];
arguments = uninstall[(spaceIdx + 1)..];
}
else
{
exe = uninstall;
arguments = "";
}
}
var psi = new System.Diagnostics.ProcessStartInfo(exe, arguments)
{ UseShellExecute = true };
System.Diagnostics.Process.Start(psi);
}
}
catch (Exception ex)
{
LogService.Warn($"제거 실행 실패 [{app.Name}]: {ex.Message}");
}
return Task.CompletedTask;
}
// ─── 내부 ─────────────────────────────────────────────────────────────────
private static string BuildSubtitle(InstalledApp a)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(a.Publisher)) parts.Add(a.Publisher!);
if (!string.IsNullOrWhiteSpace(a.Version)) parts.Add($"v{a.Version}");
if (!string.IsNullOrWhiteSpace(a.InstallDate)) parts.Add(a.InstallDate!);
parts.Add("Enter로 제거");
return string.Join(" · ", parts);
}
private static List<InstalledApp> GetInstalledApps()
{
if (_cache.HasValue && (DateTime.Now - _cache.Value.At) < CacheTtl)
return _cache.Value.Apps;
var result = new List<InstalledApp>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var root in new[] { Registry.LocalMachine, Registry.CurrentUser })
{
foreach (var regPath in _regPaths)
{
try
{
using var key = root.OpenSubKey(regPath);
if (key == null) continue;
foreach (var subName in key.GetSubKeyNames())
{
try
{
using var sub = key.OpenSubKey(subName);
if (sub == null) continue;
var name = sub.GetValue("DisplayName") as string;
var uninstall = sub.GetValue("UninstallString") as string;
// 이름·제거 문자열 없으면 스킵, 시스템 업데이트/패치 스킵
if (string.IsNullOrWhiteSpace(name)) continue;
if (string.IsNullOrWhiteSpace(uninstall)) continue;
if (name.StartsWith("KB", StringComparison.OrdinalIgnoreCase) &&
name.Length < 12) continue;
if (!seen.Add(name)) continue;
var systemComponent = sub.GetValue("SystemComponent");
if (systemComponent is int sc && sc == 1) continue;
result.Add(new InstalledApp
{
Name = name,
UninstallString = uninstall,
Publisher = sub.GetValue("Publisher") as string,
Version = sub.GetValue("DisplayVersion") as string,
InstallDate = FormatDate(sub.GetValue("InstallDate") as string),
});
}
catch (Exception) { /* 개별 키 읽기 실패 무시 */ }
}
}
catch (Exception) { /* 레지스트리 접근 실패 무시 */ }
}
}
result.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
_cache = (DateTime.Now, result);
return result;
}
private static string? FormatDate(string? raw)
{
// InstallDate 형식: "20230615" → "2023-06-15"
if (raw?.Length == 8 &&
int.TryParse(raw, out _))
{
return $"{raw[..4]}-{raw[4..6]}-{raw[6..8]}";
}
return null;
}
}
internal sealed class InstalledApp
{
public string Name { get; init; } = "";
public string? UninstallString { get; init; }
public string? Publisher { get; init; }
public string? Version { get; init; }
public string? InstallDate { get; init; }
}