diff options
| author | Filip <[email protected]> | 2026-02-11 16:18:44 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-11 09:18:44 -0600 |
| commit | eef3ae3e1f73eca103892a758d44e24d3c725d81 (patch) | |
| tree | b076ba087302927daf732bb766ae545790aad4c6 /packages/desktop/src-tauri | |
| parent | fc88dde63f1c4a7f547bbe0634c68c5ce9fc787c (diff) | |
| download | opencode-eef3ae3e1f73eca103892a758d44e24d3c725d81.tar.gz opencode-eef3ae3e1f73eca103892a758d44e24d3c725d81.zip | |
Fix/reverception (#13166)
Co-authored-by: Adam <[email protected]>
Diffstat (limited to 'packages/desktop/src-tauri')
| -rw-r--r-- | packages/desktop/src-tauri/src/cli.rs | 138 | ||||
| -rw-r--r-- | packages/desktop/src-tauri/src/constants.rs | 1 | ||||
| -rw-r--r-- | packages/desktop/src-tauri/src/lib.rs | 485 | ||||
| -rw-r--r-- | packages/desktop/src-tauri/src/server.rs | 45 | ||||
| -rw-r--r-- | packages/desktop/src-tauri/src/windows.rs | 11 |
5 files changed, 323 insertions, 357 deletions
diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index 48d9276a1..b9e1ed4bd 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -3,8 +3,11 @@ use tauri_plugin_shell::{ ShellExt, process::{Command, CommandChild, CommandEvent, TerminatedPayload}, }; +use tauri_plugin_store::StoreExt; use tokio::sync::oneshot; +use crate::constants::{SETTINGS_STORE, WSL_ENABLED_KEY}; + const CLI_INSTALL_DIR: &str = ".opencode/bin"; const CLI_BINARY_NAME: &str = "opencode"; @@ -20,7 +23,7 @@ pub struct Config { } pub async fn get_config(app: &AppHandle) -> Option<Config> { - create_command(app, "debug config") + create_command(app, "debug config", &[]) .output() .await .inspect_err(|e| tracing::warn!("Failed to read OC config: {e}")) @@ -149,25 +152,106 @@ fn get_user_shell() -> String { std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()) } -pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command { +fn is_wsl_enabled(app: &tauri::AppHandle) -> bool { + let Ok(store) = app.store(SETTINGS_STORE) else { + return false; + }; + + store + .get(WSL_ENABLED_KEY) + .as_ref() + .and_then(|value| value.as_bool()) + .unwrap_or(false) +} + +fn shell_escape(input: &str) -> String { + if input.is_empty() { + return "''".to_string(); + } + + let mut escaped = String::from("'"); + escaped.push_str(&input.replace("'", "'\"'\"'")); + escaped.push('\''); + escaped +} + +pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, String)]) -> Command { let state_dir = app .path() .resolve("", BaseDirectory::AppLocalData) .expect("Failed to resolve app local data dir"); - #[cfg(target_os = "windows")] - return app - .shell() - .sidecar("opencode-cli") - .unwrap() - .args(args.split_whitespace()) - .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true") - .env("OPENCODE_EXPERIMENTAL_FILEWATCHER", "true") - .env("OPENCODE_CLIENT", "desktop") - .env("XDG_STATE_HOME", &state_dir); - - #[cfg(not(target_os = "windows"))] - return { + let mut envs = vec![ + ( + "OPENCODE_EXPERIMENTAL_ICON_DISCOVERY".to_string(), + "true".to_string(), + ), + ( + "OPENCODE_EXPERIMENTAL_FILEWATCHER".to_string(), + "true".to_string(), + ), + ("OPENCODE_CLIENT".to_string(), "desktop".to_string()), + ( + "XDG_STATE_HOME".to_string(), + state_dir.to_string_lossy().to_string(), + ), + ]; + envs.extend( + extra_env + .iter() + .map(|(key, value)| (key.to_string(), value.clone())), + ); + + if cfg!(windows) { + if is_wsl_enabled(app) { + tracing::info!("WSL is enabled, spawning CLI server in WSL"); + let version = app.package_info().version.to_string(); + let mut script = vec![ + "set -e".to_string(), + "BIN=\"$HOME/.opencode/bin/opencode\"".to_string(), + "if [ ! -x \"$BIN\" ]; then".to_string(), + format!( + " curl -fsSL https://opencode.ai/install | bash -s -- --version {} --no-modify-path", + shell_escape(&version) + ), + "fi".to_string(), + ]; + + let mut env_prefix = vec![ + "OPENCODE_EXPERIMENTAL_ICON_DISCOVERY=true".to_string(), + "OPENCODE_EXPERIMENTAL_FILEWATCHER=true".to_string(), + "OPENCODE_CLIENT=desktop".to_string(), + "XDG_STATE_HOME=\"$HOME/.local/state\"".to_string(), + ]; + env_prefix.extend( + envs.iter() + .filter(|(key, _)| key != "OPENCODE_EXPERIMENTAL_ICON_DISCOVERY") + .filter(|(key, _)| key != "OPENCODE_EXPERIMENTAL_FILEWATCHER") + .filter(|(key, _)| key != "OPENCODE_CLIENT") + .filter(|(key, _)| key != "XDG_STATE_HOME") + .map(|(key, value)| format!("{}={}", key, shell_escape(value))), + ); + + script.push(format!("{} exec \"$BIN\" {}", env_prefix.join(" "), args)); + + return app + .shell() + .command("wsl") + .args(["-e", "bash", "-lc", &script.join("\n")]); + } else { + let mut cmd = app + .shell() + .sidecar("opencode-cli") + .unwrap() + .args(args.split_whitespace()); + + for (key, value) in envs { + cmd = cmd.env(key, value); + } + + return cmd; + } + } else { let sidecar = get_sidecar_path(app); let shell = get_user_shell(); @@ -177,14 +261,14 @@ pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command { format!("\"{}\" {}", sidecar.display(), args) }; - app.shell() - .command(&shell) - .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true") - .env("OPENCODE_EXPERIMENTAL_FILEWATCHER", "true") - .env("OPENCODE_CLIENT", "desktop") - .env("XDG_STATE_HOME", &state_dir) - .args(["-il", "-c", &cmd]) - }; + let mut cmd = app.shell().command(&shell).args(["-il", "-c", &cmd]); + + for (key, value) in envs { + cmd = cmd.env(key, value); + } + + cmd + } } pub fn serve( @@ -197,12 +281,16 @@ pub fn serve( tracing::info!(port, "Spawning sidecar"); + let envs = [ + ("OPENCODE_SERVER_USERNAME", "opencode".to_string()), + ("OPENCODE_SERVER_PASSWORD", password.to_string()), + ]; + let (mut rx, child) = create_command( app, format!("--print-logs --log-level WARN serve --hostname {hostname} --port {port}").as_str(), + &envs, ) - .env("OPENCODE_SERVER_USERNAME", "opencode") - .env("OPENCODE_SERVER_PASSWORD", password) .spawn() .expect("Failed to spawn opencode"); diff --git a/packages/desktop/src-tauri/src/constants.rs b/packages/desktop/src-tauri/src/constants.rs index a75495b54..9d50d00e2 100644 --- a/packages/desktop/src-tauri/src/constants.rs +++ b/packages/desktop/src-tauri/src/constants.rs @@ -2,6 +2,7 @@ use tauri_plugin_window_state::StateFlags; pub const SETTINGS_STORE: &str = "opencode.settings.dat"; pub const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; +pub const WSL_ENABLED_KEY: &str = "wslEnabled"; pub const UPDATER_ENABLED: bool = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some(); pub fn window_state_flags() -> StateFlags { diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 6a8e5e5dc..e0187a76b 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -52,6 +52,13 @@ enum InitStep { Done, } +#[derive(serde::Deserialize, specta::Type)] +#[serde(rename_all = "snake_case")] +enum WslPathMode { + Windows, + Linux, +} + struct InitState { current: watch::Receiver<InitStep>, } @@ -155,211 +162,28 @@ fn check_app_exists(app_name: &str) -> bool { } #[cfg(target_os = "windows")] -fn check_windows_app(app_name: &str) -> bool { - resolve_windows_app_path(app_name).is_some() +fn check_windows_app(_app_name: &str) -> bool { + // Check if command exists in PATH, including .exe + return true; } #[cfg(target_os = "windows")] fn resolve_windows_app_path(app_name: &str) -> Option<String> { use std::path::{Path, PathBuf}; - fn expand_env(value: &str) -> String { - let mut out = String::with_capacity(value.len()); - let mut index = 0; - - while let Some(start) = value[index..].find('%') { - let start = index + start; - out.push_str(&value[index..start]); - - let Some(end_rel) = value[start + 1..].find('%') else { - out.push_str(&value[start..]); - return out; - }; - - let end = start + 1 + end_rel; - let key = &value[start + 1..end]; - if key.is_empty() { - out.push('%'); - index = end + 1; - continue; - } - - if let Ok(v) = std::env::var(key) { - out.push_str(&v); - index = end + 1; - continue; - } - - out.push_str(&value[start..=end]); - index = end + 1; - } - - out.push_str(&value[index..]); - out - } - - fn extract_exe(value: &str) -> Option<String> { - let value = value.trim(); - if value.is_empty() { - return None; - } - - if let Some(rest) = value.strip_prefix('"') { - if let Some(end) = rest.find('"') { - let inner = rest[..end].trim(); - if inner.to_ascii_lowercase().contains(".exe") { - return Some(inner.to_string()); - } - } - } - - let lower = value.to_ascii_lowercase(); - let end = lower.find(".exe")?; - Some(value[..end + 4].trim().trim_matches('"').to_string()) - } - - fn candidates(app_name: &str) -> Vec<String> { - let app_name = app_name.trim().trim_matches('"'); - if app_name.is_empty() { - return vec![]; - } - - let mut out = Vec::<String>::new(); - let mut push = |value: String| { - let value = value.trim().trim_matches('"').to_string(); - if value.is_empty() { - return; - } - if out.iter().any(|v| v.eq_ignore_ascii_case(&value)) { - return; - } - out.push(value); - }; - - push(app_name.to_string()); - - let lower = app_name.to_ascii_lowercase(); - if !lower.ends_with(".exe") { - push(format!("{app_name}.exe")); - } - - let snake = { - let mut s = String::new(); - let mut underscore = false; - for c in lower.chars() { - if c.is_ascii_alphanumeric() { - s.push(c); - underscore = false; - continue; - } - if underscore { - continue; - } - s.push('_'); - underscore = true; - } - s.trim_matches('_').to_string() - }; - - if !snake.is_empty() { - push(snake.clone()); - if !snake.ends_with(".exe") { - push(format!("{snake}.exe")); - } - } - - let alnum = lower - .chars() - .filter(|c| c.is_ascii_alphanumeric()) - .collect::<String>(); - - if !alnum.is_empty() { - push(alnum.clone()); - push(format!("{alnum}.exe")); - } - - match lower.as_str() { - "sublime text" | "sublime-text" | "sublime_text" | "sublime text.exe" => { - push("subl".to_string()); - push("subl.exe".to_string()); - push("sublime_text".to_string()); - push("sublime_text.exe".to_string()); - } - _ => {} - } + // Try to find the command using 'where' + let output = Command::new("where").arg(app_name).output().ok()?; - out - } - - fn reg_app_path(exe: &str) -> Option<String> { - let exe = exe.trim().trim_matches('"'); - if exe.is_empty() { - return None; - } - - let keys = [ - format!( - r"HKCU\Software\Microsoft\Windows\CurrentVersion\App Paths\{exe}" - ), - format!( - r"HKLM\Software\Microsoft\Windows\CurrentVersion\App Paths\{exe}" - ), - format!( - r"HKLM\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\{exe}" - ), - ]; - - for key in keys { - let Some(output) = Command::new("reg") - .args(["query", &key, "/ve"]) - .output() - .ok() - else { - continue; - }; - - if !output.status.success() { - continue; - } - - let stdout = String::from_utf8_lossy(&output.stdout); - for line in stdout.lines() { - let tokens = line.split_whitespace().collect::<Vec<_>>(); - let Some(index) = tokens.iter().position(|v| v.starts_with("REG_")) else { - continue; - }; - - let value = tokens[index + 1..].join(" "); - let Some(exe) = extract_exe(&value) else { - continue; - }; - - let exe = expand_env(&exe); - let path = Path::new(exe.trim().trim_matches('"')); - if path.exists() { - return Some(path.to_string_lossy().to_string()); - } - } - } - - None - } - - let app_name = app_name.trim().trim_matches('"'); - if app_name.is_empty() { + if !output.status.success() { return None; } - let direct = Path::new(app_name); - if direct.is_absolute() && direct.exists() { - return Some(direct.to_string_lossy().to_string()); - } - - let key = app_name - .chars() - .filter(|v| v.is_ascii_alphanumeric()) - .flat_map(|v| v.to_lowercase()) - .collect::<String>(); + let paths = String::from_utf8_lossy(&output.stdout) + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(PathBuf::from) + .collect::<Vec<_>>(); let has_ext = |path: &Path, ext: &str| { path.extension() @@ -368,19 +192,22 @@ fn resolve_windows_app_path(app_name: &str) -> Option<String> { .unwrap_or(false) }; + if let Some(path) = paths.iter().find(|path| has_ext(path, "exe")) { + return Some(path.to_string_lossy().to_string()); + } + let resolve_cmd = |path: &Path| -> Option<String> { - let bytes = std::fs::read(path).ok()?; - let content = String::from_utf8_lossy(&bytes); + let content = std::fs::read_to_string(path).ok()?; for token in content.split('"') { - let Some(exe) = extract_exe(token) else { + let lower = token.to_ascii_lowercase(); + if !lower.contains(".exe") { continue; - }; + } - let lower = exe.to_ascii_lowercase(); if let Some(index) = lower.find("%~dp0") { let base = path.parent()?; - let suffix = &exe[index + 5..]; + let suffix = &token[index + 5..]; let mut resolved = PathBuf::from(base); for part in suffix.replace('/', "\\").split('\\') { @@ -397,11 +224,9 @@ fn resolve_windows_app_path(app_name: &str) -> Option<String> { if resolved.exists() { return Some(resolved.to_string_lossy().to_string()); } - - continue; } - let resolved = PathBuf::from(expand_env(&exe)); + let resolved = PathBuf::from(token); if resolved.exists() { return Some(resolved.to_string_lossy().to_string()); } @@ -410,130 +235,74 @@ fn resolve_windows_app_path(app_name: &str) -> Option<String> { None }; - let resolve_where = |query: &str| -> Option<String> { - let output = Command::new("where").arg(query).output().ok()?; - if !output.status.success() { - return None; - } - - let paths = String::from_utf8_lossy(&output.stdout) - .lines() - .map(str::trim) - .filter(|line| !line.is_empty()) - .map(PathBuf::from) - .collect::<Vec<_>>(); - - if paths.is_empty() { - return None; - } - - if let Some(path) = paths.iter().find(|path| has_ext(path, "exe")) { - return Some(path.to_string_lossy().to_string()); + for path in &paths { + if has_ext(path, "cmd") || has_ext(path, "bat") { + if let Some(resolved) = resolve_cmd(path) { + return Some(resolved); + } } - for path in &paths { - if has_ext(path, "cmd") || has_ext(path, "bat") { - if let Some(resolved) = resolve_cmd(path) { + if path.extension().is_none() { + let cmd = path.with_extension("cmd"); + if cmd.exists() { + if let Some(resolved) = resolve_cmd(&cmd) { return Some(resolved); } } - if path.extension().is_none() { - let cmd = path.with_extension("cmd"); - if cmd.exists() { - if let Some(resolved) = resolve_cmd(&cmd) { - return Some(resolved); - } - } - - let bat = path.with_extension("bat"); - if bat.exists() { - if let Some(resolved) = resolve_cmd(&bat) { - return Some(resolved); - } + let bat = path.with_extension("bat"); + if bat.exists() { + if let Some(resolved) = resolve_cmd(&bat) { + return Some(resolved); } } } + } - if !key.is_empty() { - for path in &paths { - let dirs = [ - path.parent(), - path.parent().and_then(|dir| dir.parent()), - path.parent() - .and_then(|dir| dir.parent()) - .and_then(|dir| dir.parent()), - ]; - - for dir in dirs.into_iter().flatten() { - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - let candidate = entry.path(); - if !has_ext(&candidate, "exe") { - continue; - } + let key = app_name + .chars() + .filter(|v| v.is_ascii_alphanumeric()) + .flat_map(|v| v.to_lowercase()) + .collect::<String>(); - let Some(stem) = candidate.file_stem().and_then(|v| v.to_str()) else { - continue; - }; + if !key.is_empty() { + for path in &paths { + let dirs = [ + path.parent(), + path.parent().and_then(|dir| dir.parent()), + path.parent() + .and_then(|dir| dir.parent()) + .and_then(|dir| dir.parent()), + ]; + + for dir in dirs.into_iter().flatten() { + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let candidate = entry.path(); + if !has_ext(&candidate, "exe") { + continue; + } - let name = stem - .chars() - .filter(|v| v.is_ascii_alphanumeric()) - .flat_map(|v| v.to_lowercase()) - .collect::<String>(); + let Some(stem) = candidate.file_stem().and_then(|v| v.to_str()) else { + continue; + }; - if name.contains(&key) || key.contains(&name) { - return Some(candidate.to_string_lossy().to_string()); - } + let name = stem + .chars() + .filter(|v| v.is_ascii_alphanumeric()) + .flat_map(|v| v.to_lowercase()) + .collect::<String>(); + + if name.contains(&key) || key.contains(&name) { + return Some(candidate.to_string_lossy().to_string()); } } } } } - - paths.first().map(|path| path.to_string_lossy().to_string()) - }; - - let list = candidates(app_name); - for query in &list { - if let Some(path) = resolve_where(query) { - return Some(path); - } - } - - let mut exes = Vec::<String>::new(); - for query in &list { - let query = query.trim().trim_matches('"'); - if query.is_empty() { - continue; - } - - let name = Path::new(query) - .file_name() - .and_then(|v| v.to_str()) - .unwrap_or(query); - - let exe = if name.to_ascii_lowercase().ends_with(".exe") { - name.to_string() - } else { - format!("{name}.exe") - }; - - if exes.iter().any(|v| v.eq_ignore_ascii_case(&exe)) { - continue; - } - - exes.push(exe); - } - - for exe in exes { - if let Some(path) = reg_app_path(&exe) { - return Some(path); - } } - None + paths.first().map(|path| path.to_string_lossy().to_string()) } #[tauri::command] @@ -620,32 +389,50 @@ fn check_linux_app(app_name: &str) -> bool { return true; } +#[tauri::command] +#[specta::specta] +fn wsl_path(path: String, mode: Option<WslPathMode>) -> Result<String, String> { + if !cfg!(windows) { + return Ok(path); + } + + let flag = match mode.unwrap_or(WslPathMode::Linux) { + WslPathMode::Windows => "-w", + WslPathMode::Linux => "-u", + }; + + let output = if path.starts_with('~') { + let suffix = path.strip_prefix('~').unwrap_or(""); + let escaped = suffix.replace('"', "\\\""); + let cmd = format!("wslpath {flag} \"$HOME{escaped}\""); + Command::new("wsl") + .args(["-e", "sh", "-lc", &cmd]) + .output() + .map_err(|e| format!("Failed to run wslpath: {e}"))? + } else { + Command::new("wsl") + .args(["-e", "wslpath", flag, &path]) + .output() + .map_err(|e| format!("Failed to run wslpath: {e}"))? + }; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + if stderr.is_empty() { + return Err("wslpath failed".to_string()); + } + return Err(stderr); + } + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let builder = tauri_specta::Builder::<tauri::Wry>::new() - // Then register them (separated by a comma) - .commands(tauri_specta::collect_commands![ - kill_sidecar, - cli::install_cli, - await_initialization, - server::get_default_server_url, - server::set_default_server_url, - get_display_backend, - set_display_backend, - markdown::parse_markdown_command, - check_app_exists, - resolve_app_path - ]) - .events(tauri_specta::collect_events![LoadingWindowComplete]) - .error_handling(tauri_specta::ErrorHandlingMode::Throw); + let builder = make_specta_builder(); #[cfg(debug_assertions)] // <- Only export on non-release builds - builder - .export( - specta_typescript::Typescript::default(), - "../src/bindings.ts", - ) - .expect("Failed to export typescript bindings"); + export_types(&builder); #[cfg(all(target_os = "macos", not(debug_assertions)))] let _ = std::process::Command::new("killall") @@ -712,6 +499,44 @@ pub fn run() { }); } +fn make_specta_builder() -> tauri_specta::Builder<tauri::Wry> { + tauri_specta::Builder::<tauri::Wry>::new() + // Then register them (separated by a comma) + .commands(tauri_specta::collect_commands![ + kill_sidecar, + cli::install_cli, + await_initialization, + server::get_default_server_url, + server::set_default_server_url, + server::get_wsl_config, + server::set_wsl_config, + get_display_backend, + set_display_backend, + markdown::parse_markdown_command, + check_app_exists, + wsl_path, + resolve_app_path + ]) + .events(tauri_specta::collect_events![LoadingWindowComplete]) + .error_handling(tauri_specta::ErrorHandlingMode::Throw) +} + +fn export_types(builder: &tauri_specta::Builder<tauri::Wry>) { + builder + .export( + specta_typescript::Typescript::default(), + "../src/bindings.ts", + ) + .expect("Failed to export typescript bindings"); +} + +#[cfg(test)] +#[test] +fn test_export_types() { + let builder = make_specta_builder(); + export_types(&builder); +} + #[derive(tauri_specta::Event, serde::Deserialize, specta::Type)] struct LoadingWindowComplete; diff --git a/packages/desktop/src-tauri/src/server.rs b/packages/desktop/src-tauri/src/server.rs index 81382f100..81e0595af 100644 --- a/packages/desktop/src-tauri/src/server.rs +++ b/packages/desktop/src-tauri/src/server.rs @@ -8,9 +8,20 @@ use tokio::task::JoinHandle; use crate::{ cli, - constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE}, + constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE, WSL_ENABLED_KEY}, }; +#[derive(Clone, serde::Serialize, serde::Deserialize, specta::Type, Debug)] +pub struct WslConfig { + pub enabled: bool, +} + +impl Default for WslConfig { + fn default() -> Self { + Self { enabled: false } + } +} + #[tauri::command] #[specta::specta] pub fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> { @@ -48,6 +59,38 @@ pub async fn set_default_server_url(app: AppHandle, url: Option<String>) -> Resu Ok(()) } +#[tauri::command] +#[specta::specta] +pub fn get_wsl_config(app: AppHandle) -> Result<WslConfig, String> { + let store = app + .store(SETTINGS_STORE) + .map_err(|e| format!("Failed to open settings store: {}", e))?; + + let enabled = store + .get(WSL_ENABLED_KEY) + .as_ref() + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + Ok(WslConfig { enabled }) +} + +#[tauri::command] +#[specta::specta] +pub fn set_wsl_config(app: AppHandle, config: WslConfig) -> Result<(), String> { + let store = app + .store(SETTINGS_STORE) + .map_err(|e| format!("Failed to open settings store: {}", e))?; + + store.set(WSL_ENABLED_KEY, serde_json::Value::Bool(config.enabled)); + + store + .save() + .map_err(|e| format!("Failed to save settings: {}", e))?; + + Ok(()) +} + pub async fn get_saved_server_url(app: &tauri::AppHandle) -> Option<String> { if let Some(url) = get_default_server_url(app.clone()).ok().flatten() { tracing::info!(%url, "Using desktop-specific custom URL"); diff --git a/packages/desktop/src-tauri/src/windows.rs b/packages/desktop/src-tauri/src/windows.rs index cf3e399e3..2ddcb0506 100644 --- a/packages/desktop/src-tauri/src/windows.rs +++ b/packages/desktop/src-tauri/src/windows.rs @@ -1,4 +1,7 @@ -use crate::constants::{UPDATER_ENABLED, window_state_flags}; +use crate::{ + constants::{UPDATER_ENABLED, window_state_flags}, + server::get_wsl_config, +}; use std::{ops::Deref, time::Duration}; use tauri::{AppHandle, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; use tauri_plugin_window_state::AppHandleExt; @@ -22,6 +25,11 @@ impl MainWindow { return Ok(Self(window)); } + let wsl_enabled = get_wsl_config(app.clone()) + .ok() + .map(|v| v.enabled) + .unwrap_or(false); + let window_builder = base_window_config( WebviewWindowBuilder::new(app, Self::LABEL, WebviewUrl::App("/".into())), app, @@ -36,6 +44,7 @@ impl MainWindow { r#" window.__OPENCODE__ ??= {{}}; window.__OPENCODE__.updaterEnabled = {UPDATER_ENABLED}; + window.__OPENCODE__.wsl = {wsl_enabled}; "# )); |
