Add external editor/file picker, update theme
Introduce file/application pickers and external-editor integration, plus a visual refresh. Frontend: add UI for selecting/rescanning installed IDEs, custom editor input, "Open in File Explorer" and "Open in Code Editor" actions, clone destination browse, helper utilities, new icons, and many CSS theme/UX improvements (variables, shadows, scrollbars, selection, refined component styles). State: track installedIdes and scan status. Tauri API: expose browseDirectory, browseApplication and scanInstalledIdes, and wire UI handlers to call them. Backend: add InstalledIde struct and update tauri Cargo manifest and capabilities to allow dialogs. Overall improves editor/workflow integrations and modernizes the app styling.
This commit is contained in:
Generated
+68
@@ -1297,6 +1297,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
]
|
||||
|
||||
@@ -2254,6 +2255,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
@@ -2920,6 +2922,30 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -3675,6 +3701,48 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"log",
|
||||
"objc2-foundation",
|
||||
"percent-encoding",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.4"
|
||||
|
||||
@@ -23,4 +23,5 @@ tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"dialog:default",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -126,6 +126,14 @@ struct GitCommitDetail {
|
||||
diff: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InstalledIde {
|
||||
id: String,
|
||||
name: String,
|
||||
executable_path: String,
|
||||
}
|
||||
|
||||
fn normalize_api_base_url(server_url: &str) -> Result<String, String> {
|
||||
// Normalize user input so every backend consistently resolves to /api/v1.
|
||||
let trimmed = server_url.trim().trim_end_matches('/');
|
||||
@@ -240,6 +248,194 @@ fn validate_git_path(path: &str) -> Result<String, String> {
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
fn add_installed_ide(
|
||||
ides: &mut Vec<InstalledIde>,
|
||||
seen: &mut HashSet<String>,
|
||||
name: &str,
|
||||
executable_path: PathBuf,
|
||||
) {
|
||||
if !executable_path.is_file() {
|
||||
return;
|
||||
}
|
||||
|
||||
let executable_path = executable_path.to_string_lossy().to_string();
|
||||
let key = executable_path.to_lowercase();
|
||||
if seen.insert(key) {
|
||||
ides.push(InstalledIde {
|
||||
id: format!(
|
||||
"{}:{}",
|
||||
name.to_lowercase().replace(' ', "-"),
|
||||
executable_path
|
||||
),
|
||||
name: name.to_string(),
|
||||
executable_path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn env_path(name: &str) -> Option<PathBuf> {
|
||||
env::var_os(name).map(PathBuf::from)
|
||||
}
|
||||
|
||||
fn add_relative_ide_candidate(
|
||||
ides: &mut Vec<InstalledIde>,
|
||||
seen: &mut HashSet<String>,
|
||||
env_name: &str,
|
||||
relative_path: &str,
|
||||
name: &str,
|
||||
) {
|
||||
if let Some(base_path) = env_path(env_name) {
|
||||
add_installed_ide(ides, seen, name, base_path.join(relative_path));
|
||||
}
|
||||
}
|
||||
|
||||
fn add_path_ide_candidate(
|
||||
ides: &mut Vec<InstalledIde>,
|
||||
seen: &mut HashSet<String>,
|
||||
command_names: &[&str],
|
||||
name: &str,
|
||||
) {
|
||||
if let Some(executable_path) = find_executable_on_path(command_names) {
|
||||
add_installed_ide(ides, seen, name, executable_path);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_executable_on_path(command_names: &[&str]) -> Option<PathBuf> {
|
||||
let path_values = env::var_os("PATH")?;
|
||||
let extensions: Vec<String> = if cfg!(windows) {
|
||||
env::var("PATHEXT")
|
||||
.unwrap_or_else(|_| ".EXE;.CMD;.BAT".to_string())
|
||||
.split(';')
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect()
|
||||
} else {
|
||||
vec!["".to_string()]
|
||||
};
|
||||
|
||||
for dir in env::split_paths(&path_values) {
|
||||
for command_name in command_names {
|
||||
let command_path = Path::new(command_name);
|
||||
let has_extension = command_path.extension().is_some();
|
||||
let candidates = if has_extension {
|
||||
vec![dir.join(command_name)]
|
||||
} else {
|
||||
extensions
|
||||
.iter()
|
||||
.map(|extension| dir.join(format!("{command_name}{extension}")))
|
||||
.collect()
|
||||
};
|
||||
|
||||
for candidate in candidates {
|
||||
if candidate.is_file() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn add_windows_ide_candidates(ides: &mut Vec<InstalledIde>, seen: &mut HashSet<String>) {
|
||||
let candidates = [
|
||||
(
|
||||
"LOCALAPPDATA",
|
||||
"Programs\\Microsoft VS Code\\Code.exe",
|
||||
"Visual Studio Code",
|
||||
),
|
||||
(
|
||||
"LOCALAPPDATA",
|
||||
"Programs\\Microsoft VS Code Insiders\\Code - Insiders.exe",
|
||||
"Visual Studio Code Insiders",
|
||||
),
|
||||
("LOCALAPPDATA", "Programs\\Cursor\\Cursor.exe", "Cursor"),
|
||||
(
|
||||
"LOCALAPPDATA",
|
||||
"Programs\\Windsurf\\Windsurf.exe",
|
||||
"Windsurf",
|
||||
),
|
||||
(
|
||||
"LOCALAPPDATA",
|
||||
"Programs\\VSCodium\\VSCodium.exe",
|
||||
"VSCodium",
|
||||
),
|
||||
("LOCALAPPDATA", "Programs\\Zed\\Zed.exe", "Zed"),
|
||||
(
|
||||
"ProgramFiles",
|
||||
"Microsoft VS Code\\Code.exe",
|
||||
"Visual Studio Code",
|
||||
),
|
||||
(
|
||||
"ProgramFiles",
|
||||
"Microsoft VS Code Insiders\\Code - Insiders.exe",
|
||||
"Visual Studio Code Insiders",
|
||||
),
|
||||
("ProgramFiles", "Cursor\\Cursor.exe", "Cursor"),
|
||||
("ProgramFiles", "Windsurf\\Windsurf.exe", "Windsurf"),
|
||||
("ProgramFiles", "VSCodium\\VSCodium.exe", "VSCodium"),
|
||||
(
|
||||
"ProgramFiles",
|
||||
"Sublime Text\\sublime_text.exe",
|
||||
"Sublime Text",
|
||||
),
|
||||
("ProgramFiles", "Notepad++\\notepad++.exe", "Notepad++"),
|
||||
("ProgramFiles(x86)", "Notepad++\\notepad++.exe", "Notepad++"),
|
||||
];
|
||||
|
||||
for (env_name, relative_path, name) in candidates {
|
||||
add_relative_ide_candidate(ides, seen, env_name, relative_path, name);
|
||||
}
|
||||
|
||||
for env_name in ["ProgramFiles", "ProgramFiles(x86)"] {
|
||||
if let Some(jetbrains_root) = env_path(env_name).map(|path| path.join("JetBrains")) {
|
||||
add_jetbrains_candidates(ides, seen, &jetbrains_root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn add_windows_ide_candidates(_ides: &mut Vec<InstalledIde>, _seen: &mut HashSet<String>) {}
|
||||
|
||||
fn add_jetbrains_candidates(ides: &mut Vec<InstalledIde>, seen: &mut HashSet<String>, root: &Path) {
|
||||
let Ok(entries) = fs::read_dir(root) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let app_dir = entry.path();
|
||||
if !app_dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dir_name = app_dir
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("JetBrains IDE")
|
||||
.to_string();
|
||||
let candidates = [
|
||||
("IntelliJ IDEA", "idea64.exe"),
|
||||
("WebStorm", "webstorm64.exe"),
|
||||
("PhpStorm", "phpstorm64.exe"),
|
||||
("PyCharm", "pycharm64.exe"),
|
||||
("Rider", "rider64.exe"),
|
||||
("RustRover", "rustrover64.exe"),
|
||||
("GoLand", "goland64.exe"),
|
||||
];
|
||||
|
||||
for (label, exe) in candidates {
|
||||
let label_lower = label.to_lowercase();
|
||||
let name = if dir_name.to_lowercase().contains(label_lower.as_str()) {
|
||||
dir_name.clone()
|
||||
} else {
|
||||
label.to_string()
|
||||
};
|
||||
add_installed_ide(ides, seen, &name, app_dir.join("bin").join(exe));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn status_kind(x: &str, y: &str) -> String {
|
||||
match (x, y) {
|
||||
("?", "?") => "untracked",
|
||||
@@ -1259,6 +1455,82 @@ fn open_in_external_editor(repo_path: String, editor_path: String) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn scan_installed_ides() -> Result<Vec<InstalledIde>, String> {
|
||||
let mut ides = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
add_windows_ide_candidates(&mut ides, &mut seen);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let candidates = [
|
||||
(
|
||||
"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
|
||||
"Visual Studio Code",
|
||||
),
|
||||
(
|
||||
"/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders",
|
||||
"Visual Studio Code Insiders",
|
||||
),
|
||||
("/Applications/Cursor.app/Contents/Resources/app/bin/cursor", "Cursor"),
|
||||
("/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf", "Windsurf"),
|
||||
("/Applications/VSCodium.app/Contents/Resources/app/bin/codium", "VSCodium"),
|
||||
("/Applications/Zed.app/Contents/MacOS/zed", "Zed"),
|
||||
("/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", "Sublime Text"),
|
||||
];
|
||||
|
||||
for (path, name) in candidates {
|
||||
add_installed_ide(&mut ides, &mut seen, name, PathBuf::from(path));
|
||||
}
|
||||
}
|
||||
|
||||
add_path_ide_candidate(
|
||||
&mut ides,
|
||||
&mut seen,
|
||||
&["code", "Code.exe", "code.cmd"],
|
||||
"Visual Studio Code",
|
||||
);
|
||||
add_path_ide_candidate(
|
||||
&mut ides,
|
||||
&mut seen,
|
||||
&["code-insiders", "Code - Insiders.exe", "code-insiders.cmd"],
|
||||
"Visual Studio Code Insiders",
|
||||
);
|
||||
add_path_ide_candidate(
|
||||
&mut ides,
|
||||
&mut seen,
|
||||
&["cursor", "Cursor.exe", "cursor.cmd"],
|
||||
"Cursor",
|
||||
);
|
||||
add_path_ide_candidate(
|
||||
&mut ides,
|
||||
&mut seen,
|
||||
&["windsurf", "Windsurf.exe", "windsurf.cmd"],
|
||||
"Windsurf",
|
||||
);
|
||||
add_path_ide_candidate(
|
||||
&mut ides,
|
||||
&mut seen,
|
||||
&["codium", "VSCodium.exe", "codium.cmd"],
|
||||
"VSCodium",
|
||||
);
|
||||
add_path_ide_candidate(&mut ides, &mut seen, &["zed", "Zed.exe"], "Zed");
|
||||
add_path_ide_candidate(
|
||||
&mut ides,
|
||||
&mut seen,
|
||||
&["subl", "sublime_text.exe"],
|
||||
"Sublime Text",
|
||||
);
|
||||
|
||||
ides.sort_by(|a, b| {
|
||||
a.name
|
||||
.cmp(&b.name)
|
||||
.then(a.executable_path.cmp(&b.executable_path))
|
||||
});
|
||||
Ok(ides)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn scan_local_repos(
|
||||
roots: Option<Vec<String>>,
|
||||
@@ -1558,6 +1830,7 @@ fn test_gitea_connection(
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
git_clone,
|
||||
@@ -1582,6 +1855,7 @@ pub fn run() {
|
||||
commit_detail,
|
||||
open_in_file_explorer,
|
||||
open_in_external_editor,
|
||||
scan_installed_ides,
|
||||
scan_local_repos,
|
||||
local_repo_branches,
|
||||
local_repo_tree,
|
||||
|
||||
Reference in New Issue
Block a user