From 436797abfa7d30c7accc609fbc1c5262f2807c2e Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Thu, 16 Apr 2026 14:20:52 -0400 Subject: [PATCH] support open browser --- src-tauri/src/browsers.rs | 119 ++++++++++++++++++ src-tauri/src/commands.rs | 60 ++++++++- src-tauri/src/lib.rs | 6 +- src-tauri/src/models.rs | 1 + src-tauri/src/scanner.rs | 21 +--- src/App.vue | 23 +++- .../browser-assistant/useBrowserAssistant.ts | 29 +++++ src/styles.css | 46 +++++++ 8 files changed, 282 insertions(+), 23 deletions(-) create mode 100644 src-tauri/src/browsers.rs diff --git a/src-tauri/src/browsers.rs b/src-tauri/src/browsers.rs new file mode 100644 index 0000000..700e315 --- /dev/null +++ b/src-tauri/src/browsers.rs @@ -0,0 +1,119 @@ +use std::{env, path::PathBuf}; + +use crate::models::BrowserDefinition; + +pub fn browser_definitions() -> Vec { + vec![ + BrowserDefinition { + id: "chrome", + name: "Google Chrome", + local_app_data_segments: &["Google", "Chrome", "User Data"], + executable_candidates: &[ + ExecutableCandidate::ProgramFiles(&[ + "Google", + "Chrome", + "Application", + "chrome.exe", + ]), + ExecutableCandidate::ProgramFilesX86(&[ + "Google", + "Chrome", + "Application", + "chrome.exe", + ]), + ExecutableCandidate::LocalAppData(&[ + "Google", + "Chrome", + "Application", + "chrome.exe", + ]), + ], + }, + BrowserDefinition { + id: "edge", + name: "Microsoft Edge", + local_app_data_segments: &["Microsoft", "Edge", "User Data"], + executable_candidates: &[ + ExecutableCandidate::ProgramFiles(&[ + "Microsoft", + "Edge", + "Application", + "msedge.exe", + ]), + ExecutableCandidate::ProgramFilesX86(&[ + "Microsoft", + "Edge", + "Application", + "msedge.exe", + ]), + ], + }, + BrowserDefinition { + id: "brave", + name: "Brave", + local_app_data_segments: &["BraveSoftware", "Brave-Browser", "User Data"], + executable_candidates: &[ + ExecutableCandidate::ProgramFiles(&[ + "BraveSoftware", + "Brave-Browser", + "Application", + "brave.exe", + ]), + ExecutableCandidate::ProgramFilesX86(&[ + "BraveSoftware", + "Brave-Browser", + "Application", + "brave.exe", + ]), + ExecutableCandidate::LocalAppData(&[ + "BraveSoftware", + "Brave-Browser", + "Application", + "brave.exe", + ]), + ], + }, + ] +} + +pub fn browser_definition_by_id(browser_id: &str) -> Option { + browser_definitions() + .into_iter() + .find(|definition| definition.id == browser_id) +} + +pub fn resolve_browser_executable(browser_id: &str) -> Option { + let definition = browser_definition_by_id(browser_id)?; + definition + .executable_candidates + .iter() + .find_map(resolve_executable_candidate) + .filter(|path| path.is_file()) +} + +fn resolve_executable_candidate(candidate: &ExecutableCandidate) -> Option { + match candidate { + ExecutableCandidate::ProgramFiles(segments) => env::var_os("ProgramFiles") + .map(PathBuf::from) + .map(|root| join_segments(root, segments)), + ExecutableCandidate::ProgramFilesX86(segments) => env::var_os("ProgramFiles(x86)") + .map(PathBuf::from) + .map(|root| join_segments(root, segments)), + ExecutableCandidate::LocalAppData(segments) => env::var_os("LOCALAPPDATA") + .map(PathBuf::from) + .map(|root| join_segments(root, segments)), + } +} + +fn join_segments(mut root: PathBuf, segments: &[&str]) -> PathBuf { + for segment in segments { + root.push(segment); + } + root +} + +pub enum ExecutableCandidate { + ProgramFiles(&'static [&'static str]), + ProgramFilesX86(&'static [&'static str]), + LocalAppData(&'static [&'static str]), +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 52d1703..b9b6932 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,6 +1,64 @@ -use crate::{models::ScanResponse, scanner}; +use std::{path::PathBuf, process::Command}; + +use crate::{ + browsers::{browser_definition_by_id, resolve_browser_executable}, + models::ScanResponse, + scanner, + utils::local_app_data_dir, +}; #[tauri::command] pub fn scan_browsers() -> Result { scanner::scan_browsers() } + +#[tauri::command] +pub fn open_browser_profile(browser_id: String, profile_id: String) -> Result<(), String> { + let definition = browser_definition_by_id(&browser_id) + .ok_or_else(|| format!("Unsupported browser: {browser_id}"))?; + let executable_path = resolve_browser_executable(&browser_id) + .ok_or_else(|| format!("Unable to locate executable for browser: {browser_id}"))?; + let local_app_data = local_app_data_dir().ok_or_else(|| { + "Unable to resolve the LOCALAPPDATA directory for the current user.".to_string() + })?; + let user_data_dir = definition + .local_app_data_segments + .iter() + .fold(local_app_data, |path, segment| path.join(segment)); + let profile_directory = user_data_dir.join(&profile_id); + + if !user_data_dir.is_dir() { + return Err(format!( + "User data directory does not exist: {}", + user_data_dir.display() + )); + } + + if !profile_directory.is_dir() { + return Err(format!( + "Profile directory does not exist: {}", + profile_directory.display() + )); + } + + spawn_browser_process(executable_path, user_data_dir, profile_id) +} + +fn spawn_browser_process( + executable_path: PathBuf, + user_data_dir: PathBuf, + profile_id: String, +) -> Result<(), String> { + Command::new(&executable_path) + .arg(format!("--user-data-dir={}", user_data_dir.display())) + .arg(format!("--profile-directory={profile_id}")) + .arg("https://www.google.com") + .spawn() + .map(|_| ()) + .map_err(|error| { + format!( + "Failed to open browser profile with executable {}: {error}", + executable_path.display() + ) + }) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d5c610d..848f129 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,4 @@ +mod browsers; mod commands; mod models; mod scanner; @@ -7,7 +8,10 @@ mod utils; pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![commands::scan_browsers]) + .invoke_handler(tauri::generate_handler![ + commands::scan_browsers, + commands::open_browser_profile + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index c36732c..699b954 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -61,6 +61,7 @@ pub struct BrowserDefinition { pub id: &'static str, pub name: &'static str, pub local_app_data_segments: &'static [&'static str], + pub executable_candidates: &'static [crate::browsers::ExecutableCandidate], } pub struct TempExtension { diff --git a/src-tauri/src/scanner.rs b/src-tauri/src/scanner.rs index 2e66065..6a4f5ea 100644 --- a/src-tauri/src/scanner.rs +++ b/src-tauri/src/scanner.rs @@ -7,6 +7,7 @@ use std::{ use serde_json::Value; use crate::{ + browsers::browser_definitions, models::{ BookmarkSummary, BrowserDefinition, BrowserStats, BrowserView, ExtensionSummary, ProfileSummary, ScanResponse, TempBookmark, TempExtension, @@ -30,26 +31,6 @@ pub fn scan_browsers() -> Result { Ok(ScanResponse { browsers }) } -fn browser_definitions() -> Vec { - vec![ - BrowserDefinition { - id: "chrome", - name: "Google Chrome", - local_app_data_segments: &["Google", "Chrome", "User Data"], - }, - BrowserDefinition { - id: "edge", - name: "Microsoft Edge", - local_app_data_segments: &["Microsoft", "Edge", "User Data"], - }, - BrowserDefinition { - id: "brave", - name: "Brave", - local_app_data_segments: &["BraveSoftware", "Brave-Browser", "User Data"], - }, - ] -} - fn scan_browser(local_app_data: &Path, definition: BrowserDefinition) -> Option { let root = definition .local_app_data_segments diff --git a/src/App.vue b/src/App.vue index 90dee99..8d061c9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -14,7 +14,10 @@ const { extensionMonogram, extensionProfilesExpanded, extensionSortKey, + isOpeningProfile, loading, + openProfileError, + openBrowserProfile, profileSortKey, scanBrowsers, sectionCount, @@ -109,6 +112,10 @@ const {
+
+ {{ openProfileError }} +
+

