Added an assistant to view the AI Studio logs (#859)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Paul KoudelkaandThorsten Sommer authored and GitHub committed 2026-07-15 19:44:43 +02:00
1 parent 79fbeb636c
commit b1459523d9
22 files changed
+1683 -3

No files matched your search

+17
View File
@@ -242,6 +242,20 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "ashpd"
version = "0.13.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "281e6645758940dee594495e28807a7672ce40f11ebf4df6c22c4fcd59e2689f"
dependencies = [
"enumflags2",
"futures-util",
"getrandom 0.4.2",
"serde",
"tokio",
"zbus",
]
[[package]]
name = "asn1-rs"
version = "0.7.1"
@@ -4072,6 +4086,7 @@ dependencies = [
"aes 0.9.1",
"apple-native-keyring-store",
"arboard",
"ashpd",
"async-stream",
"axum",
"axum-server",
@@ -7664,6 +7679,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.61.2",
]
@@ -9566,6 +9582,7 @@ dependencies = [
"rustix 1.1.4",
"serde",
"serde_repr",
"tokio",
"tracing",
"uds_windows",
"uuid",
+1
View File
@@ -72,6 +72,7 @@ windows-native-keyring-store = "1.1.0"
apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] }
[target.'cfg(target_os = "linux")'.dependencies]
ashpd = { version = "0.13.12", default-features = false, features = ["tokio", "open_uri"] }
dbus-secret-service-keyring-store = { version = "1.0.0", features = ["crypto-rust"] }
webkit2gtk = { version = "2.0.2", features = ["v2_8"] }
+280 -1
View File
@@ -2,10 +2,24 @@
use axum::extract::Query;
use axum::Json;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tauri_plugin_dialog::{DialogExt, FileDialogBuilder};
use crate::api_token::APIToken;
use crate::app_window::MAIN_WINDOW;
#[cfg(any(windows, target_os = "macos"))]
use std::process::Command;
#[cfg(target_os = "linux")]
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest};
#[cfg(windows)]
use std::os::windows::process::CommandExt;
/// Microsoft documents CREATE_NO_WINDOW as a process creation flag with value 0x08000000.
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;
#[derive(Clone, Deserialize)]
pub struct PreviousDirectory {
path: String,
@@ -36,6 +50,11 @@ pub struct SaveFileOptions {
filter: Option<FileTypeFilter>,
}
#[derive(Clone, Deserialize)]
pub struct OpenPathOptions {
path: String,
}
#[derive(Serialize)]
pub struct DirectorySelectionResponse {
user_cancelled: bool,
@@ -60,6 +79,12 @@ pub struct FileSaveResponse {
save_file_path: String,
}
#[derive(Serialize)]
pub struct OpenPathResponse {
success: bool,
issue: String,
}
#[derive(Clone, Deserialize)]
pub struct PreviousFile {
file_path: String,
@@ -286,6 +311,79 @@ pub async fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json
}
}
pub async fn open_path_in_file_manager(
_token: APIToken,
payload: Json<OpenPathOptions>,
) -> Json<OpenPathResponse> {
let requested_path = PathBuf::from(payload.path.trim());
if requested_path.as_os_str().is_empty() {
return Json(OpenPathResponse {
success: false,
issue: String::from("The path is empty."),
});
}
let Some(target) = resolve_file_manager_target(&requested_path) else {
let issue = format!(
"The path does not exist and its parent folder could not be found: {}",
requested_path.to_string_lossy(),
);
error!(Source = "Tauri"; "{issue}");
return Json(OpenPathResponse {
success: false,
issue,
});
};
#[cfg(target_os = "linux")]
{
return match open_path_in_linux_file_manager(&target).await {
Ok(()) => {
info!("Opened file manager for path: {:?}", target.path);
Json(OpenPathResponse {
success: true,
issue: String::new(),
})
}
Err(issue) => {
error!(Source = "Tauri"; "{issue}");
Json(OpenPathResponse {
success: false,
issue,
})
}
};
}
#[cfg(any(windows, target_os = "macos"))]
{
let mut command = create_file_manager_command(&target);
#[cfg(windows)]
command.creation_flags(CREATE_NO_WINDOW);
match command.spawn() {
Ok(_) => {
info!("Opened file manager for path: {:?}", target.path);
Json(OpenPathResponse {
success: true,
issue: String::new(),
})
}
Err(error) => {
let issue = format!("Failed to open the file manager: {error}");
error!(Source = "Tauri"; "{issue}");
Json(OpenPathResponse {
success: false,
issue,
})
}
}
}
}
/// Applies an optional file type filter to a FileDialogBuilder.
fn apply_filter<R: tauri::Runtime>(file_dialog: FileDialogBuilder<R>, filter: &Option<FileTypeFilter>) -> FileDialogBuilder<R> {
match filter {
@@ -296,4 +394,185 @@ fn apply_filter<R: tauri::Runtime>(file_dialog: FileDialogBuilder<R>, filter: &O
None => file_dialog,
}
}
}
#[derive(Debug, PartialEq, Eq)]
struct FileManagerTarget {
path: PathBuf,
reveal_file: bool,
}
#[cfg(any(target_os = "linux", test))]
#[derive(Debug, PartialEq, Eq)]
enum LinuxPortalOperation {
RevealFile,
OpenDirectory,
}
fn resolve_file_manager_target(requested_path: &Path) -> Option<FileManagerTarget> {
if requested_path.is_file() {
return Some(FileManagerTarget {
path: requested_path.to_path_buf(),
reveal_file: true,
});
}
if requested_path.is_dir() {
return Some(FileManagerTarget {
path: requested_path.to_path_buf(),
reveal_file: false,
});
}
requested_path.parent()
.filter(|parent| parent.is_dir())
.map(|parent| FileManagerTarget {
path: parent.to_path_buf(),
reveal_file: false,
})
}
#[cfg(any(target_os = "linux", test))]
fn linux_portal_operation(target: &FileManagerTarget) -> LinuxPortalOperation {
if target.reveal_file {
LinuxPortalOperation::RevealFile
} else {
LinuxPortalOperation::OpenDirectory
}
}
#[cfg(any(target_os = "linux", test))]
fn xdg_open_fallback_path(target: &FileManagerTarget) -> &Path {
if target.reveal_file {
target.path.parent().unwrap_or(&target.path)
} else {
&target.path
}
}
#[cfg(target_os = "linux")]
enum LinuxPortalError {
Unavailable(String),
RequestFailed(String),
}
#[cfg(target_os = "linux")]
async fn open_path_with_linux_portal(target: &FileManagerTarget) -> Result<(), LinuxPortalError> {
let file = std::fs::File::open(&target.path)
.map_err(|error| LinuxPortalError::Unavailable(format!("Failed to open the path for the desktop portal: {error}")))?;
let request = match linux_portal_operation(target) {
LinuxPortalOperation::RevealFile => OpenDirectoryRequest::default().send(&file).await,
LinuxPortalOperation::OpenDirectory => OpenFileRequest::default().send_file(&file).await,
}
.map_err(|error| LinuxPortalError::Unavailable(format!("Desktop portal invocation failed: {error}")))?;
request.response()
.map_err(|error| LinuxPortalError::RequestFailed(format!("Desktop portal request failed: {error}")))
}
#[cfg(target_os = "linux")]
async fn open_path_with_xdg_open(target: &FileManagerTarget) -> Result<(), String> {
let fallback_path = xdg_open_fallback_path(target);
let status = tokio::process::Command::new("xdg-open")
.arg(fallback_path)
.status()
.await
.map_err(|error| format!("xdg-open failed to start for '{}': {error}", fallback_path.to_string_lossy()))?;
if status.success() {
Ok(())
} else {
Err(format!("xdg-open failed for '{}' with exit status {status}", fallback_path.to_string_lossy()))
}
}
#[cfg(target_os = "linux")]
async fn open_path_in_linux_file_manager(target: &FileManagerTarget) -> Result<(), String> {
match open_path_with_linux_portal(target).await {
Ok(()) => Ok(()),
Err(LinuxPortalError::RequestFailed(error)) => Err(error),
Err(LinuxPortalError::Unavailable(portal_error)) => {
match open_path_with_xdg_open(target).await {
Ok(()) => Ok(()),
Err(fallback_error) => Err(format!("{portal_error} Fallback failed: {fallback_error}")),
}
}
}
}
#[cfg(target_os = "windows")]
fn create_file_manager_command(target: &FileManagerTarget) -> Command {
let mut command = Command::new("explorer.exe");
if target.reveal_file {
command.arg(format!("/select,{}", target.path.to_string_lossy()));
} else {
command.arg(&target.path);
}
command
}
#[cfg(target_os = "macos")]
fn create_file_manager_command(target: &FileManagerTarget) -> Command {
let mut command = Command::new("open");
if target.reveal_file {
command.arg("-R");
}
command.arg(&target.path);
command
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn existing_file_is_revealed_and_falls_back_to_its_parent() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("application.log");
fs::write(&file_path, "log").unwrap();
let target = resolve_file_manager_target(&file_path).unwrap();
assert_eq!(target.path, file_path);
assert!(target.reveal_file);
assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::RevealFile);
assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
}
#[test]
fn existing_directory_is_opened_directly() {
let temp_dir = tempfile::tempdir().unwrap();
let target = resolve_file_manager_target(temp_dir.path()).unwrap();
assert_eq!(target.path, temp_dir.path());
assert!(!target.reveal_file);
assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::OpenDirectory);
assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
}
#[test]
fn missing_file_uses_its_existing_parent_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let missing_file = temp_dir.path().join("missing.log");
let target = resolve_file_manager_target(&missing_file).unwrap();
assert_eq!(target.path, temp_dir.path());
assert!(!target.reveal_file);
assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::OpenDirectory);
assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
}
#[test]
fn invalid_path_without_existing_parent_is_rejected() {
let temp_dir = tempfile::tempdir().unwrap();
let invalid_path = temp_dir.path().join("missing-directory").join("missing.log");
assert!(resolve_file_manager_target(&invalid_path).is_none());
}
}
+1
View File
@@ -46,6 +46,7 @@ pub fn start_runtime_api() {
.route("/select/file", post(crate::file_actions::select_file))
.route("/select/files", post(crate::file_actions::select_files))
.route("/save/file", post(crate::file_actions::save_file))
.route("/open/path", post(crate::file_actions::open_path_in_file_manager))
.route("/secrets/get", post(crate::secret::get_secret))
.route("/secrets/store", post(crate::secret::store_secret))
.route("/secrets/delete", post(crate::secret::delete_secret))