변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
212 lines
8.7 KiB
C#
212 lines
8.7 KiB
C#
using System.Runtime.InteropServices;
|
||
using System.Windows;
|
||
using AxCopilot.SDK;
|
||
using AxCopilot.Services;
|
||
using AxCopilot.Themes;
|
||
|
||
namespace AxCopilot.Handlers;
|
||
|
||
/// <summary>
|
||
/// L27-2: 시스템 볼륨 제어 핸들러. "vol" 프리픽스로 사용합니다.
|
||
///
|
||
/// 예: vol → 현재 볼륨 표시
|
||
/// vol 50 → 볼륨 50% 설정
|
||
/// vol up / down → ±10% 조절
|
||
/// vol mute → 음소거 토글
|
||
/// Enter → 해당 명령 실행.
|
||
/// Windows Core Audio API (IAudioEndpointVolume) COM 인터페이스 사용.
|
||
/// </summary>
|
||
public class VolHandler : IActionHandler
|
||
{
|
||
public string? Prefix => "vol";
|
||
|
||
public PluginMetadata Metadata => new(
|
||
"볼륨 제어",
|
||
"시스템 볼륨 조절 — 설정·증감·음소거",
|
||
"1.0",
|
||
"AX");
|
||
|
||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||
{
|
||
var q = query.Trim().ToLowerInvariant();
|
||
var items = new List<LauncherItem>();
|
||
|
||
// 현재 볼륨 읽기
|
||
float curLevel;
|
||
bool curMuted;
|
||
try
|
||
{
|
||
using var audio = AudioEndpoint.GetDefault();
|
||
curLevel = audio.GetVolume();
|
||
curMuted = audio.GetMute();
|
||
}
|
||
catch
|
||
{
|
||
items.Add(new LauncherItem(
|
||
"오디오 장치를 찾을 수 없습니다",
|
||
"기본 재생 장치가 연결되어 있는지 확인하세요",
|
||
null, null, Symbol: Symbols.Warning));
|
||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||
}
|
||
|
||
int pct = (int)Math.Round(curLevel * 100);
|
||
var bar = VolumeBar(pct);
|
||
var muteLabel = curMuted ? " 🔇 음소거" : "";
|
||
var symbol = curMuted ? Symbols.VolumeMute
|
||
: pct == 0 ? Symbols.VolumeMute
|
||
: pct < 50 ? Symbols.VolumeDown
|
||
: Symbols.VolumeUp;
|
||
|
||
if (string.IsNullOrWhiteSpace(q))
|
||
{
|
||
items.Add(new LauncherItem(
|
||
$"현재 볼륨: {pct}%{muteLabel}",
|
||
$"{bar} · vol 50 / vol up / vol down / vol mute",
|
||
null, null, Symbol: symbol));
|
||
|
||
items.Add(new LauncherItem("vol up", "볼륨 +10%", null, ("set", Math.Min(pct + 10, 100)), Symbol: Symbols.VolumeUp));
|
||
items.Add(new LauncherItem("vol down", "볼륨 −10%", null, ("set", Math.Max(pct - 10, 0)), Symbol: Symbols.VolumeDown));
|
||
items.Add(new LauncherItem("vol mute", curMuted ? "음소거 해제" : "음소거", null, ("mute", !curMuted), Symbol: Symbols.VolumeMute));
|
||
items.Add(new LauncherItem("vol 0", "볼륨 0% (무음)", null, ("set", 0), Symbol: Symbols.VolumeMute));
|
||
items.Add(new LauncherItem("vol 50", "볼륨 50%", null, ("set", 50), Symbol: Symbols.VolumeDown));
|
||
items.Add(new LauncherItem("vol 100", "볼륨 100%", null, ("set", 100), Symbol: Symbols.VolumeUp));
|
||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||
}
|
||
|
||
// 명령 파싱
|
||
if (q is "up" or "올려" or "+")
|
||
{
|
||
var target = Math.Min(pct + 10, 100);
|
||
items.Add(new LauncherItem($"볼륨 +10% → {target}%", $"{VolumeBar(target)}", null, ("set", target), Symbol: Symbols.VolumeUp));
|
||
}
|
||
else if (q is "down" or "내려" or "-")
|
||
{
|
||
var target = Math.Max(pct - 10, 0);
|
||
items.Add(new LauncherItem($"볼륨 −10% → {target}%", $"{VolumeBar(target)}", null, ("set", target), Symbol: Symbols.VolumeDown));
|
||
}
|
||
else if (q is "mute" or "음소거" or "m")
|
||
{
|
||
items.Add(new LauncherItem(
|
||
curMuted ? "음소거 해제" : "음소거 설정",
|
||
$"현재: {pct}%{muteLabel}",
|
||
null, ("mute", !curMuted), Symbol: Symbols.VolumeMute));
|
||
}
|
||
else if (int.TryParse(q, out int val) && val is >= 0 and <= 100)
|
||
{
|
||
items.Add(new LauncherItem(
|
||
$"볼륨 {val}% 설정",
|
||
$"{VolumeBar(val)} (현재 {pct}%)",
|
||
null, ("set", val), Symbol: val > pct ? Symbols.VolumeUp : Symbols.VolumeDown));
|
||
}
|
||
else
|
||
{
|
||
items.Add(new LauncherItem(
|
||
$"'{query}' — 알 수 없는 명령",
|
||
"사용법: vol 50 / vol up / vol down / vol mute",
|
||
null, null, Symbol: Symbols.Warning));
|
||
}
|
||
|
||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||
}
|
||
|
||
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||
{
|
||
try
|
||
{
|
||
using var audio = AudioEndpoint.GetDefault();
|
||
|
||
if (item.Data is ("set", int level))
|
||
{
|
||
audio.SetVolume(level / 100f);
|
||
if (audio.GetMute()) audio.SetMute(false);
|
||
NotificationService.Notify("vol", $"볼륨 {level}%");
|
||
}
|
||
else if (item.Data is ("mute", bool mute))
|
||
{
|
||
audio.SetMute(mute);
|
||
NotificationService.Notify("vol", mute ? "음소거" : "음소거 해제");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
NotificationService.Notify("vol", $"오류: {ex.Message}");
|
||
}
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
private static string VolumeBar(int pct)
|
||
{
|
||
int filled = pct / 5; // 0~20
|
||
return "[" + new string('█', filled) + new string('░', 20 - filled) + "]";
|
||
}
|
||
|
||
// ─── Core Audio API COM 래퍼 ─────────────────────────────────────────────
|
||
|
||
private sealed class AudioEndpoint : IDisposable
|
||
{
|
||
private readonly IAudioEndpointVolume _vol;
|
||
|
||
private AudioEndpoint(IAudioEndpointVolume vol) => _vol = vol;
|
||
|
||
public static AudioEndpoint GetDefault()
|
||
{
|
||
var enumerator = (IMMDeviceEnumerator)new MMDeviceEnumeratorClass();
|
||
enumerator.GetDefaultAudioEndpoint(0 /*eRender*/, 1 /*eMultimedia*/, out var device);
|
||
var iid = typeof(IAudioEndpointVolume).GUID;
|
||
device.Activate(ref iid, 1 /*CLSCTX_ALL*/, IntPtr.Zero, out var obj);
|
||
return new AudioEndpoint((IAudioEndpointVolume)obj);
|
||
}
|
||
|
||
public float GetVolume() { _vol.GetMasterVolumeLevelScalar(out float l); return l; }
|
||
public bool GetMute() { _vol.GetMute(out bool m); return m; }
|
||
public void SetVolume(float level) { var g = Guid.Empty; _vol.SetMasterVolumeLevelScalar(Math.Clamp(level, 0f, 1f), ref g); }
|
||
public void SetMute(bool mute) { var g = Guid.Empty; _vol.SetMute(mute, ref g); }
|
||
|
||
public void Dispose()
|
||
{
|
||
if (_vol is IDisposable d) d.Dispose();
|
||
if (_vol != null) Marshal.ReleaseComObject(_vol);
|
||
}
|
||
}
|
||
|
||
// ─── COM 인터페이스 정의 (Windows Core Audio API) ──────────────────────
|
||
|
||
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
||
private class MMDeviceEnumeratorClass { }
|
||
|
||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
|
||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
private interface IMMDeviceEnumerator
|
||
{
|
||
int EnumAudioEndpoints(int dataFlow, uint stateMask, out IntPtr devices);
|
||
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice device);
|
||
}
|
||
|
||
[Guid("D666063F-1587-4E43-81F1-B948E807363F")]
|
||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
private interface IMMDevice
|
||
{
|
||
int Activate(ref Guid iid, uint clsCtx, IntPtr activationParams,
|
||
[MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
||
}
|
||
|
||
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A")]
|
||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||
private interface IAudioEndpointVolume
|
||
{
|
||
int RegisterControlChangeNotify(IntPtr pNotify);
|
||
int UnregisterControlChangeNotify(IntPtr pNotify);
|
||
int GetChannelCount(out uint pnChannelCount);
|
||
int SetMasterVolumeLevel(float fLevelDB, ref Guid pguidEventContext);
|
||
int SetMasterVolumeLevelScalar(float fLevel, ref Guid pguidEventContext);
|
||
int GetMasterVolumeLevel(out float pfLevelDB);
|
||
int GetMasterVolumeLevelScalar(out float pfLevel);
|
||
int SetChannelVolumeLevel(uint nChannel, float fLevelDB, ref Guid pguidEventContext);
|
||
int SetChannelVolumeLevelScalar(uint nChannel, float fLevel, ref Guid pguidEventContext);
|
||
int GetChannelVolumeLevel(uint nChannel, out float pfLevelDB);
|
||
int GetChannelVolumeLevelScalar(uint nChannel, out float pfLevel);
|
||
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, ref Guid pguidEventContext);
|
||
int GetMute([MarshalAs(UnmanagedType.Bool)] out bool pbMute);
|
||
}
|
||
}
|