|
3 | 3 | //! Handles loading, saving, and managing configuration files. |
4 | 4 |
|
5 | 5 | use std::collections::HashMap; |
6 | | -use std::path::PathBuf; |
| 6 | +use std::path::{Path, PathBuf}; |
7 | 7 |
|
8 | 8 | use directories::ProjectDirs; |
9 | 9 | use serde::{Deserialize, Serialize}; |
@@ -903,6 +903,150 @@ pub fn save_config_with_sync(config: &Config, sync: bool) -> Result<(), ConfigEr |
903 | 903 | Ok(()) |
904 | 904 | } |
905 | 905 |
|
| 906 | +/// An external tool shortcut entry for the Tools menu |
| 907 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 908 | +pub struct ToolShortcutEntry { |
| 909 | + pub name: String, |
| 910 | + pub command: String, |
| 911 | + #[serde(default)] |
| 912 | + pub args: Vec<String>, |
| 913 | +} |
| 914 | + |
| 915 | +impl ToolShortcutEntry { |
| 916 | + /// Execute this shortcut in the given working directory. |
| 917 | + /// Spawns the process detached (stdin/stdout/stderr null). |
| 918 | + pub fn execute(&self, cwd: &Path) -> Result<(), std::io::Error> { |
| 919 | + std::process::Command::new(&self.command) |
| 920 | + .args(&self.args) |
| 921 | + .current_dir(cwd) |
| 922 | + .stdin(std::process::Stdio::null()) |
| 923 | + .stdout(std::process::Stdio::null()) |
| 924 | + .stderr(std::process::Stdio::null()) |
| 925 | + .spawn()?; |
| 926 | + Ok(()) |
| 927 | + } |
| 928 | +} |
| 929 | + |
| 930 | +/// Get the platform-specific tool shortcuts file path |
| 931 | +pub fn tool_shortcuts_path() -> Option<PathBuf> { |
| 932 | + let filename = if cfg!(target_os = "macos") { |
| 933 | + "shortcuts_macos.toml" |
| 934 | + } else if cfg!(target_os = "windows") { |
| 935 | + "shortcuts_windows.toml" |
| 936 | + } else { |
| 937 | + "shortcuts_linux.toml" |
| 938 | + }; |
| 939 | + config_dir().map(|p| p.join(filename)) |
| 940 | +} |
| 941 | + |
| 942 | +/// Return default tool shortcuts for the current platform |
| 943 | +pub fn default_tool_shortcuts() -> Vec<ToolShortcutEntry> { |
| 944 | + #[cfg(target_os = "macos")] |
| 945 | + { |
| 946 | + vec![ |
| 947 | + ToolShortcutEntry { |
| 948 | + name: "Open in Finder".into(), |
| 949 | + command: "open".into(), |
| 950 | + args: vec![".".into()], |
| 951 | + }, |
| 952 | + ToolShortcutEntry { |
| 953 | + name: "Open in Xcode".into(), |
| 954 | + command: "xed".into(), |
| 955 | + args: vec![".".into()], |
| 956 | + }, |
| 957 | + ToolShortcutEntry { |
| 958 | + name: "Open in VS Code".into(), |
| 959 | + command: "code".into(), |
| 960 | + args: vec![".".into()], |
| 961 | + }, |
| 962 | + ToolShortcutEntry { |
| 963 | + name: "Open in Terminal".into(), |
| 964 | + command: "open".into(), |
| 965 | + args: vec!["-a".into(), "Terminal".into(), ".".into()], |
| 966 | + }, |
| 967 | + ] |
| 968 | + } |
| 969 | + #[cfg(target_os = "windows")] |
| 970 | + { |
| 971 | + vec![ |
| 972 | + ToolShortcutEntry { |
| 973 | + name: "Open in Explorer".into(), |
| 974 | + command: "explorer".into(), |
| 975 | + args: vec![".".into()], |
| 976 | + }, |
| 977 | + ToolShortcutEntry { |
| 978 | + name: "Open in VS Code".into(), |
| 979 | + command: "code".into(), |
| 980 | + args: vec![".".into()], |
| 981 | + }, |
| 982 | + ToolShortcutEntry { |
| 983 | + name: "Open in PowerShell".into(), |
| 984 | + command: "powershell".into(), |
| 985 | + args: vec!["-NoExit".into(), "-Command".into(), "cd .".into()], |
| 986 | + }, |
| 987 | + ] |
| 988 | + } |
| 989 | + #[cfg(not(any(target_os = "macos", target_os = "windows")))] |
| 990 | + { |
| 991 | + vec![ |
| 992 | + ToolShortcutEntry { |
| 993 | + name: "Open File Manager".into(), |
| 994 | + command: "xdg-open".into(), |
| 995 | + args: vec![".".into()], |
| 996 | + }, |
| 997 | + ToolShortcutEntry { |
| 998 | + name: "Open in VS Code".into(), |
| 999 | + command: "code".into(), |
| 1000 | + args: vec![".".into()], |
| 1001 | + }, |
| 1002 | + ToolShortcutEntry { |
| 1003 | + name: "Open in Terminal".into(), |
| 1004 | + command: "x-terminal-emulator".into(), |
| 1005 | + args: vec![".".into()], |
| 1006 | + }, |
| 1007 | + ] |
| 1008 | + } |
| 1009 | +} |
| 1010 | + |
| 1011 | +/// Load tool shortcuts from the platform-specific config file. |
| 1012 | +/// Returns defaults if the file doesn't exist. |
| 1013 | +pub fn load_tool_shortcuts() -> Result<Vec<ToolShortcutEntry>, ConfigError> { |
| 1014 | + let path = tool_shortcuts_path().ok_or(ConfigError::NoConfigDir)?; |
| 1015 | + |
| 1016 | + if !path.exists() { |
| 1017 | + return Ok(default_tool_shortcuts()); |
| 1018 | + } |
| 1019 | + |
| 1020 | + let content = std::fs::read_to_string(&path)?; |
| 1021 | + |
| 1022 | + #[derive(Deserialize)] |
| 1023 | + struct ToolShortcutsFile { |
| 1024 | + tools: Vec<ToolShortcutEntry>, |
| 1025 | + } |
| 1026 | + |
| 1027 | + let file: ToolShortcutsFile = toml::from_str(&content)?; |
| 1028 | + Ok(file.tools) |
| 1029 | +} |
| 1030 | + |
| 1031 | +/// Save tool shortcuts to the platform-specific config file |
| 1032 | +pub fn save_tool_shortcuts(tools: &[ToolShortcutEntry]) -> Result<(), ConfigError> { |
| 1033 | + let dir = config_dir().ok_or(ConfigError::NoConfigDir)?; |
| 1034 | + std::fs::create_dir_all(&dir)?; |
| 1035 | + |
| 1036 | + let path = tool_shortcuts_path().ok_or(ConfigError::NoConfigDir)?; |
| 1037 | + |
| 1038 | + #[derive(Serialize)] |
| 1039 | + struct ToolShortcutsFile<'a> { |
| 1040 | + tools: &'a [ToolShortcutEntry], |
| 1041 | + } |
| 1042 | + |
| 1043 | + let file = ToolShortcutsFile { tools }; |
| 1044 | + let content = toml::to_string_pretty(&file)?; |
| 1045 | + std::fs::write(&path, content)?; |
| 1046 | + |
| 1047 | + Ok(()) |
| 1048 | +} |
| 1049 | + |
906 | 1050 | #[cfg(test)] |
907 | 1051 | mod tests { |
908 | 1052 | use super::*; |
@@ -1260,4 +1404,59 @@ mod tests { |
1260 | 1404 | let visibility = TabBarVisibility::default(); |
1261 | 1405 | assert!(matches!(visibility, TabBarVisibility::Always)); |
1262 | 1406 | } |
| 1407 | + |
| 1408 | + #[test] |
| 1409 | + fn test_default_tool_shortcuts_not_empty() { |
| 1410 | + let shortcuts = default_tool_shortcuts(); |
| 1411 | + assert!(!shortcuts.is_empty()); |
| 1412 | + // First entry should always have a name and command |
| 1413 | + assert!(!shortcuts[0].name.is_empty()); |
| 1414 | + assert!(!shortcuts[0].command.is_empty()); |
| 1415 | + } |
| 1416 | + |
| 1417 | + #[test] |
| 1418 | + fn test_tool_shortcut_serialize() { |
| 1419 | + let shortcut = ToolShortcutEntry { |
| 1420 | + name: "Test".into(), |
| 1421 | + command: "echo".into(), |
| 1422 | + args: vec!["hello".into()], |
| 1423 | + }; |
| 1424 | + let serialized = toml::to_string(&shortcut).unwrap(); |
| 1425 | + assert!(serialized.contains("name = \"Test\"")); |
| 1426 | + assert!(serialized.contains("command = \"echo\"")); |
| 1427 | + } |
| 1428 | + |
| 1429 | + #[test] |
| 1430 | + fn test_tool_shortcut_deserialize() { |
| 1431 | + let toml_str = r#" |
| 1432 | + [[tools]] |
| 1433 | + name = "Open Finder" |
| 1434 | + command = "open" |
| 1435 | + args = ["."] |
| 1436 | + "#; |
| 1437 | + |
| 1438 | + #[derive(Deserialize)] |
| 1439 | + struct File { |
| 1440 | + tools: Vec<ToolShortcutEntry>, |
| 1441 | + } |
| 1442 | + |
| 1443 | + let file: File = toml::from_str(toml_str).unwrap(); |
| 1444 | + assert_eq!(file.tools.len(), 1); |
| 1445 | + assert_eq!(file.tools[0].name, "Open Finder"); |
| 1446 | + assert_eq!(file.tools[0].command, "open"); |
| 1447 | + assert_eq!(file.tools[0].args, vec!["."]); |
| 1448 | + } |
| 1449 | + |
| 1450 | + #[test] |
| 1451 | + fn test_tool_shortcut_deserialize_no_args() { |
| 1452 | + let toml_str = r#" |
| 1453 | + name = "Test" |
| 1454 | + command = "ls" |
| 1455 | + "#; |
| 1456 | + |
| 1457 | + let entry: ToolShortcutEntry = toml::from_str(toml_str).unwrap(); |
| 1458 | + assert_eq!(entry.name, "Test"); |
| 1459 | + assert_eq!(entry.command, "ls"); |
| 1460 | + assert!(entry.args.is_empty()); |
| 1461 | + } |
1263 | 1462 | } |
0 commit comments