feat: implement path normalization across commands

This commit is contained in:
Bohdan Triapitsyn
2025-12-25 01:25:52 +02:00
parent 359cfd45b1
commit b806b01c29
9 changed files with 205 additions and 64 deletions
@@ -1,4 +1,5 @@
use crate::{DesktopRuntime, SettingsStore};
use crate::path_utils::expand_tilde_path;
use serde::Serialize;
use std::{
collections::{HashSet, VecDeque},
@@ -346,7 +347,7 @@ async fn resolve_sandboxed_path(
.filter(|value| !value.is_empty());
let candidate_path = match (candidate_input, workspace_root) {
(Some(value), _) => PathBuf::from(value),
(Some(value), _) => expand_tilde_path(value),
(None, Some(root)) => root.clone(),
(None, None) => default_home_directory(),
};
@@ -376,7 +377,7 @@ async fn resolve_creatable_path(
path: &str,
workspace_root: Option<&PathBuf>,
) -> Result<PathBuf, FsCommandError> {
let candidate = PathBuf::from(path);
let candidate = expand_tilde_path(path);
if candidate.as_os_str().is_empty() {
return Err(FsCommandError::Other("Path is required".to_string()));
}
@@ -1,4 +1,5 @@
use crate::{DesktopRuntime, SettingsStore};
use crate::path_utils::expand_tilde_path;
use anyhow::{anyhow, Context, Result};
use log::{error, info, warn};
use regex::Regex;
@@ -357,7 +358,7 @@ fn append_git_option_map(args: &mut Vec<String>, map: &serde_json::Map<String, V
// Removed unused resolve_workspace_root function
async fn validate_git_path(path: &str, _settings: &SettingsStore) -> Result<PathBuf> {
let path_buf = PathBuf::from(path);
let path_buf = expand_tilde_path(path);
if !path_buf.exists() {
return Err(anyhow!("Directory does not exist: {}", path));
}
@@ -4,6 +4,7 @@ use tauri::AppHandle;
use tauri::State;
use crate::DesktopRuntime;
use crate::path_utils::expand_tilde_path;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -34,10 +35,12 @@ pub async fn process_directory_selection(
path: String,
state: State<'_, DesktopRuntime>,
) -> Result<DirectoryPermissionResult, String> {
use std::path::PathBuf;
// Validate directory exists
let path_buf = PathBuf::from(&path);
let mut path_buf = expand_tilde_path(&path);
if let Ok(canonicalized) = std::fs::canonicalize(&path_buf) {
path_buf = canonicalized;
}
let normalized_path = path_buf.to_string_lossy().to_string();
if !path_buf.exists() {
return Ok(DirectoryPermissionResult {
success: false,
@@ -64,7 +67,7 @@ pub async fn process_directory_selection(
if let Some(obj) = settings.as_object_mut() {
obj.insert(
"lastDirectory".to_string(),
serde_json::Value::String(path.clone()),
serde_json::Value::String(normalized_path.clone()),
);
}
@@ -76,12 +79,12 @@ pub async fn process_directory_selection(
info!(
"[permissions] Updated settings with lastDirectory: {}",
path
normalized_path
);
Ok(DirectoryPermissionResult {
success: true,
path: Some(path),
path: Some(normalized_path),
error: None,
})
}
@@ -110,7 +113,11 @@ pub async fn request_directory_access(
) -> Result<DirectoryPermissionResult, String> {
let path = request.path;
let path_buf = std::path::PathBuf::from(&path);
let mut path_buf = expand_tilde_path(&path);
if let Ok(canonicalized) = std::fs::canonicalize(&path_buf) {
path_buf = canonicalized;
}
let normalized_path = path_buf.to_string_lossy().to_string();
if !path_buf.exists() {
return Ok(DirectoryPermissionResult {
success: false,
@@ -131,7 +138,7 @@ pub async fn request_directory_access(
match std::fs::read_dir(&path_buf) {
Ok(_) => Ok(DirectoryPermissionResult {
success: true,
path: Some(path),
path: Some(normalized_path),
error: None,
}),
Err(e) => Ok(DirectoryPermissionResult {
@@ -4,6 +4,7 @@ use std::collections::HashSet;
use tauri::State;
use crate::DesktopRuntime;
use crate::path_utils::expand_tilde_path;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -105,12 +106,14 @@ fn sanitize_settings_update(payload: &Value) -> Value {
}
if let Some(Value::String(s)) = obj.get("lastDirectory") {
if !s.is_empty() {
result_obj.insert("lastDirectory".to_string(), json!(s));
let expanded = expand_tilde_path(s).to_string_lossy().to_string();
result_obj.insert("lastDirectory".to_string(), json!(expanded));
}
}
if let Some(Value::String(s)) = obj.get("homeDirectory") {
if !s.is_empty() {
result_obj.insert("homeDirectory".to_string(), json!(s));
let expanded = expand_tilde_path(s).to_string_lossy().to_string();
result_obj.insert("homeDirectory".to_string(), json!(expanded));
}
}
if let Some(Value::String(s)) = obj.get("uiFont") {
+11 -2
View File
@@ -7,6 +7,7 @@ mod session_activity;
mod opencode_config;
mod opencode_manager;
mod window_state;
mod path_utils;
use std::{collections::HashMap, path::PathBuf, sync::Arc, time::{Duration, Instant}};
@@ -63,6 +64,7 @@ use tokio::{
};
use tower_http::cors::CorsLayer;
use window_state::{load_window_state, persist_window_state, WindowStateManager};
use path_utils::expand_tilde_path;
#[cfg(target_os = "macos")]
use std::sync::atomic::{AtomicBool, Ordering};
@@ -1485,7 +1487,10 @@ async fn change_directory_handler(
return Err(StatusCode::BAD_REQUEST);
}
let resolved_path = PathBuf::from(requested_path);
let mut resolved_path = expand_tilde_path(requested_path);
if !resolved_path.is_absolute() {
resolved_path = state.opencode.get_working_directory().join(resolved_path);
}
// Validate directory exists and is accessible
match fs::metadata(&resolved_path).await {
@@ -1507,6 +1512,10 @@ async fn change_directory_handler(
}
}
if let Ok(canonicalized) = fs::canonicalize(&resolved_path).await {
resolved_path = canonicalized;
}
let current_dir = state.opencode.get_working_directory();
let is_running = state.opencode.current_port().is_some();
@@ -1682,7 +1691,7 @@ impl SettingsStore {
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(PathBuf::from);
.map(expand_tilde_path);
Ok(candidate)
}
}
@@ -0,0 +1,21 @@
use std::path::PathBuf;
pub fn expand_tilde_path(value: &str) -> PathBuf {
let trimmed = value.trim();
if trimmed.is_empty() {
return PathBuf::from(trimmed);
}
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
if trimmed == "~" {
return home;
}
if trimmed.starts_with("~/") || trimmed.starts_with("~\\") {
return home.join(&trimmed[2..]);
}
PathBuf::from(trimmed)
}