using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Threading; using AxCopilot.Core; using AxCopilot.Handlers; using AxCopilot.Models; using AxCopilot.SDK; using AxCopilot.Services; namespace AxCopilot.ViewModels; public class LauncherViewModel : INotifyPropertyChanged { private sealed class FavJson { [JsonPropertyName("name")] public string Name { get; set; } = ""; [JsonPropertyName("path")] public string Path { get; set; } = ""; } private readonly CommandResolver _resolver; private readonly SettingsService _settings; private string _inputText = ""; private LauncherItem? _selectedItem; private bool _isLoading; private CancellationTokenSource? _searchCts; private Timer? _debounceTimer; private const int DebounceMs = 30; private string _lastSearchQuery = ""; private bool _isActionMode; private LauncherItem? _actionSourceItem; private string _savedQuery = ""; private readonly HashSet _mergeQueue = new HashSet(); private string _placeholderText = L10n.Get("placeholder"); private static readonly Dictionary PrefixMap = new Dictionary { { "@", ("URL", "\ue774", "#0078D4") }, { "~", ("워크", "\ue8a1", "#C50F1F") }, { ">", ("명령", "\ue756", "#323130") }, { "$", ("클립", "\ue77f", "#8764B8") }, { "cd", ("폴더", "\ue8b7", "#107C10") }, { "#", ("히스", "\ue81c", "#B7791F") }, { ";", ("스닛", "\ue70b", "#0F6CBD") }, { "=", ("계산", "\ue8ef", "#4B5EFC") }, { "!", ("AI", "\ue8bd", "#8B2FC9") }, { "?", ("검색", "\ue774", "#006EAF") }, { "/", ("시스템", "\ue7e8", "#4A4A4A") }, { "kill ", ("킬", "\uea39", "#CC2222") }, { "media ", ("미디어", "\ue768", "#1A6B3C") }, { "info ", ("시스템", "\ue7f4", "#5B4E7E") }, { "port", ("포트", "\ue968", "#006699") }, { "emoji", ("이모지", "\ue76e", "#F59E0B") }, { "color", ("색상", "\ue771", "#EC4899") }, { "recent", ("최근", "\ue81c", "#059669") }, { "note", ("메모", "\ue70b", "#7C3AED") }, { "uninstall", ("제거", "\ue74d", "#DC2626") }, { "env", ("환경변수", "\ue8d7", "#0D9488") }, { "json", ("JSON", "\ue930", "#D97706") }, { "encode ", ("인코딩", "\ue8cb", "#6366F1") }, { "snap", ("스냅", "\ue8a0", "#B45309") }, { "cap", ("캡처", "\ue722", "#BE185D") }, { "help", ("도움말", "\ue946", "#6B7280") } }; public BulkObservableCollection Results { get; } = new BulkObservableCollection(); public string InputText { get { return _inputText; } set { if (_inputText == value) { return; } _inputText = value; OnPropertyChanged("InputText"); OnPropertyChanged("HasActivePrefix"); OnPropertyChanged("ActivePrefixLabel"); OnPropertyChanged("ActivePrefixSymbol"); OnPropertyChanged("ActivePrefixBrush"); OnPropertyChanged("IsClipboardMode"); OnPropertyChanged("ShowMergeHint"); OnPropertyChanged("MergeHintText"); _searchCts?.Cancel(); _debounceTimer?.Dispose(); if (string.IsNullOrWhiteSpace(value)) { Results.Clear(); return; } string captured = value; _debounceTimer = new Timer(delegate(object? _) { Application current = Application.Current; if (current != null) { ((DispatcherObject)current).Dispatcher.InvokeAsync((Func)(() => _ = SearchAsync(captured))); } }, null, 30, -1); } } public LauncherItem? SelectedItem { get { return _selectedItem; } set { _selectedItem = value; OnPropertyChanged("SelectedItem"); } } public bool IsLoading { get { return _isLoading; } set { _isLoading = value; OnPropertyChanged("IsLoading"); } } public string PlaceholderText { get { return _placeholderText; } private set { _placeholderText = value; OnPropertyChanged("PlaceholderText"); } } public bool EnableIconAnimation => _settings.Settings.Launcher.EnableIconAnimation; public bool EnableRainbowGlow => _settings.Settings.Launcher.EnableRainbowGlow; public bool EnableSelectionGlow => _settings.Settings.Launcher.EnableSelectionGlow; public bool ShowLauncherBorder => _settings.Settings.Launcher.ShowLauncherBorder; public string ThemeSetting => _settings.Settings.Launcher.Theme; public CustomThemeColors? CustomThemeColors => _settings.Settings.Launcher.CustomTheme; public string WindowPosition => _settings.Settings.Launcher.Position; public bool ShowNumberBadges => _settings.Settings.Launcher.ShowNumberBadges; public bool CloseOnFocusLost => _settings.Settings.Launcher.CloseOnFocusLost; public bool EnableActionMode => _settings.Settings.Launcher.EnableActionMode; public bool HasActivePrefix => _settings.Settings.Launcher.ShowPrefixBadge && _inputText.Length > 0 && PrefixMap.Keys.Any((string k) => (k != "!" || IsAiEnabled()) && _inputText.StartsWith(k, StringComparison.OrdinalIgnoreCase)); public string? ActivePrefix => PrefixMap.Keys.FirstOrDefault((string k) => (k != "!" || IsAiEnabled()) && _inputText.StartsWith(k, StringComparison.OrdinalIgnoreCase)); public string ActivePrefixLabel { get { object result; if (ActivePrefix != null && PrefixMap.TryGetValue(ActivePrefix, out (string, string, string) value)) { (result, _, _) = value; } else { result = ""; } return (string)result; } } public string ActivePrefixSymbol { get { (string, string, string) value; return (ActivePrefix != null && PrefixMap.TryGetValue(ActivePrefix, out value)) ? value.Item2 : "\ue721"; } } public SolidColorBrush ActivePrefixBrush { get { if (ActivePrefix != null && PrefixMap.TryGetValue(ActivePrefix, out (string, string, string) value)) { Color color = (Color)ColorConverter.ConvertFromString(value.Item3); return new SolidColorBrush(color); } return new SolidColorBrush(Color.FromRgb(75, 94, 252)); } } public bool IsActionMode { get { return _isActionMode; } private set { _isActionMode = value; OnPropertyChanged("IsActionMode"); OnPropertyChanged("ShowActionModeBar"); OnPropertyChanged("ActionModeBreadcrumb"); } } public bool ShowActionModeBar => IsActionMode; public string ActionModeBreadcrumb => _actionSourceItem?.Title ?? ""; public bool IsClipboardMode => _inputText.StartsWith("#", StringComparison.Ordinal); public int MergeCount => _mergeQueue.Count; public bool ShowMergeHint => _mergeQueue.Count > 0 && IsClipboardMode; public string MergeHintText => (_mergeQueue.Count > 0) ? $"{_mergeQueue.Count}개 선택됨 · Shift+Enter로 합치기 · Esc로 취소" : ""; public event EventHandler? CloseRequested; public event EventHandler? NotificationRequested; public event PropertyChangedEventHandler? PropertyChanged; public void RefreshPlaceholder() { PlaceholderText = (_settings.Settings.Launcher.EnableRandomPlaceholder ? L10n.GetRandomPlaceholder() : L10n.Get("placeholder")); } private static bool IsAiEnabled() { return (Application.Current as App)?.SettingsService?.Settings.AiEnabled ?? true; } public bool CanEnterActionMode() { return !IsActionMode && SelectedItem?.Data is IndexEntry; } public bool IsItemMarkedForMerge(LauncherItem item) { return item.Data is ClipboardEntry item2 && _mergeQueue.Contains(item2); } public LauncherViewModel(CommandResolver resolver, SettingsService settings) { _resolver = resolver; _settings = settings; } public void OnShown() { if (IsActionMode) { IsActionMode = false; _actionSourceItem = null; } Results.Clear(); _lastSearchQuery = ""; ClearMerge(); } internal Task TriggerImeSearchAsync(string text) { string text2 = text.Trim().ToLowerInvariant(); if (text2 == _lastSearchQuery && Results.Count > 0) { return Task.CompletedTask; } return SearchAsync(text); } private async Task SearchAsync(string query) { _searchCts = new CancellationTokenSource(); CancellationToken ct = _searchCts.Token; string queryKey = query.Trim().ToLowerInvariant(); bool isSameQuery = queryKey == _lastSearchQuery && Results.Count > 0; if (!isSameQuery) { Results.Clear(); } if (string.IsNullOrWhiteSpace(query)) { _lastSearchQuery = ""; } else { if ((!_settings.Settings.Launcher.EnableFavorites && query.StartsWith("fav", StringComparison.OrdinalIgnoreCase)) || (!_settings.Settings.Launcher.EnableRecent && query.StartsWith("recent", StringComparison.OrdinalIgnoreCase))) { return; } LauncherItem prevSelected = (isSameQuery ? SelectedItem : null); IsLoading = true; try { IEnumerable items = await _resolver.ResolveAsync(query, ct); if (ct.IsCancellationRequested) { return; } Results.ReplaceAll(items); _lastSearchQuery = queryKey; if (isSameQuery && prevSelected != null) { LauncherItem restored = Results.FirstOrDefault((LauncherItem r) => r.Data == prevSelected.Data || r.Title == prevSelected.Title); SelectedItem = restored ?? Results.FirstOrDefault(); } else { SelectedItem = Results.FirstOrDefault(); } } catch (OperationCanceledException) { } catch (Exception ex2) { Exception ex3 = ex2; LogService.Error("검색 오류: " + ex3.Message); } finally { if (!ct.IsCancellationRequested) { IsLoading = false; } } } } public async Task ExecuteSelectedAsync() { FileActionData fileAction = default(FileActionData); int num; if (IsActionMode) { object obj = SelectedItem?.Data; fileAction = obj as FileActionData; num = (((object)fileAction != null) ? 1 : 0); } else { num = 0; } if (num != 0) { ExecuteFileAction(fileAction); ExitActionMode(); this.CloseRequested?.Invoke(this, EventArgs.Empty); } else if (!(SelectedItem == null)) { this.CloseRequested?.Invoke(this, EventArgs.Empty); try { await _resolver.ExecuteAsync(SelectedItem, InputText, CancellationToken.None); } catch (Exception ex) { this.NotificationRequested?.Invoke(this, "실행 실패: " + ex.Message); LogService.Error("Execute 오류: " + ex.Message); } } } public bool ShowDelayTimerItems() { if (!(SelectedItem?.Data is string text)) { return false; } if (text.StartsWith("delay:")) { return false; } if (!_resolver.RegisteredHandlers.TryGetValue(ActivePrefix ?? "", out IActionHandler value) || !(value is ScreenCaptureHandler screenCaptureHandler)) { return false; } List list = screenCaptureHandler.GetDelayItems(text).ToList(); Results.Clear(); foreach (LauncherItem item in list) { Results.Add(item); } SelectedItem = Results.FirstOrDefault(); return true; } public void SelectNext() { if (Results.Count != 0) { int num = ((SelectedItem != null) ? Results.IndexOf(SelectedItem) : (-1)); SelectedItem = Results[(num + 1) % Results.Count]; } } public void SelectPrev() { if (Results.Count != 0) { int num = ((SelectedItem != null) ? Results.IndexOf(SelectedItem) : 0); SelectedItem = Results[(num - 1 + Results.Count) % Results.Count]; } } public string GetLargeTypeText() { if (SelectedItem == null) { return ""; } if (SelectedItem.Data is string text && !string.IsNullOrWhiteSpace(text)) { return text; } if (SelectedItem.Data is ClipboardEntry { IsText: not false } clipboardEntry) { return clipboardEntry.Text; } return SelectedItem.Title; } public void EnterActionMode(LauncherItem item) { if (_settings.Settings.Launcher.EnableActionMode && item.Data is IndexEntry indexEntry) { _actionSourceItem = item; _savedQuery = _inputText; IsActionMode = true; string text = Environment.ExpandEnvironmentVariables(indexEntry.Path); bool flag = Directory.Exists(text); string fileName = Path.GetFileName(text); Results.Clear(); Results.Add(MakeAction("경로 복사", text, FileAction.CopyPath, "\ue77f", "#8764B8")); Results.Add(MakeAction("전체 경로 복사", text, FileAction.CopyFullPath, "\ue77f", "#C55A11")); Results.Add(MakeAction("파일 탐색기에서 열기", "Explorer에서 위치 선택됨으로 표시", FileAction.OpenExplorer, "\ue8b7", "#107C10")); if (!flag) { Results.Add(MakeAction("관리자 권한으로 실행", "UAC 권한 상승 후 실행", FileAction.RunAsAdmin, "\ue72e", "#C50F1F")); } Results.Add(MakeAction("터미널에서 열기", flag ? text : (Path.GetDirectoryName(text) ?? text), FileAction.OpenTerminal, "\ue756", "#323130")); if (!flag) { Results.Add(MakeAction("파일 속성 보기", "Windows 속성 대화 상자 열기", FileAction.ShowProperties, "\ue946", "#6B2C91")); } Results.Add(MakeAction("이름 바꾸기", fileName, FileAction.Rename, "\ue8ac", "#D97706")); Results.Add(MakeAction("휴지통으로 삭제", "복구 가능한 삭제 · 확인 후 실행", FileAction.DeleteToRecycleBin, "\ue74d", "#C50F1F")); SelectedItem = Results.FirstOrDefault(); } static LauncherItem MakeAction(string title, string subtitle, FileAction action, string symbol, string colorHex) { FileActionData data = new FileActionData(subtitle, action); return new LauncherItem(title, subtitle, null, data, null, symbol); } } public void ExitActionMode() { IsActionMode = false; _actionSourceItem = null; string savedQuery = _savedQuery; _savedQuery = ""; SearchAsync(savedQuery); } private static void ExecuteFileAction(FileActionData data) { string path = data.Path; switch (data.Action) { case FileAction.CopyPath: ((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate { Clipboard.SetText(path); }); break; case FileAction.OpenExplorer: if (File.Exists(path)) { Process.Start("explorer.exe", "/select,\"" + path + "\""); } else { Process.Start("explorer.exe", "\"" + path + "\""); } break; case FileAction.RunAsAdmin: try { Process.Start(new ProcessStartInfo(path) { UseShellExecute = true, Verb = "runas" }); break; } catch (Exception ex2) { LogService.Warn("관리자 실행 취소: " + ex2.Message); break; } case FileAction.CopyFullPath: ((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate { Clipboard.SetText(path); }); break; case FileAction.ShowProperties: try { ProcessStartInfo startInfo = new ProcessStartInfo("explorer.exe") { Arguments = "/select,\"" + path + "\"", UseShellExecute = true }; Process.Start(startInfo); ProcessStartInfo processStartInfo = new ProcessStartInfo { FileName = "rundll32.exe", Arguments = "shell32.dll,ShellExec_RunDLL \"properties\" \"" + path + "\"", UseShellExecute = false }; try { Process.Start(new ProcessStartInfo(path) { UseShellExecute = true, Verb = "properties" }); break; } catch { break; } } catch (Exception ex3) { LogService.Warn("속성 열기 실패: " + ex3.Message); break; } case FileAction.Rename: break; case FileAction.DeleteToRecycleBin: break; case FileAction.OpenTerminal: { string text = (File.Exists(path) ? (Path.GetDirectoryName(path) ?? path) : path); try { Process.Start("wt.exe", "-d \"" + text + "\""); break; } catch { try { Process.Start("cmd.exe", "/k cd /d \"" + text + "\""); break; } catch (Exception ex) { LogService.Warn("터미널 열기 실패: " + ex.Message); break; } } } } } public bool CopySelectedPath() { if (SelectedItem?.Data is IndexEntry indexEntry) { string path = Path.GetFileName(Environment.ExpandEnvironmentVariables(indexEntry.Path)); ((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate { Clipboard.SetText(path); }); return true; } return false; } public bool CopySelectedFullPath() { if (SelectedItem?.Data is IndexEntry indexEntry) { string path = Environment.ExpandEnvironmentVariables(indexEntry.Path); ((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate { Clipboard.SetText(path); }); return true; } return false; } public bool OpenSelectedInExplorer() { if (SelectedItem?.Data is IndexEntry indexEntry) { string text = Environment.ExpandEnvironmentVariables(indexEntry.Path); if (File.Exists(text)) { Process.Start("explorer.exe", "/select,\"" + text + "\""); } else if (Directory.Exists(text)) { Process.Start("explorer.exe", "\"" + text + "\""); } return true; } return false; } public bool RunSelectedAsAdmin() { if (SelectedItem?.Data is IndexEntry indexEntry) { string fileName = Environment.ExpandEnvironmentVariables(indexEntry.Path); try { Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true, Verb = "runas" }); return true; } catch (Exception ex) { LogService.Warn("관리자 실행 취소: " + ex.Message); } } return false; } public bool ShowSelectedProperties() { if (SelectedItem?.Data is IndexEntry indexEntry) { string text = Environment.ExpandEnvironmentVariables(indexEntry.Path); try { Process.Start(new ProcessStartInfo(text) { UseShellExecute = true, Verb = "properties" }); return true; } catch { Process.Start("explorer.exe", "/select,\"" + text + "\""); return true; } } return false; } public bool RemoveSelectedFromRecent() { if (SelectedItem == null || Results.Count == 0) { return false; } int val = Results.IndexOf(SelectedItem); Results.Remove(SelectedItem); if (Results.Count > 0) { SelectedItem = Results[Math.Min(val, Results.Count - 1)]; } else { SelectedItem = null; } return true; } public void ClearInput() { InputText = ""; } public void SelectFirst() { if (Results.Count > 0) { SelectedItem = Results[0]; } } public void SelectLast() { if (Results.Count > 0) { BulkObservableCollection results = Results; SelectedItem = results[results.Count - 1]; } } public bool? ToggleFavorite() { if (!(SelectedItem?.Data is IndexEntry indexEntry)) { return null; } string path = Environment.ExpandEnvironmentVariables(indexEntry.Path); string text = Path.GetFileNameWithoutExtension(path); if (string.IsNullOrWhiteSpace(text)) { text = Path.GetFileName(path); } string path2 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favorites.json"); try { JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true, PropertyNameCaseInsensitive = true }; List list = new List(); if (File.Exists(path2)) { list = JsonSerializer.Deserialize>(File.ReadAllText(path2), options) ?? new List(); } FavJson favJson = list.FirstOrDefault((FavJson f) => f.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); if (favJson != null) { list.Remove(favJson); Directory.CreateDirectory(Path.GetDirectoryName(path2)); File.WriteAllText(path2, JsonSerializer.Serialize(list, options)); return false; } list.Insert(0, new FavJson { Name = text, Path = path }); Directory.CreateDirectory(Path.GetDirectoryName(path2)); File.WriteAllText(path2, JsonSerializer.Serialize(list, options)); return true; } catch (Exception ex) { LogService.Warn("즐겨찾기 토글 실패: " + ex.Message); return null; } } public bool OpenSelectedInTerminal() { string text2; if (SelectedItem?.Data is IndexEntry indexEntry) { string text = Environment.ExpandEnvironmentVariables(indexEntry.Path); text2 = (Directory.Exists(text) ? text : (Path.GetDirectoryName(text) ?? text)); } else { text2 = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); } try { Process.Start("wt.exe", "-d \"" + text2 + "\""); return true; } catch { try { Process.Start("cmd.exe", "/k cd /d \"" + text2 + "\""); return true; } catch (Exception ex) { LogService.Warn("터미널 열기 실패: " + ex.Message); return false; } } } public void NavigateToDownloads() { string text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); InputText = "cd " + text; } public void ToggleMergeItem(LauncherItem? item) { if (item?.Data is ClipboardEntry { IsText: not false } clipboardEntry) { if (!_mergeQueue.Remove(clipboardEntry)) { _mergeQueue.Add(clipboardEntry); } OnPropertyChanged("MergeCount"); OnPropertyChanged("ShowMergeHint"); OnPropertyChanged("MergeHintText"); } } public void ExecuteMerge() { if (_mergeQueue.Count == 0) { return; } List list = (from r in Results where r.Data is ClipboardEntry item && _mergeQueue.Contains(item) select ((ClipboardEntry)r.Data).Text).ToList(); if (list.Count == 0) { list = _mergeQueue.Select((ClipboardEntry e) => e.Text).ToList(); } string merged = string.Join("\n", list); try { ((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate { Clipboard.SetText(merged); }); } catch (Exception ex) { LogService.Warn("병합 클립보드 실패: " + ex.Message); } ClearMerge(); this.CloseRequested?.Invoke(this, EventArgs.Empty); LogService.Info($"클립보드 병합: {list.Count}개 항목"); } public void ClearMerge() { _mergeQueue.Clear(); OnPropertyChanged("MergeCount"); OnPropertyChanged("ShowMergeHint"); OnPropertyChanged("MergeHintText"); } protected void OnPropertyChanged([CallerMemberName] string? name = null) { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } }