support open browser

This commit is contained in:
Julian Freeman
2026-04-16 14:20:52 -04:00
parent 0b46db0b43
commit 436797abfa
8 changed files with 282 additions and 23 deletions

119
src-tauri/src/browsers.rs Normal file
View File

@@ -0,0 +1,119 @@
use std::{env, path::PathBuf};
use crate::models::BrowserDefinition;
pub fn browser_definitions() -> Vec<BrowserDefinition> {
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<BrowserDefinition> {
browser_definitions()
.into_iter()
.find(|definition| definition.id == browser_id)
}
pub fn resolve_browser_executable(browser_id: &str) -> Option<PathBuf> {
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<PathBuf> {
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]),
}

View File

@@ -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<ScanResponse, String> {
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()
)
})
}

View File

@@ -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");
}

View File

@@ -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 {

View File

@@ -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<ScanResponse, String> {
Ok(ScanResponse { browsers })
}
fn browser_definitions() -> Vec<BrowserDefinition> {
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<BrowserView> {
let root = definition
.local_app_data_segments

View File

@@ -14,7 +14,10 @@ const {
extensionMonogram,
extensionProfilesExpanded,
extensionSortKey,
isOpeningProfile,
loading,
openProfileError,
openBrowserProfile,
profileSortKey,
scanBrowsers,
sectionCount,
@@ -109,6 +112,10 @@ const {
<div class="content-scroll-area">
<section v-if="activeSection === 'profiles'" class="content-section">
<div v-if="openProfileError" class="inline-error">
{{ openProfileError }}
</div>
<div class="sort-bar">
<SortDropdown
v-model="profileSortKey"
@@ -138,7 +145,21 @@ const {
<div class="profile-body">
<div class="profile-topline">
<h4>{{ profile.name }}</h4>
<span class="badge neutral">{{ profile.id }}</span>
<div class="profile-actions">
<button
class="card-action-button"
:disabled="isOpeningProfile(currentBrowser.browserId, profile.id)"
type="button"
@click="openBrowserProfile(currentBrowser.browserId, profile.id)"
>
{{
isOpeningProfile(currentBrowser.browserId, profile.id)
? "Opening..."
: "Open"
}}
</button>
<span class="badge neutral">{{ profile.id }}</span>
</div>
</div>
<p class="profile-email">{{ profile.email || "No email found" }}</p>
</div>

View File

@@ -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<ScanResponse>({ browsers: [] });
const selectedBrowserId = ref("");
const activeSection = ref<ActiveSection>("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,

View File

@@ -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;