Initial commit to new repository
This commit is contained in:
47
.decompiledproj/AxCopilot/Core/BulkObservableCollection.cs
Normal file
47
.decompiledproj/AxCopilot/Core/BulkObservableCollection.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
249
.decompiledproj/AxCopilot/Core/CommandResolver.cs
Normal file
249
.decompiledproj/AxCopilot/Core/CommandResolver.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
384
.decompiledproj/AxCopilot/Core/ContextManager.cs
Normal file
384
.decompiledproj/AxCopilot/Core/ContextManager.cs
Normal 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);
|
||||
}
|
||||
468
.decompiledproj/AxCopilot/Core/FuzzyEngine.cs
Normal file
468
.decompiledproj/AxCopilot/Core/FuzzyEngine.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
5
.decompiledproj/AxCopilot/Core/FuzzyResult.cs
Normal file
5
.decompiledproj/AxCopilot/Core/FuzzyResult.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Core;
|
||||
|
||||
public record FuzzyResult(IndexEntry Entry, int Score);
|
||||
3
.decompiledproj/AxCopilot/Core/HotkeyDefinition.cs
Normal file
3
.decompiledproj/AxCopilot/Core/HotkeyDefinition.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace AxCopilot.Core;
|
||||
|
||||
public record struct HotkeyDefinition(int VkCode, bool Ctrl, bool Alt, bool Shift, bool Win);
|
||||
175
.decompiledproj/AxCopilot/Core/HotkeyParser.cs
Normal file
175
.decompiledproj/AxCopilot/Core/HotkeyParser.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
249
.decompiledproj/AxCopilot/Core/InputListener.cs
Normal file
249
.decompiledproj/AxCopilot/Core/InputListener.cs
Normal 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);
|
||||
}
|
||||
195
.decompiledproj/AxCopilot/Core/PluginHost.cs
Normal file
195
.decompiledproj/AxCopilot/Core/PluginHost.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
.decompiledproj/AxCopilot/Core/RestoreResult.cs
Normal file
3
.decompiledproj/AxCopilot/Core/RestoreResult.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace AxCopilot.Core;
|
||||
|
||||
public record RestoreResult(bool Success, string Message);
|
||||
264
.decompiledproj/AxCopilot/Core/SnippetExpander.cs
Normal file
264
.decompiledproj/AxCopilot/Core/SnippetExpander.cs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user