Files
AX-Copilot-Codex/src/AxCopilot/Handlers/VolHandler.cs
lacvet 0336904258 AX Commander 비교본 런처 기능 대량 이식
변경 목적: 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개를 확인했습니다.
2026-04-05 00:59:45 +09:00

212 lines
8.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}