Initial commit to new repository

This commit is contained in:
2026-04-03 18:22:19 +09:00
commit 4458bb0f52
7672 changed files with 452440 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace AxCopilot.Core;
public sealed class BulkObservableCollection<T> : ObservableCollection<T>
{
private bool _suppressNotification;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
{
base.OnCollectionChanged(e);
}
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (!_suppressNotification)
{
base.OnPropertyChanged(e);
}
}
public void ReplaceAll(IEnumerable<T> items)
{
_suppressNotification = true;
try
{
base.Items.Clear();
foreach (T item in items)
{
base.Items.Add(item);
}
}
finally
{
_suppressNotification = false;
}
OnPropertyChanged(new PropertyChangedEventArgs("Count"));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}

View File

@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class CommandResolver
{
private readonly FuzzyEngine _fuzzy;
private readonly SettingsService _settings;
private readonly Dictionary<string, IActionHandler> _handlers = new Dictionary<string, IActionHandler>();
private readonly List<IActionHandler> _fuzzyHandlers = new List<IActionHandler>();
public IReadOnlyDictionary<string, IActionHandler> RegisteredHandlers => _handlers;
public CommandResolver(FuzzyEngine fuzzy, SettingsService settings)
{
_fuzzy = fuzzy;
_settings = settings;
}
public void RegisterHandler(IActionHandler handler)
{
if (handler.Prefix == null)
{
_fuzzyHandlers.Add(handler);
LogService.Info("FuzzyHandler 등록: name='" + handler.Metadata.Name + "'");
return;
}
if (_handlers.ContainsKey(handler.Prefix))
{
LogService.Warn($"Prefix '{handler.Prefix}' 중복 등록: '{handler.Metadata.Name}'이 기존 핸들러를 덮어씁니다.");
}
_handlers[handler.Prefix] = handler;
LogService.Info($"Handler 등록: prefix='{handler.Prefix}', name='{handler.Metadata.Name}'");
}
public async Task<IEnumerable<LauncherItem>> ResolveAsync(string input, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(input))
{
return Enumerable.Empty<LauncherItem>();
}
foreach (var (prefix, handler) in _handlers)
{
if (input.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
object obj;
if (input.Length <= prefix.Length)
{
obj = "";
}
else
{
string text2 = input;
int length = prefix.Length;
obj = text2.Substring(length, text2.Length - length).Trim();
}
string query = (string)obj;
try
{
return await handler.GetItemsAsync(query, ct);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex2)
{
LogService.Error("Handler '" + handler.Metadata.Name + "' 오류: " + ex2.Message);
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("오류: " + ex2.Message, handler.Metadata.Name, null, null));
}
}
}
int maxResults = _settings.Settings.Launcher.MaxResults;
HashSet<string> seenPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
List<LauncherItem> fuzzyItems = UsageRankingService.SortByUsage((from r in _fuzzy.Search(input, maxResults * 2)
where seenPaths.Add(r.Entry.Path)
select r).Take(maxResults).Select(delegate(FuzzyResult r)
{
string displayName = r.Entry.DisplayName;
string subtitle;
if (r.Entry.Type == IndexEntryType.Alias)
{
string aliasType = r.Entry.AliasType;
if (1 == 0)
{
}
string text3 = ((aliasType == "url") ? "URL 단축키" : ((!(aliasType == "batch")) ? r.Entry.Path : "명령 단축키"));
if (1 == 0)
{
}
subtitle = text3;
}
else
{
subtitle = r.Entry.Path + " ⇧ Shift+Enter: 폴더 열기";
}
object entry = r.Entry;
IndexEntryType type = r.Entry.Type;
if (1 == 0)
{
}
string symbol;
switch (type)
{
case IndexEntryType.App:
symbol = "\uecaa";
break;
case IndexEntryType.Folder:
symbol = "\ue8b7";
break;
case IndexEntryType.Alias:
{
string aliasType2 = r.Entry.AliasType;
if (1 == 0)
{
}
string text3 = ((aliasType2 == "url") ? "\ue774" : ((!(aliasType2 == "batch")) ? "\uecca" : "\ue756"));
if (1 == 0)
{
}
symbol = text3;
break;
}
default:
symbol = "\ue8a5";
break;
}
if (1 == 0)
{
}
return new LauncherItem(displayName, subtitle, null, entry, null, symbol);
}), (LauncherItem item) => (item.Data as IndexEntry)?.Path).ToList();
if (_fuzzyHandlers.Count > 0)
{
List<Task<IEnumerable<LauncherItem>>> extraTasks = _fuzzyHandlers.Select((IActionHandler h) => SafeGetItemsAsync(h, input, ct)).ToList();
await Task.WhenAll(extraTasks);
foreach (Task<IEnumerable<LauncherItem>> task in extraTasks)
{
if (task.IsCompletedSuccessfully)
{
fuzzyItems.AddRange(task.Result.Take(3));
}
}
}
return fuzzyItems;
}
public async Task ExecuteAsync(LauncherItem item, string lastInput, CancellationToken ct)
{
foreach (var (prefix, handler) in _handlers)
{
if (lastInput.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
object obj;
if (lastInput.Length <= prefix.Length)
{
obj = "";
}
else
{
int length = prefix.Length;
obj = lastInput.Substring(length, lastInput.Length - length).Trim().Split(' ')[0];
}
string q = (string)obj;
string cmdKey = (string.IsNullOrEmpty(q) ? prefix : (prefix + q));
UsageStatisticsService.RecordCommandUsage(cmdKey);
await handler.ExecuteAsync(item, ct);
return;
}
}
object data = item.Data;
if (data is string urlData && urlData.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
await ExecuteNullPrefixAsync(item, ct);
return;
}
data = item.Data;
IndexEntry entry = data as IndexEntry;
if (entry == null)
{
return;
}
string expanded = Environment.ExpandEnvironmentVariables(entry.Path);
try
{
await Task.Run(() => Process.Start(new ProcessStartInfo(expanded)
{
UseShellExecute = true
}));
}
catch (Exception ex)
{
Exception ex2 = ex;
LogService.Error("실행 실패: " + expanded + " - " + ex2.Message);
}
Task.Run(delegate
{
UsageRankingService.RecordExecution(entry.Path);
});
}
public async Task ExecuteNullPrefixAsync(LauncherItem item, CancellationToken ct)
{
foreach (IActionHandler handler in _fuzzyHandlers)
{
try
{
await handler.ExecuteAsync(item, ct);
return;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex2)
{
LogService.Error("FuzzyHandler '" + handler.Metadata.Name + "' 실행 오류: " + ex2.Message);
}
}
}
private static async Task<IEnumerable<LauncherItem>> SafeGetItemsAsync(IActionHandler handler, string query, CancellationToken ct)
{
try
{
return await handler.GetItemsAsync(query, ct);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex2)
{
Exception ex3 = ex2;
LogService.Error("FuzzyHandler '" + handler.Metadata.Name + "' 오류: " + ex3.Message);
return Enumerable.Empty<LauncherItem>();
}
}
}

View File

@@ -0,0 +1,384 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class ContextManager
{
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
private struct WINDOWPLACEMENT
{
public uint length;
public uint flags;
public uint showCmd;
public POINT ptMinPosition;
public POINT ptMaxPosition;
public RECT rcNormalPosition;
}
private struct POINT
{
public int x;
public int y;
}
private delegate bool EnumWindowsProc(nint hWnd, nint lParam);
private delegate bool MonitorEnumProc(nint hMonitor, nint hdcMonitor, ref RECT lprcMonitor, nint dwData);
private readonly SettingsService _settings;
private const uint SWP_NOZORDER = 4u;
private const uint SWP_NOACTIVATE = 16u;
private const uint MONITOR_DEFAULTTONEAREST = 2u;
public ContextManager(SettingsService settings)
{
_settings = settings;
}
public WorkspaceProfile CaptureProfile(string name)
{
List<WindowSnapshot> snapshots = new List<WindowSnapshot>();
Dictionary<nint, int> monitorMap = BuildMonitorMap();
EnumWindows(delegate(nint hWnd, nint _)
{
if (!IsWindowVisible(hWnd))
{
return true;
}
if (IsIconic(hWnd))
{
return true;
}
string windowTitle = GetWindowTitle(hWnd);
if (string.IsNullOrWhiteSpace(windowTitle))
{
return true;
}
if (IsSystemWindow(hWnd))
{
return true;
}
string processPath = GetProcessPath(hWnd);
if (string.IsNullOrEmpty(processPath))
{
return true;
}
GetWindowRect(hWnd, out var lpRect);
GetWindowPlacement(hWnd, out var lpwndpl);
uint showCmd = lpwndpl.showCmd;
if (1 == 0)
{
}
string text = showCmd switch
{
1u => "Normal",
2u => "Minimized",
3u => "Maximized",
_ => "Normal",
};
if (1 == 0)
{
}
string showCmd2 = text;
int monitorIndex = GetMonitorIndex(hWnd, monitorMap);
snapshots.Add(new WindowSnapshot
{
Exe = processPath,
Title = windowTitle,
Rect = new WindowRect
{
X = lpRect.Left,
Y = lpRect.Top,
Width = lpRect.Right - lpRect.Left,
Height = lpRect.Bottom - lpRect.Top
},
ShowCmd = showCmd2,
Monitor = monitorIndex
});
return true;
}, IntPtr.Zero);
WorkspaceProfile workspaceProfile = new WorkspaceProfile
{
Name = name,
Windows = snapshots,
CreatedAt = DateTime.Now
};
WorkspaceProfile workspaceProfile2 = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (workspaceProfile2 != null)
{
_settings.Settings.Profiles.Remove(workspaceProfile2);
}
_settings.Settings.Profiles.Add(workspaceProfile);
_settings.Save();
LogService.Info($"프로필 '{name}' 저장 완료: {snapshots.Count}개 창");
return workspaceProfile;
}
public async Task<RestoreResult> RestoreProfileAsync(string name, CancellationToken ct = default(CancellationToken))
{
WorkspaceProfile profile = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (profile == null)
{
return new RestoreResult(Success: false, "프로필 '" + name + "'을 찾을 수 없습니다.");
}
List<string> results = new List<string>();
int monitorCount = GetMonitorCount();
foreach (WindowSnapshot snapshot in profile.Windows)
{
ct.ThrowIfCancellationRequested();
nint hWnd = FindMatchingWindow(snapshot);
if (hWnd == IntPtr.Zero && File.Exists(snapshot.Exe))
{
try
{
Process.Start(new ProcessStartInfo(snapshot.Exe)
{
UseShellExecute = true
});
hWnd = await WaitForWindowAsync(snapshot.Exe, TimeSpan.FromSeconds(3.0), ct);
}
catch (Exception ex)
{
Exception ex2 = ex;
results.Add($"⚠ {snapshot.Title}: 실행 실패 ({ex2.Message})");
LogService.Warn("앱 실행 실패: " + snapshot.Exe + " - " + ex2.Message);
continue;
}
}
if (hWnd == IntPtr.Zero)
{
results.Add("⏭ " + snapshot.Title + ": 창 없음, 건너뜀");
continue;
}
if (snapshot.Monitor >= monitorCount)
{
string policy = _settings.Settings.MonitorMismatch;
if (policy == "skip")
{
results.Add("⏭ " + snapshot.Title + ": 모니터 불일치, 건너뜀");
continue;
}
}
try
{
nint hWnd2 = hWnd;
string showCmd = snapshot.ShowCmd;
if (1 == 0)
{
}
string text = showCmd;
int nCmdShow = ((text == "Maximized") ? 3 : ((!(text == "Minimized")) ? 9 : 2));
if (1 == 0)
{
}
ShowWindow(hWnd2, nCmdShow);
if (snapshot.ShowCmd == "Normal")
{
SetWindowPos(hWnd, IntPtr.Zero, snapshot.Rect.X, snapshot.Rect.Y, snapshot.Rect.Width, snapshot.Rect.Height, 20u);
}
results.Add("✓ " + snapshot.Title + ": 복원 완료");
}
catch (Exception ex3)
{
results.Add($"⚠ {snapshot.Title}: 복원 실패 ({ex3.Message})");
LogService.Warn("창 복원 실패 (권한 문제 가능): " + snapshot.Exe);
}
}
LogService.Info("프로필 '" + name + "' 복원: " + string.Join(", ", results));
return new RestoreResult(Success: true, string.Join("\n", results));
}
public bool DeleteProfile(string name)
{
WorkspaceProfile workspaceProfile = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (workspaceProfile == null)
{
return false;
}
_settings.Settings.Profiles.Remove(workspaceProfile);
_settings.Save();
return true;
}
public bool RenameProfile(string oldName, string newName)
{
WorkspaceProfile workspaceProfile = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(oldName, StringComparison.OrdinalIgnoreCase));
if (workspaceProfile == null)
{
return false;
}
workspaceProfile.Name = newName;
_settings.Save();
return true;
}
private static nint FindMatchingWindow(WindowSnapshot snapshot)
{
nint found = IntPtr.Zero;
EnumWindows(delegate(nint hWnd, nint _)
{
string processPath = GetProcessPath(hWnd);
if (string.Equals(processPath, snapshot.Exe, StringComparison.OrdinalIgnoreCase))
{
found = hWnd;
return false;
}
return true;
}, IntPtr.Zero);
return found;
}
private static async Task<nint> WaitForWindowAsync(string exePath, TimeSpan timeout, CancellationToken ct)
{
DateTime deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
ct.ThrowIfCancellationRequested();
nint hWnd = FindMatchingWindow(new WindowSnapshot
{
Exe = exePath
});
if (hWnd != IntPtr.Zero)
{
return hWnd;
}
await Task.Delay(200, ct);
}
return IntPtr.Zero;
}
private static string GetWindowTitle(nint hWnd)
{
StringBuilder stringBuilder = new StringBuilder(256);
GetWindowText(hWnd, stringBuilder, stringBuilder.Capacity);
return stringBuilder.ToString();
}
private static string GetProcessPath(nint hWnd)
{
try
{
GetWindowThreadProcessId(hWnd, out var lpdwProcessId);
if (lpdwProcessId == 0)
{
return "";
}
Process processById = Process.GetProcessById((int)lpdwProcessId);
return processById.MainModule?.FileName ?? "";
}
catch
{
return "";
}
}
private static bool IsSystemWindow(nint hWnd)
{
StringBuilder stringBuilder = new StringBuilder(256);
GetClassName(hWnd, stringBuilder, stringBuilder.Capacity);
switch (stringBuilder.ToString())
{
case "Shell_TrayWnd":
case "Progman":
case "WorkerW":
case "DV2ControlHost":
return true;
default:
return false;
}
}
private static Dictionary<nint, int> BuildMonitorMap()
{
List<nint> monitors = new List<nint>();
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, delegate(nint hMonitor, nint hdcMonitor, ref RECT lprc, nint dwData)
{
monitors.Add(hMonitor);
return true;
}, IntPtr.Zero);
return monitors.Select((nint hm, int idx) => (hm: hm, idx: idx)).ToDictionary(((nint hm, int idx) t) => t.hm, ((nint hm, int idx) t) => t.idx);
}
private static int GetMonitorIndex(nint hWnd, Dictionary<nint, int> map)
{
nint key = MonitorFromWindow(hWnd, 2u);
int value;
return map.TryGetValue(key, out value) ? value : 0;
}
private static int GetMonitorCount()
{
int count = 0;
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, delegate
{
count++;
return true;
}, IntPtr.Zero);
return count;
}
[DllImport("user32.dll")]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, nint lParam);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(nint hWnd);
[DllImport("user32.dll")]
private static extern bool IsIconic(nint hWnd);
[DllImport("user32.dll")]
private static extern int GetWindowText(nint hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
private static extern bool GetWindowRect(nint hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern bool GetWindowPlacement(nint hWnd, out WINDOWPLACEMENT lpwndpl);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern int GetClassName(nint hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll")]
private static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern bool ShowWindow(nint hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern nint MonitorFromWindow(nint hwnd, uint dwFlags);
[DllImport("user32.dll")]
private static extern bool EnumDisplayMonitors(nint hdc, nint lprcClip, MonitorEnumProc lpfnEnum, nint dwData);
}

View File

@@ -0,0 +1,468 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class FuzzyEngine
{
private readonly IndexService _index;
private static readonly char[] Chosungs = new char[19]
{
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ',
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
};
private static readonly HashSet<char> ChosungSet = new HashSet<char>(new _003C_003Ez__ReadOnlyArray<char>(new char[19]
{
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ',
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
}));
private static readonly char[] Jungsungs = new char[21]
{
'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ',
'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ',
'ㅣ'
};
private static readonly char[] Jongsungs = new char[28]
{
'\0', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ',
'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ',
'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
};
public FuzzyEngine(IndexService index)
{
_index = index;
}
public IEnumerable<FuzzyResult> Search(string query, int maxResults = 7)
{
if (string.IsNullOrWhiteSpace(query))
{
return Enumerable.Empty<FuzzyResult>();
}
string normalized = query.Trim().ToLowerInvariant();
IReadOnlyList<IndexEntry> entries = _index.Entries;
bool queryHasKorean = false;
string text = normalized;
foreach (char c in text)
{
if ((c >= '가' && c <= '힣') || ChosungSet.Contains(c))
{
queryHasKorean = true;
break;
}
}
if (entries.Count > 300)
{
return (from e in entries.AsParallel()
select new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)) into r
where r.Score > 0
orderby r.Score descending
select r).Take(maxResults);
}
return (from e in entries
select new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)) into r
where r.Score > 0
orderby r.Score descending
select r).Take(maxResults);
}
private static int CalculateScoreFast(string query, IndexEntry entry, bool queryHasKorean)
{
string text = (string.IsNullOrEmpty(entry.NameLower) ? entry.Name.ToLowerInvariant() : entry.NameLower);
if (query.Length == 0)
{
return 0;
}
if (text == query)
{
return 1000 + entry.Score;
}
if (text.StartsWith(query))
{
return 800 + entry.Score;
}
if (text.Contains(query))
{
return 600 + entry.Score;
}
if (!queryHasKorean)
{
int num = FuzzyMatch(query, text);
return (num > 0) ? (num + entry.Score) : 0;
}
int num2 = JamoContainsScoreFast(string.IsNullOrEmpty(entry.NameJamo) ? DecomposeToJamo(text) : entry.NameJamo, query);
if (num2 > 0)
{
return num2 + entry.Score;
}
int num3 = ChosungMatchScoreFast(string.IsNullOrEmpty(entry.NameChosung) ? null : entry.NameChosung, text, query);
if (num3 > 0)
{
return num3 + entry.Score;
}
int num4 = FuzzyMatch(query, text);
if (num4 > 0)
{
return num4 + entry.Score;
}
return 0;
}
internal static int CalculateScore(string query, string target, int baseScore)
{
if (query.Length == 0)
{
return 0;
}
if (target == query)
{
return 1000 + baseScore;
}
if (target.StartsWith(query))
{
return 800 + baseScore;
}
if (target.Contains(query))
{
return 600 + baseScore;
}
int num = JamoContainsScore(target, query);
if (num > 0)
{
return num + baseScore;
}
int num2 = ChosungMatchScore(target, query);
if (num2 > 0)
{
return num2 + baseScore;
}
int num3 = FuzzyMatch(query, target);
if (num3 > 0)
{
return num3 + baseScore;
}
return 0;
}
internal static int FuzzyMatch(string query, string target)
{
int num = 0;
int num2 = 0;
int num3 = 0;
int num4 = -1;
while (num < query.Length && num2 < target.Length)
{
if (query[num] == target[num2])
{
num3 = ((num4 != num2 - 1) ? (num3 + 10) : (num3 + 30));
if (num2 == 0)
{
num3 += 15;
}
num4 = num2;
num++;
}
num2++;
}
return (num == query.Length) ? Math.Max(num3, 50) : 0;
}
internal static string DecomposeToJamo(string text)
{
StringBuilder stringBuilder = new StringBuilder(text.Length * 3);
foreach (char c in text)
{
if (c >= '가' && c <= '힣')
{
int num = c - 44032;
int num2 = num / 588;
int num3 = num % 588 / 28;
int num4 = num % 28;
stringBuilder.Append(Chosungs[num2]);
stringBuilder.Append(Jungsungs[num3]);
if (num4 > 0)
{
stringBuilder.Append(Jongsungs[num4]);
}
}
else
{
stringBuilder.Append(c);
}
}
return stringBuilder.ToString();
}
internal static char GetChosung(char hangul)
{
if (hangul < '가' || hangul > '힣')
{
return '\0';
}
int num = hangul - 44032;
return Chosungs[num / 588];
}
internal static int JamoContainsScore(string target, string query)
{
if (!HasKorean(query))
{
return 0;
}
string text = DecomposeToJamo(target);
string text2 = DecomposeToJamo(query);
if (text2.Length == 0 || text.Length == 0)
{
return 0;
}
if (text.Contains(text2))
{
return (text.IndexOf(text2) == 0) ? 580 : 550;
}
int num = 0;
for (int i = 0; i < text.Length; i++)
{
if (num >= text2.Length)
{
break;
}
if (text2[num] == text[i])
{
num++;
}
}
if (num == text2.Length)
{
return 400;
}
return 0;
}
internal static bool HasChosung(string text)
{
return text.Any((char c) => ChosungSet.Contains(c));
}
internal static bool IsChosung(string text)
{
return text.Length > 0 && text.All((char c) => ChosungSet.Contains(c));
}
private static bool HasKorean(string text)
{
return text.Any((char c) => c >= '가' && c <= '힣');
}
internal static int ChosungMatchScore(string target, string query)
{
if (!HasChosung(query))
{
return 0;
}
List<char> list = new List<char>();
List<char> list2 = new List<char>();
foreach (char c in target)
{
char chosung = GetChosung(c);
if (chosung != 0)
{
list.Add(chosung);
list2.Add(c);
}
else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
{
list.Add(c);
list2.Add(c);
}
}
if (list.Count == 0)
{
return 0;
}
if (IsChosung(query))
{
if (ContainsChosungConsecutive(list, query))
{
return 520;
}
if (ContainsChosungSubsequence(list, query))
{
return 480;
}
return 0;
}
return MixedChosungMatch(list2, list, query);
}
private static bool ContainsChosungConsecutive(List<char> targetChosungs, string query)
{
for (int i = 0; i <= targetChosungs.Count - query.Length; i++)
{
bool flag = true;
for (int j = 0; j < query.Length; j++)
{
if (targetChosungs[i + j] != query[j])
{
flag = false;
break;
}
}
if (flag)
{
return true;
}
}
return false;
}
private static bool ContainsChosungSubsequence(List<char> targetChosungs, string query)
{
int num = 0;
for (int i = 0; i < targetChosungs.Count; i++)
{
if (num >= query.Length)
{
break;
}
if (targetChosungs[i] == query[num])
{
num++;
}
}
return num == query.Length;
}
private static int MixedChosungMatch(List<char> targetChars, List<char> targetChosungs, string query)
{
int num = 0;
int num2 = 0;
while (num < query.Length && num2 < targetChars.Count)
{
char c = query[num];
if (ChosungSet.Contains(c))
{
if (targetChosungs[num2] == c)
{
num++;
}
}
else if (targetChars[num2] == c)
{
num++;
}
num2++;
}
return (num == query.Length) ? 460 : 0;
}
private static int JamoContainsScoreFast(string targetJamo, string query)
{
if (!HasKorean(query))
{
return 0;
}
string text = DecomposeToJamo(query);
if (text.Length == 0 || targetJamo.Length == 0)
{
return 0;
}
if (targetJamo.Contains(text))
{
return (targetJamo.IndexOf(text, StringComparison.Ordinal) == 0) ? 580 : 550;
}
int num = 0;
for (int i = 0; i < targetJamo.Length; i++)
{
if (num >= text.Length)
{
break;
}
if (text[num] == targetJamo[i])
{
num++;
}
}
return (num == text.Length) ? 400 : 0;
}
private static int ChosungMatchScoreFast(string? targetChosung, string targetLower, string query)
{
if (!HasChosung(query))
{
return 0;
}
if (IsChosung(query))
{
if (string.IsNullOrEmpty(targetChosung))
{
return 0;
}
if (targetChosung.Contains(query, StringComparison.Ordinal))
{
return 520;
}
int num = 0;
for (int i = 0; i < targetChosung.Length; i++)
{
if (num >= query.Length)
{
break;
}
if (targetChosung[i] == query[num])
{
num++;
}
}
if (num == query.Length)
{
return 480;
}
return 0;
}
int num2 = 0;
int num3 = 0;
while (num2 < query.Length && num3 < targetLower.Length)
{
char c = query[num2];
char c2 = targetLower[num3];
if (ChosungSet.Contains(c))
{
char c3 = GetChosung(c2);
if (c3 == '\0' && ((c2 >= 'a' && c2 <= 'z') || (c2 >= '0' && c2 <= '9')))
{
c3 = c2;
}
if (c3 == c)
{
num2++;
}
}
else if (c2 == c)
{
num2++;
}
num3++;
}
return (num2 == query.Length) ? 460 : 0;
}
internal static bool ContainsChosung(string target, string chosungQuery)
{
List<char> list = (from c in target.Select(GetChosung)
where c != '\0'
select c).ToList();
if (list.Count < chosungQuery.Length)
{
return false;
}
return ContainsChosungConsecutive(list, chosungQuery) || ContainsChosungSubsequence(list, chosungQuery);
}
}

View File

@@ -0,0 +1,5 @@
using AxCopilot.Services;
namespace AxCopilot.Core;
public record FuzzyResult(IndexEntry Entry, int Score);

View File

@@ -0,0 +1,3 @@
namespace AxCopilot.Core;
public record struct HotkeyDefinition(int VkCode, bool Ctrl, bool Alt, bool Shift, bool Win);

View File

@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
namespace AxCopilot.Core;
public static class HotkeyParser
{
private static readonly Dictionary<string, int> _keyMap;
static HotkeyParser()
{
_keyMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["Space"] = 32,
["Enter"] = 13,
["Return"] = 13,
["Tab"] = 9,
["Esc"] = 27,
["Escape"] = 27,
["Backspace"] = 8,
["Back"] = 8,
["Delete"] = 46,
["Del"] = 46,
["Insert"] = 45,
["Ins"] = 45,
["Home"] = 36,
["End"] = 35,
["PageUp"] = 33,
["PgUp"] = 33,
["PageDown"] = 34,
["PgDn"] = 34,
["PrintScreen"] = 44,
["PrtSc"] = 44,
["Snapshot"] = 44,
["Pause"] = 19,
["Break"] = 19,
["ScrollLock"] = 145,
["Left"] = 37,
["Up"] = 38,
["Right"] = 39,
["Down"] = 40,
["`"] = 192,
["Grave"] = 192,
["-"] = 189,
["="] = 187,
["["] = 219,
["]"] = 221,
["\\"] = 220,
[";"] = 186,
["'"] = 222,
[","] = 188,
["."] = 190,
["/"] = 191
};
for (char c = 'A'; c <= 'Z'; c = (char)(c + 1))
{
_keyMap[c.ToString()] = c;
}
for (char c2 = '0'; c2 <= '9'; c2 = (char)(c2 + 1))
{
_keyMap[c2.ToString()] = c2;
}
for (int i = 1; i <= 24; i++)
{
_keyMap[$"F{i}"] = 111 + i;
}
for (int j = 0; j <= 9; j++)
{
_keyMap[$"Num{j}"] = 96 + j;
}
}
public static bool TryParse(string hotkey, out HotkeyDefinition result)
{
result = default(HotkeyDefinition);
if (string.IsNullOrWhiteSpace(hotkey))
{
return false;
}
string[] array = hotkey.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
bool ctrl = false;
bool alt = false;
bool shift = false;
bool win = false;
int? num = null;
string[] array2 = array;
foreach (string text in array2)
{
if (text.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) || text.Equals("Control", StringComparison.OrdinalIgnoreCase))
{
ctrl = true;
continue;
}
if (text.Equals("Alt", StringComparison.OrdinalIgnoreCase))
{
alt = true;
continue;
}
if (text.Equals("Shift", StringComparison.OrdinalIgnoreCase))
{
shift = true;
continue;
}
if (text.Equals("Win", StringComparison.OrdinalIgnoreCase) || text.Equals("Windows", StringComparison.OrdinalIgnoreCase))
{
win = true;
continue;
}
if (_keyMap.TryGetValue(text, out var value))
{
num = value;
continue;
}
return false;
}
if (!num.HasValue)
{
return false;
}
result = new HotkeyDefinition(num.Value, ctrl, alt, shift, win);
return true;
}
public static string Format(HotkeyDefinition def)
{
List<string> list = new List<string>(5);
if (def.Ctrl)
{
list.Add("Ctrl");
}
if (def.Alt)
{
list.Add("Alt");
}
if (def.Shift)
{
list.Add("Shift");
}
if (def.Win)
{
list.Add("Win");
}
list.Add(VkToName(def.VkCode));
return string.Join("+", list);
}
private static string VkToName(int vk)
{
if (vk >= 65 && vk <= 90)
{
return ((char)vk).ToString();
}
if (vk >= 48 && vk <= 57)
{
return ((char)vk).ToString();
}
if (vk >= 112 && vk <= 135)
{
return $"F{vk - 111}";
}
if (vk >= 96 && vk <= 105)
{
return $"Num{vk - 96}";
}
string text = null;
foreach (var (text3, num2) in _keyMap)
{
if (num2 == vk && (text == null || text3.Length > text.Length))
{
text = text3;
}
}
return text ?? $"0x{vk:X2}";
}
}

