using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.ComponentModel;
using System.Windows.Forms;
using AxCopilot.Models;
using AxCopilot.Services;
using AxCopilot.Themes;
using AxCopilot.Views;
namespace AxCopilot.ViewModels;
public partial class SettingsViewModel
{
// ─── 인덱스 경로 메서드 ──────────────────────────────────────────────
public void AddIndexPath(string path)
{
var trimmed = path.Trim();
if (string.IsNullOrWhiteSpace(trimmed)) return;
if (IndexPaths.Any(p => p.Equals(trimmed, StringComparison.OrdinalIgnoreCase))) return;
IndexPaths.Add(trimmed);
}
public void RemoveIndexPath(string path) => IndexPaths.Remove(path);
// ─── 인덱스 확장자 메서드 ──────────────────────────────────────────
public void AddExtension(string ext)
{
var trimmed = ext.Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(trimmed)) return;
if (!trimmed.StartsWith(".")) trimmed = "." + trimmed;
if (IndexExtensions.Any(e => e.Equals(trimmed, StringComparison.OrdinalIgnoreCase))) return;
IndexExtensions.Add(trimmed);
}
public void RemoveExtension(string ext) => IndexExtensions.Remove(ext);
public void BrowseIndexPath()
{
using var dlg = new FolderBrowserDialog
{
Description = "인덱스할 폴더 선택",
UseDescriptionForTitle = true,
ShowNewFolderButton = false
};
if (dlg.ShowDialog() != DialogResult.OK) return;
AddIndexPath(dlg.SelectedPath);
}
// ─── 빠른 실행 단축키 메서드 ──────────────────────────────────────────
public bool AddShortcut()
{
if (string.IsNullOrWhiteSpace(_newKey) || string.IsNullOrWhiteSpace(_newTarget))
return false;
// 중복 키 확인
if (AppShortcuts.Any(s => s.Key.Equals(_newKey.Trim(), StringComparison.OrdinalIgnoreCase)))
return false;
AppShortcuts.Add(new AppShortcutModel
{
Key = _newKey.Trim(),
Description = _newDescription.Trim(),
Target = _newTarget.Trim(),
Type = _newType
});
NewKey = ""; NewDescription = ""; NewTarget = ""; NewType = "app";
return true;
}
public void RemoveShortcut(AppShortcutModel shortcut) => AppShortcuts.Remove(shortcut);
/// 파일 선택 대화상자. 선택 시 NewTarget에 자동 설정.
public void BrowseTarget()
{
using var dlg = new OpenFileDialog
{
Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*",
Title = "앱 선택"
};
if (dlg.ShowDialog() != DialogResult.OK) return;
NewTarget = dlg.FileName;
NewType = "app";
if (string.IsNullOrWhiteSpace(NewDescription))
NewDescription = System.IO.Path.GetFileNameWithoutExtension(dlg.FileName);
}
public void SelectTheme(string key)
{
SelectedThemeKey = key;
ThemePreviewRequested?.Invoke(this, EventArgs.Empty);
}
public void PickColor(ColorRowModel row)
{
using var dlg = new ColorDialog { FullOpen = true };
try
{
var color = ThemeResourceHelper.HexColor(row.Hex);
dlg.Color = System.Drawing.Color.FromArgb(color.R, color.G, color.B);
}
catch (Exception) { /* 기본값 사용 */ }
if (dlg.ShowDialog() != DialogResult.OK) return;
row.Hex = $"#{dlg.Color.R:X2}{dlg.Color.G:X2}{dlg.Color.B:X2}";
if (_selectedThemeKey == "custom")
ThemePreviewRequested?.Invoke(this, EventArgs.Empty);
}
// 시스템 예약 프리픽스 (핸들러에서 이미 사용 중인 키)
private static readonly HashSet ReservedPrefixes = new(StringComparer.OrdinalIgnoreCase)
{
"=", "?", "#", "$", ";", "@", "~", ">", "!",
"emoji", "color", "recent", "note", "uninstall", "kill", "media",
"info", "*", "json", "encode", "port", "env", "snap", "help",
"pick", "date", "svc", "pipe", "journal", "routine", "batch",
"diff", "win", "stats", "fav", "rename", "monitor", "scaffold",
};
/// 설정 저장 전 프리픽스/키워드 충돌을 검사합니다. 충돌 시 메시지를 반환합니다.
public string? ValidateBeforeSave()
{
// 캡처 프리픽스 충돌 검사
var cap = string.IsNullOrWhiteSpace(_capPrefix) ? "cap" : _capPrefix.Trim();
if (cap != "cap" && ReservedPrefixes.Contains(cap))
return $"캡처 프리픽스 '{cap}'은(는) 이미 사용 중인 예약어입니다.";
// 빠른 실행 별칭 키 중복 검사
var aliasKeys = AppShortcuts.Select(s => s.Key.ToLowerInvariant()).ToList();
var duplicateAlias = aliasKeys.GroupBy(k => k).FirstOrDefault(g => g.Count() > 1);
if (duplicateAlias != null)
return $"빠른 실행 키워드 '{duplicateAlias.Key}'이(가) 중복되었습니다.";
// 빠른 실행 별칭 키 vs 예약 프리픽스 충돌
foreach (var key in aliasKeys)
{
if (ReservedPrefixes.Contains(key))
return $"빠른 실행 키워드 '{key}'은(는) 시스템 예약어와 충돌합니다.";
}
// 배치 명령 키 중복 검사
var batchKeys = BatchCommands.Select(b => b.Key.ToLowerInvariant()).ToList();
var duplicateBatch = batchKeys.GroupBy(k => k).FirstOrDefault(g => g.Count() > 1);
if (duplicateBatch != null)
return $"배치 명령 키워드 '{duplicateBatch.Key}'이(가) 중복되었습니다.";
return null; // 문제 없음
}
public void Save()
{
// 충돌 검사
var conflict = ValidateBeforeSave();
if (conflict != null)
{
CustomMessageBox.Show(
conflict,
"AX Copilot — 설정 저장 오류",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Warning);
return;
}
var s = _service.Settings;
s.Hotkey = _hotkey;
s.Launcher.MaxResults = _maxResults;
s.Launcher.Opacity = _opacity;
s.Launcher.Theme = _selectedThemeKey;
s.Launcher.Position = _launcherPosition;
s.Launcher.WebSearchEngine = _webSearchEngine;
s.Launcher.SnippetAutoExpand = _snippetAutoExpand;
s.Launcher.Language = _language;
L10n.SetLanguage(_language);
// 기능 토글 저장
s.Launcher.ShowNumberBadges = _showNumberBadges;
s.Launcher.EnableFavorites = _enableFavorites;
s.Launcher.EnableRecent = _enableRecent;
s.Launcher.EnableActionMode = _enableActionMode;
s.Launcher.CloseOnFocusLost = _closeOnFocusLost;
s.Launcher.ShowPrefixBadge = _showPrefixBadge;
s.Launcher.EnableIconAnimation = _enableIconAnimation;
s.Launcher.EnableRainbowGlow = _enableRainbowGlow;
s.Launcher.EnableSelectionGlow = _enableSelectionGlow;
s.Launcher.EnableRandomPlaceholder = _enableRandomPlaceholder;
s.Launcher.ShowLauncherBorder = _showLauncherBorder;
s.Launcher.ShortcutHelpUseThemeColor = _shortcutHelpUseThemeColor;
s.Launcher.EnableTextAction = _enableTextAction;
s.Launcher.EnableFileDialogIntegration = _enableFileDialogIntegration;
s.Launcher.EnableClipboardAutoCategory = _enableClipboardAutoCategory;
s.Launcher.MaxPinnedClipboardItems = _maxPinnedClipboardItems;
s.Launcher.TextActionTranslateLanguage = _textActionTranslateLanguage;
s.Llm.MaxSubAgents = _maxSubAgents;
s.Llm.PdfExportPath = _pdfExportPath;
s.Llm.TipDurationSeconds = _tipDurationSeconds;
// LLM 공통 설정 저장
s.Llm.Service = _llmService;
s.Llm.Streaming = _llmStreaming;
s.Llm.MaxContextTokens = _llmMaxContextTokens;
s.Llm.RetentionDays = _llmRetentionDays;
s.Llm.Temperature = _llmTemperature;
s.Llm.DefaultAgentPermission = _defaultAgentPermission;
s.Llm.DefaultOutputFormat = _defaultOutputFormat;
s.Llm.DefaultMood = _defaultMood;
s.Llm.AutoPreview = _autoPreview;
s.Llm.MaxAgentIterations = _maxAgentIterations;
s.Llm.MaxRetryOnError = _maxRetryOnError;
s.Llm.AgentLogLevel = _agentLogLevel;
s.Llm.AgentDecisionLevel = _agentDecisionLevel;
s.Llm.PlanMode = _planMode;
s.Llm.EnableMultiPassDocument = _enableMultiPassDocument;
s.Llm.EnableCoworkVerification = _enableCoworkVerification;
s.Llm.EnableFilePathHighlight = _enableFilePathHighlight;
s.Llm.FolderDataUsage = _folderDataUsage;
s.Llm.EnableAuditLog = _enableAuditLog;
s.Llm.EnableAgentMemory = _enableAgentMemory;
s.Llm.EnableProjectRules = _enableProjectRules;
s.Llm.MaxMemoryEntries = _maxMemoryEntries;
s.Llm.EnableImageInput = _enableImageInput;
s.Llm.MaxImageSizeKb = _maxImageSizeKb;
s.Llm.EnableToolHooks = _enableToolHooks;
s.Llm.ToolHookTimeoutMs = _toolHookTimeoutMs;
s.Llm.EnableSkillSystem = _enableSkillSystem;
s.Llm.SkillsFolderPath = _skillsFolderPath;
s.Llm.SlashPopupPageSize = _slashPopupPageSize;
s.Llm.EnableDragDropAiActions = _enableDragDropAiActions;
s.Llm.DragDropAutoSend = _dragDropAutoSend;
s.Llm.Code.EnableCodeReview = _enableCodeReview;
s.Llm.EnableAutoRouter = _enableAutoRouter;
s.Llm.AutoRouterConfidence = _autoRouterConfidence;
s.Llm.EnableChatRainbowGlow = _enableChatRainbowGlow;
s.Llm.NotifyOnComplete = _notifyOnComplete;
s.Llm.ShowTips = _showTips;
s.Llm.DevMode = _devMode;
s.Llm.DevModeStepApproval = _devModeStepApproval;
s.Llm.WorkflowVisualizer = _workflowVisualizer;
s.Llm.FreeTierMode = _freeTierMode;
s.Llm.FreeTierDelaySeconds = _freeTierDelaySeconds;
s.Llm.ShowTotalCallStats = _showTotalCallStats;
// 서비스별 독립 설정 저장
s.Llm.OllamaEndpoint = _ollamaEndpoint;
s.Llm.OllamaModel = _ollamaModel;
s.Llm.VllmEndpoint = _vllmEndpoint;
s.Llm.VllmModel = _vllmModel;
s.Llm.GeminiModel = _geminiModel;
s.Llm.ClaudeModel = _claudeModel;
s.Llm.GeminiApiKey = _geminiApiKey;
s.Llm.ClaudeApiKey = _claudeApiKey;
// 내부 서비스 API 키 저장 (암호화 분기)
if (!string.IsNullOrEmpty(_ollamaApiKey) && _ollamaApiKey != "(저장됨)")
s.Llm.OllamaApiKey = CryptoService.EncryptIfEnabled(_ollamaApiKey, s.Llm.EncryptionEnabled);
if (!string.IsNullOrEmpty(_vllmApiKey) && _vllmApiKey != "(저장됨)")
s.Llm.VllmApiKey = CryptoService.EncryptIfEnabled(_vllmApiKey, s.Llm.EncryptionEnabled);
// 활성 서비스의 설정을 기존 호환 필드에도 동기화 (LlmService.cs 호환)
switch (_llmService)
{
case "ollama":
s.Llm.Endpoint = _ollamaEndpoint;
s.Llm.Model = _ollamaModel;
s.Llm.EncryptedApiKey = s.Llm.OllamaApiKey;
break;
case "vllm":
s.Llm.Endpoint = _vllmEndpoint;
s.Llm.Model = _vllmModel;
s.Llm.EncryptedApiKey = s.Llm.VllmApiKey;
break;
case "gemini":
s.Llm.ApiKey = _geminiApiKey;
s.Llm.Model = _geminiModel;
break;
case "claude":
s.Llm.ApiKey = _claudeApiKey;
s.Llm.Model = _claudeModel;
break;
}
// 등록 모델 저장
s.Llm.RegisteredModels = RegisteredModels
.Where(rm => !string.IsNullOrWhiteSpace(rm.Alias))
.Select(rm => new RegisteredModel
{
Alias = rm.Alias,
EncryptedModelName = rm.EncryptedModelName,
Service = rm.Service,
Endpoint = rm.Endpoint,
ApiKey = rm.ApiKey,
AuthType = rm.AuthType ?? "bearer",
Cp4dUrl = rm.Cp4dUrl ?? "",
Cp4dUsername = rm.Cp4dUsername ?? "",
Cp4dPassword = rm.Cp4dPassword ?? "",
})
.ToList();
// 프롬프트 템플릿 저장
s.Llm.PromptTemplates = PromptTemplates
.Where(pt => !string.IsNullOrWhiteSpace(pt.Name))
.Select(pt => new PromptTemplate { Name = pt.Name, Content = pt.Content, Icon = pt.Icon })
.ToList();
// 인덱스 경로 + 확장자 저장
s.IndexPaths = IndexPaths.ToList();
s.IndexExtensions = IndexExtensions.ToList();
s.IndexSpeed = _indexSpeed;
// 커스텀 색상 + 모양 저장
var c = s.Launcher.CustomTheme ??= new CustomThemeColors();
foreach (var row in ColorRows)
{
var prop = typeof(CustomThemeColors).GetProperty(row.Property);
prop?.SetValue(c, row.Hex);
}
c.WindowCornerRadius = _customWindowCornerRadius;
c.ItemCornerRadius = _customItemCornerRadius;
// 빠른 실행 단축키 저장:
// batch/api/clipboard type은 그대로 유지, app/url/folder는 ViewModel 내용으로 교체
var otherAliases = s.Aliases
.Where(a => a.Type is not ("app" or "url" or "folder" or "batch"))
.ToList();
s.Aliases = otherAliases
.Concat(AppShortcuts.Select(sc => new AliasEntry
{
Key = sc.Key,
Type = sc.Type,
Target = sc.Target,
Description = string.IsNullOrWhiteSpace(sc.Description) ? null : sc.Description
}))
.Concat(BatchCommands.Select(b => new AliasEntry
{
Key = b.Key,
Type = "batch",
Target = b.Command,
ShowWindow = b.ShowWindow
}))
.ToList();
// 전용 핫키 저장
s.CustomHotkeys = CustomHotkeys.Select(h => new Models.HotkeyAssignment
{
Hotkey = h.Hotkey,
Target = h.Target,
Label = h.Label,
Type = h.Type
}).ToList();
// 스니펫 저장
s.Snippets = Snippets.Select(sn => new SnippetEntry
{
Key = sn.Key,
Name = sn.Name,
Content = sn.Content
}).ToList();
// 알림 설정 저장
s.Reminder.Enabled = _reminderEnabled;
s.Reminder.Corner = _reminderCorner;
s.Reminder.IntervalMinutes = _reminderIntervalMinutes;
s.Reminder.DisplaySeconds = _reminderDisplaySeconds;
// 캡처 설정 저장
s.ScreenCapture.Prefix = string.IsNullOrWhiteSpace(_capPrefix) ? "cap" : _capPrefix.Trim();
s.ScreenCapture.GlobalHotkeyEnabled = _capGlobalHotkeyEnabled;
s.ScreenCapture.GlobalHotkey = string.IsNullOrWhiteSpace(_capGlobalHotkey) ? "PrintScreen" : _capGlobalHotkey.Trim();
s.ScreenCapture.GlobalHotkeyMode = _capGlobalMode;
s.ScreenCapture.ScrollDelayMs = Math.Max(50, _capScrollDelayMs);
// 클립보드 히스토리 설정 저장
s.ClipboardHistory.Enabled = _clipboardEnabled;
s.ClipboardHistory.MaxItems = _clipboardMaxItems;
// 시스템 명령 설정 저장
var sc = s.SystemCommands;
sc.ShowLock = _sysShowLock;
sc.ShowSleep = _sysShowSleep;
sc.ShowRestart = _sysShowRestart;
sc.ShowShutdown = _sysShowShutdown;
sc.ShowHibernate = _sysShowHibernate;
sc.ShowLogout = _sysShowLogout;
sc.ShowRecycleBin = _sysShowRecycleBin;
// 시스템 명령 별칭 저장
var cmdAliases = new Dictionary>();
void SaveAlias(string key, string val)
{
var list = ParseAliases(val);
if (list.Count > 0) cmdAliases[key] = list;
}
SaveAlias("lock", _aliasLock);
SaveAlias("sleep", _aliasSleep);
SaveAlias("restart", _aliasRestart);
SaveAlias("shutdown", _aliasShutdown);
SaveAlias("hibernate", _aliasHibernate);
SaveAlias("logout", _aliasLogout);
SaveAlias("recycle", _aliasRecycle);
sc.CommandAliases = cmdAliases;
_service.Save();
SaveCompleted?.Invoke(this, EventArgs.Empty);
}
public bool AddBatchCommand()
{
if (string.IsNullOrWhiteSpace(_newBatchKey) || string.IsNullOrWhiteSpace(_newBatchCommand))
return false;
if (BatchCommands.Any(b => b.Key.Equals(_newBatchKey.Trim(), StringComparison.OrdinalIgnoreCase)))
return false;
BatchCommands.Add(new BatchCommandModel
{
Key = _newBatchKey.Trim(),
Command = _newBatchCommand.Trim(),
ShowWindow = _newBatchShowWindow
});
NewBatchKey = ""; NewBatchCommand = ""; NewBatchShowWindow = false;
return true;
}
public void RemoveBatchCommand(BatchCommandModel cmd) => BatchCommands.Remove(cmd);
// ─── 전용 핫키 메서드 ─────────────────────────────────────────────────────
public bool AddCustomHotkey()
{
if (string.IsNullOrWhiteSpace(_newHotkeyStr) || string.IsNullOrWhiteSpace(_newHotkeyTarget))
return false;
var hotkey = _newHotkeyStr.Trim();
// 핫키 형식 검증
if (!Core.HotkeyParser.TryParse(hotkey, out _))
{
CustomMessageBox.Show(
$"핫키 형식이 올바르지 않습니다: '{hotkey}'\n예) Ctrl+Alt+1, Ctrl+Shift+F2",
"AX Copilot — 핫키 오류",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Warning);
return false;
}
// 중복 검사
if (CustomHotkeys.Any(h => h.Hotkey.Equals(hotkey, StringComparison.OrdinalIgnoreCase)))
{
CustomMessageBox.Show(
$"이미 등록된 핫키입니다: '{hotkey}'",
"AX Copilot — 중복 핫키",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Warning);
return false;
}
CustomHotkeys.Add(new HotkeyRowModel
{
Hotkey = hotkey,
Target = _newHotkeyTarget.Trim(),
Label = string.IsNullOrWhiteSpace(_newHotkeyLabel) ? System.IO.Path.GetFileNameWithoutExtension(_newHotkeyTarget.Trim()) : _newHotkeyLabel.Trim(),
Type = _newHotkeyType
});
NewHotkeyStr = ""; NewHotkeyTarget = ""; NewHotkeyLabel = ""; NewHotkeyType = "app";
return true;
}
public void RemoveCustomHotkey(HotkeyRowModel row) => CustomHotkeys.Remove(row);
// ─── 스니펫 메서드 ──────────────────────────────────────────────────────
public bool AddSnippet()
{
if (string.IsNullOrWhiteSpace(_newSnippetKey) || string.IsNullOrWhiteSpace(_newSnippetContent))
return false;
if (Snippets.Any(sn => sn.Key.Equals(_newSnippetKey.Trim(), StringComparison.OrdinalIgnoreCase)))
return false;
Snippets.Add(new SnippetRowModel
{
Key = _newSnippetKey.Trim(),
Name = _newSnippetName.Trim(),
Content = _newSnippetContent.Trim()
});
NewSnippetKey = ""; NewSnippetName = ""; NewSnippetContent = "";
return true;
}
public void RemoveSnippet(SnippetRowModel row) => Snippets.Remove(row);
// ─── 캡처 메서드 ────────────────────────────────────────────────────────
public void ResetCapPrefix() => CapPrefix = "cap";
public void ResetCapGlobalHotkey()
{
CapGlobalHotkey = "PrintScreen";
CapGlobalMode = "screen";
}
// ─── 알림 메서드 ────────────────────────────────────────────────────────
public List GetReminderCategories() => _service.Settings.Reminder.EnabledCategories;
// ─── 시스템 명령 메서드 ─────────────────────────────────────────────────
public void ResetSystemCommandAliases()
{
AliasLock = ""; AliasSleep = ""; AliasRestart = "";
AliasShutdown = ""; AliasHibernate = ""; AliasLogout = ""; AliasRecycle = "";
}
private static string FormatAliases(Dictionary> dict, string key)
=> dict.TryGetValue(key, out var list) ? string.Join(", ", list) : "";
private static List ParseAliases(string input)
=> input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(s => !string.IsNullOrWhiteSpace(s))
.ToList();
public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string? n = null)
=> PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(n));
}