{{ profile.name }}

- {{ profile.id }} +
+ + {{ profile.id }} +

{{ profile.email || "No email found" }}

diff --git a/src/features/browser-assistant/useBrowserAssistant.ts b/src/features/browser-assistant/useBrowserAssistant.ts index 43276ec..d8758c1 100644 --- a/src/features/browser-assistant/useBrowserAssistant.ts +++ b/src/features/browser-assistant/useBrowserAssistant.ts @@ -14,6 +14,8 @@ import type { export function useBrowserAssistant() { const loading = ref(true); const error = ref(""); + const openProfileError = ref(""); + const openingProfileKey = ref(""); const response = ref({ browsers: [] }); const selectedBrowserId = ref(""); const activeSection = ref("profiles"); @@ -81,6 +83,30 @@ export function useBrowserAssistant() { } } + async function openBrowserProfile(browserId: string, profileId: string) { + const profileKey = `${browserId}:${profileId}`; + openingProfileKey.value = profileKey; + openProfileError.value = ""; + + try { + await invoke("open_browser_profile", { + browserId, + profileId, + }); + } catch (openError) { + openProfileError.value = + openError instanceof Error + ? openError.message + : "Failed to open the selected browser profile."; + } finally { + openingProfileKey.value = ""; + } + } + + function isOpeningProfile(browserId: string, profileId: string) { + return openingProfileKey.value === `${browserId}:${profileId}`; + } + function browserMonogram(browserId: string) { if (browserId === "chrome") return "CH"; if (browserId === "edge") return "ED"; @@ -144,6 +170,9 @@ export function useBrowserAssistant() { extensionProfilesExpanded, extensionSortKey, loading, + isOpeningProfile, + openProfileError, + openBrowserProfile, profileSortKey, scanBrowsers, sectionCount, diff --git a/src/styles.css b/src/styles.css index ae72c4b..6125e7e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -289,6 +289,16 @@ button { padding: 16px; } +.inline-error { + margin-bottom: 12px; + padding: 10px 12px; + border: 1px solid rgba(239, 68, 68, 0.18); + border-radius: 12px; + background: rgba(254, 242, 242, 0.92); + color: #b42318; + font-size: 0.86rem; +} + .sort-bar { display: flex; justify-content: flex-end; @@ -524,6 +534,12 @@ button { line-height: 1.35; } +.profile-actions { + display: inline-flex; + align-items: center; + gap: 8px; +} + .profile-email, .meta-line, .bookmark-url { @@ -561,6 +577,36 @@ button { cursor: pointer; } +.card-action-button { + padding: 6px 10px; + border-radius: 10px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(241, 245, 249, 0.92)); + border: 1px solid rgba(148, 163, 184, 0.24); + color: var(--text); + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: + border-color 160ms ease, + background 160ms ease, + box-shadow 160ms ease; +} + +.card-action-button:hover { + border-color: rgba(100, 116, 139, 0.36); + background: linear-gradient(180deg, rgba(255, 255, 255, 1), rgba(226, 232, 240, 0.9)); +} + +.card-action-button:disabled { + cursor: default; + opacity: 0.7; + border-color: rgba(148, 163, 184, 0.2); +} + +.card-action-button:active { + box-shadow: inset 0 2px 4px rgba(148, 163, 184, 0.18); +} + .disclosure-panel { display: flex; flex-wrap: wrap;