View File

@@ -0,0 +1,249 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class InputListener : IDisposable
{
private delegate nint LowLevelKeyboardProc(int nCode, nint wParam, nint lParam);
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 256;
private const int WM_SYSKEYDOWN = 260;
private const int WM_KEYUP = 257;
private const int WM_SYSKEYUP = 261;
private const int VK_SHIFT = 16;
private const int VK_CONTROL = 17;
private const int VK_MENU = 18;
private const int VK_LWIN = 91;
private const int VK_RWIN = 92;
private nint _hookHandle = IntPtr.Zero;
private LowLevelKeyboardProc? _proc;
private int _retryCount = 0;
private const int MaxRetry = 3;
private volatile bool _suppressNextAltUp;
private volatile bool _suppressNextKeyUp;
private volatile int _suppressKeyUpVk;
private HotkeyDefinition _hotkey = new HotkeyDefinition(32, Ctrl: false, Alt: true, Shift: false, Win: false);
private HotkeyDefinition _captureHotkey;
private bool _captureHotkeyEnabled;
public bool SuspendHotkey { get; set; }
public Func<int, bool>? KeyFilter { get; set; }
public event EventHandler? HotkeyTriggered;
public event EventHandler? CaptureHotkeyTriggered;
public event EventHandler? HookFailed;
public void UpdateHotkey(string hotkeyStr)
{
if (HotkeyParser.TryParse(hotkeyStr, out var result))
{
_hotkey = result;
LogService.Info("핫키 변경: " + hotkeyStr);
}
else
{
LogService.Warn("핫키 파싱 실패: '" + hotkeyStr + "' — 기존 핫키 유지");
}
}
public void UpdateCaptureHotkey(string hotkeyStr, bool enabled)
{
_captureHotkeyEnabled = enabled;
if (enabled && HotkeyParser.TryParse(hotkeyStr, out var result))
{
_captureHotkey = result;
LogService.Info("캡처 단축키 활성화: " + hotkeyStr);
}
else if (!enabled)
{
LogService.Info("캡처 단축키 비활성화");
}
}
public void Start()
{
_proc = HookCallback;
Register();
}
private void Register()
{
using Process process = Process.GetCurrentProcess();
using ProcessModule processModule = process.MainModule;
_hookHandle = SetWindowsHookEx(13, _proc, GetModuleHandle(processModule.ModuleName), 0u);
if (_hookHandle == IntPtr.Zero)
{
int lastWin32Error = Marshal.GetLastWin32Error();
LogService.Error($"Global Hook 등록 실패 (에러 코드: {lastWin32Error})");
TryRetryRegister();
}
else
{
_retryCount = 0;
LogService.Info("Global Keyboard Hook 등록 완료 (" + HotkeyParser.Format(_hotkey) + ")");
}
}
private void TryRetryRegister()
{
if (_retryCount < 3)
{
_retryCount++;
LogService.Warn($"Hook 재등록 시도 {_retryCount}/{3}");
Task.Delay(1000).ContinueWith(delegate
{
Register();
});
}
else
{
LogService.Error("Hook 재등록 최대 횟수 초과");
this.HookFailed?.Invoke(this, EventArgs.Empty);
}
}
private static bool IsSuppressedForegroundWindow()
{
nint foregroundWindow = GetForegroundWindow();
if (foregroundWindow == IntPtr.Zero)
{
return false;
}
StringBuilder stringBuilder = new StringBuilder(64);
GetClassName(foregroundWindow, stringBuilder, 64);
string text = stringBuilder.ToString();
return text == "#32770" || text == "SunAwtDialog";
}
private nint HookCallback(int nCode, nint wParam, nint lParam)
{
if (nCode < 0)
{
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
int num = Marshal.ReadInt32(lParam);
if (wParam == 257 || wParam == 261)
{
if (_suppressNextAltUp && num == 18)
{
_suppressNextAltUp = false;
return 1;
}
if (_suppressNextKeyUp && num == _suppressKeyUpVk)
{
_suppressNextKeyUp = false;
return 1;
}
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
if (wParam != 256 && wParam != 260)
{
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
if (IsSuppressedForegroundWindow())
{
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
if (!SuspendHotkey && num == _hotkey.VkCode)
{
bool flag = !_hotkey.Ctrl || (GetAsyncKeyState(17) & 0x8000) != 0;
bool flag2 = !_hotkey.Alt || (GetAsyncKeyState(18) & 0x8000) != 0;
bool flag3 = !_hotkey.Shift || (GetAsyncKeyState(16) & 0x8000) != 0;
bool flag4 = !_hotkey.Win || (GetAsyncKeyState(91) & 0x8000) != 0 || (GetAsyncKeyState(92) & 0x8000) != 0;
if (flag && flag2 && flag3 && flag4)
{
this.HotkeyTriggered?.Invoke(this, EventArgs.Empty);
_suppressNextKeyUp = true;
_suppressKeyUpVk = num;
if (_hotkey.Alt)
{
_suppressNextAltUp = true;
}
return 1;
}
}
if (!SuspendHotkey && _captureHotkeyEnabled && num == _captureHotkey.VkCode)
{
bool flag5 = !_captureHotkey.Ctrl || (GetAsyncKeyState(17) & 0x8000) != 0;
bool flag6 = !_captureHotkey.Alt || (GetAsyncKeyState(18) & 0x8000) != 0;
bool flag7 = !_captureHotkey.Shift || (GetAsyncKeyState(16) & 0x8000) != 0;
bool flag8 = !_captureHotkey.Win || (GetAsyncKeyState(91) & 0x8000) != 0 || (GetAsyncKeyState(92) & 0x8000) != 0;
if (flag5 && flag6 && flag7 && flag8)
{
this.CaptureHotkeyTriggered?.Invoke(this, EventArgs.Empty);
_suppressNextKeyUp = true;
_suppressKeyUpVk = num;
if (_captureHotkey.Alt)
{
_suppressNextAltUp = true;
}
return 1;
}
}
Func<int, bool>? keyFilter = KeyFilter;
if (keyFilter != null && keyFilter(num))
{
return 1;
}
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
public void Dispose()
{
if (_hookHandle != IntPtr.Zero)
{
UnhookWindowsHookEx(_hookHandle);
_hookHandle = IntPtr.Zero;
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern nint SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, nint hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(nint hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern nint CallNextHookEx(nint hhk, int nCode, nint wParam, nint lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern nint GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll")]
private static extern nint GetForegroundWindow();
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetClassName(nint hWnd, StringBuilder lpClassName, int nMaxCount);
}

View File

@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using AxCopilot.Handlers;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class PluginHost
{
private readonly SettingsService _settings;
private readonly CommandResolver _resolver;
private readonly List<IActionHandler> _loadedPlugins = new List<IActionHandler>();
public IReadOnlyList<IActionHandler> LoadedPlugins => _loadedPlugins;
public PluginHost(SettingsService settings, CommandResolver resolver)
{
_settings = settings;
_resolver = resolver;
}
public void LoadAll()
{
_loadedPlugins.Clear();
foreach (PluginEntry item in _settings.Settings.Plugins.Where((PluginEntry p) => p.Enabled))
{
LoadPlugin(item.Path);
}
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills");
if (!Directory.Exists(path))
{
return;
}
foreach (string item2 in Directory.EnumerateFiles(path, "*.skill.json"))
{
LoadJsonSkill(item2);
}
}
private void LoadPlugin(string dllPath)
{
if (!File.Exists(dllPath))
{
LogService.Warn("플러그인 파일 없음: " + dllPath);
return;
}
try
{
Assembly assembly = Assembly.LoadFrom(dllPath);
IEnumerable<Type> enumerable = from t in assembly.GetExportedTypes()
where typeof(IActionHandler).IsAssignableFrom(t) && !t.IsAbstract
select t;
foreach (Type item in enumerable)
{
if (Activator.CreateInstance(item) is IActionHandler actionHandler)
{
_resolver.RegisterHandler(actionHandler);
_loadedPlugins.Add(actionHandler);
LogService.Info("플러그인 로드: " + actionHandler.Metadata.Name + " v" + actionHandler.Metadata.Version);
}
}
}
catch (Exception ex)
{
LogService.Error("플러그인 로드 실패 (" + dllPath + "): " + ex.Message);
}
}
private void LoadJsonSkill(string skillPath)
{
try
{
IActionHandler actionHandler = JsonSkillLoader.Load(skillPath);
if (actionHandler != null)
{
_resolver.RegisterHandler(actionHandler);
_loadedPlugins.Add(actionHandler);
LogService.Info("JSON 스킬 로드: " + actionHandler.Metadata.Name);
}
}
catch (Exception ex)
{
LogService.Error("JSON 스킬 로드 실패 (" + skillPath + "): " + ex.Message);
}
}
public void Reload()
{
LogService.Info("플러그인 전체 재로드 시작");
LoadAll();
}
public int InstallFromZip(string zipPath)
{
if (!File.Exists(zipPath))
{
LogService.Warn("플러그인 zip 파일 없음: " + zipPath);
return 0;
}
string text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "plugins");
Directory.CreateDirectory(text);
int num = 0;
try
{
using ZipArchive zipArchive = ZipFile.OpenRead(zipPath);
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(zipPath);
string text2 = Path.Combine(text, fileNameWithoutExtension);
Directory.CreateDirectory(text2);
foreach (ZipArchiveEntry entry in zipArchive.Entries)
{
if (!string.IsNullOrEmpty(entry.Name))
{
string text3 = Path.Combine(text2, entry.Name);
if (!Path.GetFullPath(text3).StartsWith(Path.GetFullPath(text2)))
{
LogService.Warn("플러그인 zip 경로 위험: " + entry.FullName);
}
else
{
entry.ExtractToFile(text3, overwrite: true);
}
}
}
foreach (string dllFile in Directory.EnumerateFiles(text2, "*.dll"))
{
if (!_settings.Settings.Plugins.Any((PluginEntry p) => p.Path == dllFile))
{
_settings.Settings.Plugins.Add(new PluginEntry
{
Enabled = true,
Path = dllFile
});
LoadPlugin(dllFile);
num++;
}
}
string text4 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills");
Directory.CreateDirectory(text4);
foreach (string item in Directory.EnumerateFiles(text2, "*.skill.json"))
{
string text5 = Path.Combine(text4, Path.GetFileName(item));
File.Copy(item, text5, overwrite: true);
LoadJsonSkill(text5);
num++;
}
if (num > 0)
{
_settings.Save();
}
LogService.Info($"플러그인 설치 완료: {zipPath} → {num}개 핸들러");
}
catch (Exception ex)
{
LogService.Error("플러그인 zip 설치 실패: " + ex.Message);
}
return num;
}
public bool UninstallPlugin(string dllPath)
{
try
{
PluginEntry pluginEntry = _settings.Settings.Plugins.FirstOrDefault((PluginEntry p) => p.Path == dllPath);
if (pluginEntry != null)
{
_settings.Settings.Plugins.Remove(pluginEntry);
_settings.Save();
}
string directoryName = Path.GetDirectoryName(dllPath);
if (directoryName != null && Directory.Exists(directoryName))
{
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "plugins");
if (Path.GetFullPath(directoryName).StartsWith(Path.GetFullPath(path)))
{
Directory.Delete(directoryName, recursive: true);
}
}
LogService.Info("플러그인 제거 완료: " + dllPath);
return true;
}
catch (Exception ex)
{
LogService.Error("플러그인 제거 실패: " + ex.Message);
return false;
}
}
}

View File

@@ -0,0 +1,3 @@
namespace AxCopilot.Core;
public record RestoreResult(bool Success, string Message);

View File

@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class SnippetExpander
{
private struct INPUT
{
public int type;
public InputUnion u;
}
[StructLayout(LayoutKind.Explicit)]
private struct InputUnion
{
[FieldOffset(0)]
public MOUSEINPUT mi;
[FieldOffset(0)]
public KEYBDINPUT ki;
[FieldOffset(0)]
public HARDWAREINPUT hi;
}
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public nint dwExtraInfo;
}
private struct MOUSEINPUT
{
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public nint dwExtraInfo;
}
private struct HARDWAREINPUT
{
public uint uMsg;
public ushort wParamL;
public ushort wParamH;
}
private readonly SettingsService _settings;
private readonly StringBuilder _buffer = new StringBuilder();
private bool _tracking;
private const ushort VK_BACK = 8;
private const int VK_ESCAPE = 27;
private const int VK_SPACE = 32;
private const int VK_RETURN = 13;
private const int VK_OEM_1 = 186;
private const int VK_SHIFT = 16;
private const int VK_CONTROL = 17;
private const int VK_MENU = 18;
private const ushort VK_CTRL_US = 17;
private static readonly HashSet<int> ClearKeys = new HashSet<int> { 33, 34, 35, 36, 37, 38, 39, 40, 46 };
public SnippetExpander(SettingsService settings)
{
_settings = settings;
}
public bool HandleKey(int vkCode)
{
if (!_settings.Settings.Launcher.SnippetAutoExpand)
{
return false;
}
if ((GetAsyncKeyState(17) & 0x8000) != 0)
{
_tracking = false;
_buffer.Clear();
return false;
}
if ((GetAsyncKeyState(18) & 0x8000) != 0)
{
_tracking = false;
_buffer.Clear();
return false;
}
if (vkCode == 186 && (GetAsyncKeyState(16) & 0x8000) == 0)
{
_tracking = true;
_buffer.Clear();
_buffer.Append(';');
return false;
}
if (!_tracking)
{
return false;
}
if ((vkCode >= 65 && vkCode <= 90) || (vkCode >= 48 && vkCode <= 57) || (vkCode >= 96 && vkCode <= 105) || vkCode == 189)
{
bool shifted = (GetAsyncKeyState(16) & 0x8000) != 0;
char c = VkToChar(vkCode, shifted);
if (c != 0)
{
_buffer.Append(char.ToLowerInvariant(c));
}
return false;
}
switch (vkCode)
{
case 8:
if (_buffer.Length > 1)
{
_buffer.Remove(_buffer.Length - 1, 1);
}
else
{
_tracking = false;
_buffer.Clear();
}
return false;
default:
if (vkCode != 13)
{
if (vkCode == 27 || ClearKeys.Contains(vkCode) || vkCode >= 112)
{
_tracking = false;
_buffer.Clear();
}
return false;
}
goto case 32;
case 32:
if (_buffer.Length > 1)
{
string keyword = _buffer.ToString(1, _buffer.Length - 1);
_tracking = false;
_buffer.Clear();
SnippetEntry snippetEntry = _settings.Settings.Snippets.FirstOrDefault((SnippetEntry s) => s.Key.Equals(keyword, StringComparison.OrdinalIgnoreCase));
if (snippetEntry != null)
{
string expanded = ExpandVariables(snippetEntry.Content);
int deleteCount = keyword.Length + 1;
((DispatcherObject)Application.Current).Dispatcher.BeginInvoke((Delegate)(Action)delegate
{
PasteExpansion(expanded, deleteCount);
}, Array.Empty<object>());
return true;
}
}
_tracking = false;
_buffer.Clear();
return false;
}
}
private static void PasteExpansion(string text, int deleteCount)
{
try
{
INPUT[] array = new INPUT[deleteCount * 2];
for (int i = 0; i < deleteCount; i++)
{
array[i * 2] = MakeKeyInput(8, keyUp: false);
array[i * 2 + 1] = MakeKeyInput(8, keyUp: true);
}
SendInput((uint)array.Length, array, Marshal.SizeOf<INPUT>());
Clipboard.SetText(text);
INPUT[] array2 = new INPUT[4]
{
MakeKeyInput(17, keyUp: false),
MakeKeyInput(86, keyUp: false),
MakeKeyInput(86, keyUp: true),
MakeKeyInput(17, keyUp: true)
};
SendInput((uint)array2.Length, array2, Marshal.SizeOf<INPUT>());
LogService.Info($"스니펫 확장 완료: {deleteCount}자 삭제 후 붙여넣기");
}
catch (Exception ex)
{
LogService.Warn("스니펫 확장 실패: " + ex.Message);
}
}
private static INPUT MakeKeyInput(ushort vk, bool keyUp)
{
INPUT result = new INPUT
{
type = 1
};
result.u.ki.wVk = vk;
result.u.ki.dwFlags = (keyUp ? 2u : 0u);
return result;
}
private static string ExpandVariables(string content)
{
DateTime now = DateTime.Now;
return content.Replace("{date}", now.ToString("yyyy-MM-dd")).Replace("{time}", now.ToString("HH:mm:ss")).Replace("{datetime}", now.ToString("yyyy-MM-dd HH:mm:ss"))
.Replace("{year}", now.Year.ToString())
.Replace("{month}", now.Month.ToString("D2"))
.Replace("{day}", now.Day.ToString("D2"));
}
private static char VkToChar(int vk, bool shifted)
{
if (vk >= 65 && vk <= 90)
{
return shifted ? ((char)vk) : char.ToLowerInvariant((char)vk);
}
if (vk >= 48 && vk <= 57)
{
return shifted ? ")!@#$%^&*("[vk - 48] : ((char)vk);
}
if (vk >= 96 && vk <= 105)
{
return (char)(48 + (vk - 96));
}
if (vk == 189)
{
return shifted ? '_' : '-';
}
return '\0';
}
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
}