323 lines
13 KiB
C#
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);
|
|
}
|