Files
AX-Copilot/src/AxCopilot/Handlers/SystemCommandHandler.cs

323 lines
13 KiB
C#

using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
/// <summary>
/// 시스템 명령 핸들러. "/" 프리픽스로 사용합니다.
/// 예: /lock → 화면 잠금
/// /sleep → 절전 모드
/// /shutdown → 종료
/// /timer 5m → 5분 타이머
/// /timer 1h30m → 1시간 30분 타이머
/// /timer 30s 회의 → 라벨 포함 타이머
/// /alarm 14:30 → 지정 시각 알람
/// </summary>
public class SystemCommandHandler : IActionHandler
{
private static App? CurrentApp => System.Windows.Application.Current as App;
private readonly SettingsService _settings;
public string? Prefix => "/";
public PluginMetadata Metadata => new(
"SystemCommands",
"시스템 명령 — / 뒤에 명령 입력",
"1.0",
"AX");
public SystemCommandHandler(SettingsService settings)
{
_settings = settings;
}
// 타이머 파싱: 5m, 30s, 1h, 1h30m, 2h15m30s
private static readonly Regex _timerRe = new(
@"^(?:(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?)$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
// 알람 파싱: 14:30, 9:00, 23:59
private static readonly Regex _alarmRe = new(
@"^(\d{1,2}):(\d{2})$",
RegexOptions.Compiled);
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var cfg = _settings.Settings.SystemCommands;
var q = query.Trim().ToLowerInvariant();
// ─── 타이머 ─────────────────────────────────────────────────────────
if (q.StartsWith("timer"))
{
var rest = q[5..].Trim();
var items = new List<LauncherItem>();
if (string.IsNullOrEmpty(rest))
{
items.Add(new LauncherItem("타이머 — 시간을 입력하세요",
"예: /timer 5m · /timer 1h30m · /timer 30s 회의",
null, null, Symbol: Symbols.Timer));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 라벨 분리: "5m 회의" → durationPart="5m", label="회의"
var parts = rest.Split(' ', 2);
var durationStr = parts[0];
var label = parts.Length > 1 ? parts[1].Trim() : "";
if (TryParseTimer(durationStr, out var seconds) && seconds > 0)
{
var display = FormatDuration(seconds);
var title = string.IsNullOrEmpty(label) ? $"타이머 {display}" : $"타이머 {display} — {label}";
items.Add(new LauncherItem(
title,
$"{display} 후 알림 · Enter로 시작",
null,
(Func<Task>)(() => StartTimerAsync(seconds, label.Length > 0 ? label : display)),
Symbol: Symbols.Timer));
}
else
{
items.Add(new LauncherItem("형식 오류",
"예: /timer 5m · /timer 1h30m · /timer 90s",
null, null, Symbol: Symbols.Error));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// ─── 알람 ─────────────────────────────────────────────────────────
if (q.StartsWith("alarm"))
{
var rest = q[5..].Trim();
var items = new List<LauncherItem>();
if (string.IsNullOrEmpty(rest))
{
items.Add(new LauncherItem("알람 — 시각을 입력하세요",
"예: /alarm 14:30 · /alarm 9:00",
null, null, Symbol: Symbols.Timer));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var m = _alarmRe.Match(rest.Split(' ')[0]);
if (m.Success)
{
int hour = int.Parse(m.Groups[1].Value);
int min = int.Parse(m.Groups[2].Value);
var label = rest.Contains(' ') ? rest[(rest.IndexOf(' ') + 1)..] : "";
if (hour is >= 0 and <= 23 && min is >= 0 and <= 59)
{
var target = DateTime.Today.AddHours(hour).AddMinutes(min);
if (target <= DateTime.Now) target = target.AddDays(1); // 내일로
var diff = (int)(target - DateTime.Now).TotalSeconds;
var timeStr = target.ToString("HH:mm");
items.Add(new LauncherItem(
$"알람 {timeStr} {(target.Date > DateTime.Today ? "()" : "")}",
$"{FormatDuration(diff)} 후 알림 · Enter로 시작 {(label.Length > 0 ? " " + label : "")}".Trim(),
null,
(Func<Task>)(() => StartTimerAsync(diff, label.Length > 0 ? label : $"{timeStr} 알람")),
Symbol: Symbols.Timer));
}
else
{
items.Add(new LauncherItem("시각 범위 오류",
"00:00 ~ 23:59 범위로 입력하세요",
null, null, Symbol: Symbols.Error));
}
}
else
{
items.Add(new LauncherItem("형식 오류",
"예: /alarm 14:30 · /alarm 09:00",
null, null, Symbol: Symbols.Error));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// ─── 일반 시스템 명령 ────────────────────────────────────────────────
var allCommands = new List<(string Key, string Name, string Hint, string Symbol, bool Enabled, Func<Task> Action)>
{
("lock", "화면 잠금", "현재 세션을 잠급니다", Symbols.Lock, cfg.ShowLock, LockAsync),
("sleep", "절전 모드", "시스템을 절전 상태로 전환합니다", Symbols.Sleep, cfg.ShowSleep, SleepAsync),
("restart", "재시작", "컴퓨터를 재시작합니다", Symbols.Restart, cfg.ShowRestart, RestartAsync),
("shutdown", "시스템 종료", "컴퓨터를 종료합니다", Symbols.Power, cfg.ShowShutdown, ShutdownAsync),
("hibernate", "최대 절전", "최대 절전 모드로 전환합니다", Symbols.Sleep, cfg.ShowHibernate, HibernateAsync),
("logout", "로그아웃", "현재 사용자 세션에서 로그아웃합니다", Symbols.Logout, cfg.ShowLogout, LogoutAsync),
("recycle", "휴지통 비우기", "휴지통의 모든 파일을 영구 삭제합니다", Symbols.RecycleBin, cfg.ShowRecycleBin, EmptyRecycleBinAsync),
("dock", "독 바 표시/숨기기", "화면 하단 독 바를 표시하거나 숨깁니다", Symbols.SnapLayout, true, ToggleDockAsync),
// timer/alarm은 q.StartsWith("timer"/"alarm") early return으로 처리됨
};
var filtered = allCommands
.Where(c => c.Enabled)
.Where(c => string.IsNullOrEmpty(q) ||
c.Key.StartsWith(q) ||
c.Name.Contains(q) ||
(cfg.CommandAliases.TryGetValue(c.Key, out var ca) &&
ca.Any(a => a.StartsWith(q, StringComparison.OrdinalIgnoreCase))))
.Select(c => new LauncherItem(
c.Name,
c.Hint,
null,
c.Action,
Symbol: c.Symbol));
return Task.FromResult<IEnumerable<LauncherItem>>(filtered.ToList());
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is Func<Task> action)
await action();
}
// ─── 타이머 구현 ─────────────────────────────────────────────────────────
private static async Task StartTimerAsync(int totalSeconds, string label)
{
await Task.Delay(TimeSpan.FromSeconds(totalSeconds));
NotificationService.Notify("⏰ 타이머 완료", label);
}
private static bool TryParseTimer(string s, out int totalSeconds)
{
totalSeconds = 0;
var m = _timerRe.Match(s.ToLowerInvariant());
if (!m.Success) return false;
int h = m.Groups[1].Success ? int.Parse(m.Groups[1].Value) : 0;
int mn = m.Groups[2].Success ? int.Parse(m.Groups[2].Value) : 0;
int sc = m.Groups[3].Success ? int.Parse(m.Groups[3].Value) : 0;
totalSeconds = h * 3600 + mn * 60 + sc;
return true;
}
private static string FormatDuration(int totalSeconds)
{
int h = totalSeconds / 3600;
int m = (totalSeconds % 3600) / 60;
int s = totalSeconds % 60;
if (h > 0 && m > 0) return $"{h}시간 {m}분";
if (h > 0) return $"{h}시간";
if (m > 0 && s > 0) return $"{m}분 {s}초";
if (m > 0) return $"{m}분";
return $"{s}초";
}
// ─── 시스템 명령 구현 ───────────────────────────────────────────────────
private static Task LockAsync()
{
LockWorkStation();
return Task.CompletedTask;
}
private static Task SleepAsync()
{
System.Windows.Forms.Application.SetSuspendState(
System.Windows.Forms.PowerState.Suspend, false, false);
return Task.CompletedTask;
}
private static Task HibernateAsync()
{
System.Windows.Forms.Application.SetSuspendState(
System.Windows.Forms.PowerState.Hibernate, false, false);
return Task.CompletedTask;
}
private static Task RestartAsync()
{
var result = CustomMessageBox.Show(
"컴퓨터를 재시작하시겠습니까?\n저장되지 않은 작업이 있으면 먼저 저장하세요.",
"재시작 확인",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(
"shutdown", "/r /t 5 /c \"AX Copilot에 의해 재시작됩니다.\"")
{ UseShellExecute = true, CreateNoWindow = true });
return Task.CompletedTask;
}
private static Task ShutdownAsync()
{
var result = CustomMessageBox.Show(
"컴퓨터를 종료하시겠습니까?\n저장되지 않은 작업이 있으면 먼저 저장하세요.",
"종료 확인",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(
"shutdown", "/s /t 5 /c \"AX Copilot에 의해 종료됩니다.\"")
{ UseShellExecute = true, CreateNoWindow = true });
return Task.CompletedTask;
}
private static Task LogoutAsync()
{
var result = CustomMessageBox.Show(
"로그아웃하시겠습니까?",
"로그아웃 확인",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
ExitWindowsEx(0, 0); // EWX_LOGOFF
return Task.CompletedTask;
}
private static Task EmptyRecycleBinAsync()
{
var result = CustomMessageBox.Show(
"휴지통을 비우시겠습니까?\n삭제된 파일은 복구할 수 없습니다.",
"휴지통 비우기",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
SHEmptyRecycleBin(IntPtr.Zero, null,
0x0001 | 0x0002 | 0x0004); // SHERB_NOCONFIRMATION | SHERB_NOPROGRESSUI | SHERB_NOSOUND
return Task.CompletedTask;
}
private static Task ToggleDockAsync()
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
CurrentApp?.ToggleDockBar();
});
return Task.CompletedTask;
}
// ─── P/Invoke ──────────────────────────────────────────────────────────
[DllImport("user32.dll", SetLastError = true)]
private static extern bool LockWorkStation();
[DllImport("user32.dll", SetLastError = true)]
private static extern bool ExitWindowsEx(uint uFlags, uint dwReason);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern uint SHEmptyRecycleBin(IntPtr hwnd, string? pszRootPath, uint dwFlags);
}