Initial commit to new repository
This commit is contained in:
322
src/AxCopilot/Handlers/SystemCommandHandler.cs
Normal file
322
src/AxCopilot/Handlers/SystemCommandHandler.cs
Normal file
@@ -0,0 +1,322 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user