Compare commits
2 Commits
alpha-v202
...
stable-v20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5ed863d5d | ||
|
|
4f7f5a31e4 |
@@ -217,7 +217,7 @@ The main Tauri window also persists its last normal size and screen position in
|
||||
Tauri setup keeps launch-time filesystem and subprocess work off the window creation critical path. Legacy `~/Laputa` housekeeping and the initial persisted-vault MCP bridge sync run on named background threads, so large legacy vaults, stale active-vault paths, or slow process startup cannot beachball the macOS app before React mounts. React still resyncs the bridge from `useVaultSwitcher` after the persisted selection loads, and no selected vault stops the bridge. The HTML bootstrap also installs a Tauri-only one-shot watchdog: React reports readiness from an effect after the root commits, and if that readiness signal never arrives the WebView reloads once instead of leaving macOS users in an inert rendered shell.
|
||||
|
||||
Linux uses custom React-rendered window chrome instead of the native Tauri menu bar. `setup_linux_window_chrome()` drops server-side decorations on the main window, `openNoteInNewWindow()` does the same for detached note windows, and `LinuxTitlebar`/`LinuxMenuButton` route both window controls and menu actions back through the same shared command pipeline that the desktop native menus use. The native app menu keeps macOS-only Services/Hide entries off Windows and Linux, while cross-platform custom items such as Check for Updates emit Tolaria command IDs and show visible updater feedback.
|
||||
When Tolaria is launched from a Linux AppImage, `run()` also applies AppImage-only WebKitGTK startup safeguards without changing native package installs. It injects `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` independently unless the user already set either variable, and on Wayland sessions it re-execs once with the first available system `libwayland-client.so` in `LD_PRELOAD` when the user has not provided their own preload. The rendering overrides keep AppImage WebViews from blanking after accelerated compositing/DMA-BUF failures, while the re-exec addresses AppImage library-order failures that can surface as `Could not create default EGL display: EGL_BAD_PARAMETER` before GTK/WebKit create the display.
|
||||
When Tolaria is launched from a Linux AppImage, `run()` also applies AppImage-only WebKitGTK startup safeguards without changing native package installs. It injects `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` independently unless the user already set either variable, and on Wayland sessions it re-execs once with the first architecture-matching system `libwayland-client.so` in `LD_PRELOAD` when the user has not provided their own preload. The candidate order prefers Fedora-style `lib64` and Debian-style `x86_64-linux-gnu` paths before generic `/usr/lib`, and the ELF header is checked so a 64-bit Tolaria process does not retry with a 32-bit Wayland client library. The rendering overrides keep AppImage WebViews from blanking after accelerated compositing/DMA-BUF failures, while the re-exec addresses AppImage library-order failures that can surface as `Could not create default EGL display: EGL_BAD_PARAMETER` before GTK/WebKit create the display.
|
||||
|
||||
## Multi-Window (Note Windows)
|
||||
|
||||
|
||||
@@ -37,13 +37,13 @@ On some Wayland systems, the Linux AppImage may fail to launch with:
|
||||
Could not create default EGL display: EGL_BAD_PARAMETER. Aborting...
|
||||
```
|
||||
|
||||
Recent Tolaria AppImages automatically disable unstable WebKitGTK AppImage rendering paths and retry startup with the system Wayland client library when they detect this class of AppImage + Wayland environment. If you are running an older build, use this workaround:
|
||||
Recent Tolaria AppImages automatically disable unstable WebKitGTK AppImage rendering paths and retry startup with an architecture-matching system Wayland client library when they detect this class of AppImage + Wayland environment. If you are running an older build, use this workaround:
|
||||
|
||||
```bash
|
||||
WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 LD_PRELOAD=/usr/lib/libwayland-client.so ./Tolaria*.AppImage
|
||||
WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 LD_PRELOAD=/usr/lib64/libwayland-client.so.0 ./Tolaria*.AppImage
|
||||
```
|
||||
|
||||
If your distribution stores the library elsewhere, use that path instead, for example `/usr/lib64/libwayland-client.so.0` or `/usr/lib/x86_64-linux-gnu/libwayland-client.so.0`.
|
||||
If your distribution stores the 64-bit library elsewhere, use that path instead, for example `/usr/lib/x86_64-linux-gnu/libwayland-client.so.0`. On 64-bit Fedora, avoid `/usr/lib/libwayland-client.so.0`; that path can point at a 32-bit library and be ignored by the loader with a wrong ELF class warning.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ pub mod gemini_cli;
|
||||
mod gemini_config;
|
||||
mod gemini_discovery;
|
||||
pub mod git;
|
||||
#[cfg(any(test, all(desktop, target_os = "linux")))]
|
||||
mod linux_appimage;
|
||||
pub mod mcp;
|
||||
#[cfg(desktop)]
|
||||
pub mod menu;
|
||||
@@ -32,8 +34,6 @@ mod window_state;
|
||||
use std::ffi::OsStr;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
use std::os::unix::process::CommandExt;
|
||||
#[cfg(desktop)]
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(desktop)]
|
||||
@@ -65,136 +65,6 @@ struct WsBridgeChild(Mutex<Option<Child>>);
|
||||
#[cfg(desktop)]
|
||||
struct AllowedAssetScopeRoots(Mutex<Vec<PathBuf>>);
|
||||
|
||||
#[cfg(any(test, all(desktop, target_os = "linux")))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct StartupEnvOverride {
|
||||
key: &'static str,
|
||||
value: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(any(test, all(desktop, target_os = "linux")))]
|
||||
const LINUX_APPIMAGE_WEBKIT_OVERRIDES: [StartupEnvOverride; 2] = [
|
||||
StartupEnvOverride {
|
||||
key: "WEBKIT_DISABLE_DMABUF_RENDERER",
|
||||
value: "1",
|
||||
},
|
||||
StartupEnvOverride {
|
||||
key: "WEBKIT_DISABLE_COMPOSITING_MODE",
|
||||
value: "1",
|
||||
},
|
||||
];
|
||||
|
||||
#[cfg(any(test, all(desktop, target_os = "linux")))]
|
||||
const WAYLAND_CLIENT_PRELOAD_CANDIDATES: [&str; 7] = [
|
||||
"/usr/lib/libwayland-client.so",
|
||||
"/usr/lib/libwayland-client.so.0",
|
||||
"/usr/lib64/libwayland-client.so",
|
||||
"/usr/lib64/libwayland-client.so.0",
|
||||
"/lib64/libwayland-client.so.0",
|
||||
"/lib/x86_64-linux-gnu/libwayland-client.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libwayland-client.so.0",
|
||||
];
|
||||
|
||||
#[cfg(any(test, all(desktop, target_os = "linux")))]
|
||||
fn is_linux_appimage_launch<F>(mut get_var: F) -> bool
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
["APPIMAGE", "APPDIR"]
|
||||
.into_iter()
|
||||
.any(|key| get_var(key).is_some_and(|value| !value.trim().is_empty()))
|
||||
}
|
||||
|
||||
#[cfg(any(test, all(desktop, target_os = "linux")))]
|
||||
fn is_wayland_session<F>(mut get_var: F) -> bool
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
get_var("WAYLAND_DISPLAY").is_some_and(|value| !value.trim().is_empty())
|
||||
|| get_var("XDG_SESSION_TYPE")
|
||||
.is_some_and(|value| value.trim().eq_ignore_ascii_case("wayland"))
|
||||
}
|
||||
|
||||
#[cfg(any(test, all(desktop, target_os = "linux")))]
|
||||
fn linux_appimage_wayland_client_preload_path_with<F, E>(
|
||||
mut get_var: F,
|
||||
mut file_exists: E,
|
||||
) -> Option<&'static str>
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
E: FnMut(&str) -> bool,
|
||||
{
|
||||
if !is_linux_appimage_launch(&mut get_var) || !is_wayland_session(&mut get_var) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if get_var("LD_PRELOAD").is_some_and(|value| !value.trim().is_empty())
|
||||
|| get_var("TOLARIA_APPIMAGE_WAYLAND_PRELOAD_ATTEMPTED").is_some_and(|value| value == "1")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
WAYLAND_CLIENT_PRELOAD_CANDIDATES
|
||||
.into_iter()
|
||||
.find(|path| file_exists(path))
|
||||
}
|
||||
|
||||
#[cfg(any(test, all(desktop, target_os = "linux")))]
|
||||
fn linux_appimage_startup_env_overrides_with<F>(mut get_var: F) -> Vec<StartupEnvOverride>
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
if !is_linux_appimage_launch(&mut get_var) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
LINUX_APPIMAGE_WEBKIT_OVERRIDES
|
||||
.into_iter()
|
||||
.filter(|env_override| {
|
||||
!get_var(env_override.key).is_some_and(|value| !value.trim().is_empty())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
fn apply_linux_appimage_startup_env_overrides() {
|
||||
apply_linux_appimage_wayland_client_preload();
|
||||
|
||||
for env_override in linux_appimage_startup_env_overrides_with(|key| std::env::var(key).ok()) {
|
||||
std::env::set_var(env_override.key, env_override.value);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
fn apply_linux_appimage_wayland_client_preload() {
|
||||
let Some(preload_path) = linux_appimage_wayland_client_preload_path_with(
|
||||
|key| std::env::var(key).ok(),
|
||||
|path| std::path::Path::new(path).is_file(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let exe = match std::env::current_exe() {
|
||||
Ok(exe) => exe,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Tolaria AppImage Wayland preload skipped: failed to resolve executable ({e})"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let error = Command::new(exe)
|
||||
.args(std::env::args_os().skip(1))
|
||||
.env("LD_PRELOAD", preload_path)
|
||||
.env("TOLARIA_APPIMAGE_WAYLAND_PRELOAD_ATTEMPTED", "1")
|
||||
.exec();
|
||||
eprintln!("Tolaria AppImage Wayland preload skipped: failed to re-exec ({error})");
|
||||
}
|
||||
|
||||
#[cfg(not(all(desktop, target_os = "linux")))]
|
||||
fn apply_linux_appimage_startup_env_overrides() {}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn log_startup_result(label: &str, result: Result<usize, String>) {
|
||||
match result {
|
||||
@@ -612,7 +482,9 @@ fn handle_run_event(app_handle: &tauri::AppHandle, event: &tauri::RunEvent) {
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
apply_linux_appimage_startup_env_overrides();
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
linux_appimage::apply_startup_env_overrides();
|
||||
|
||||
let builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(desktop)]
|
||||
@@ -634,9 +506,6 @@ pub fn run() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::linux_appimage_startup_env_overrides_with;
|
||||
use super::linux_appimage_wayland_client_preload_path_with;
|
||||
use super::StartupEnvOverride;
|
||||
use super::MACOS_WEBVIEW_RESERVED_COMMAND_KEYS;
|
||||
use super::MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS;
|
||||
|
||||
@@ -659,98 +528,6 @@ mod tests {
|
||||
assert_eq!(MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS, ["L"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_appimage_startup_env_overrides_are_empty_outside_appimage_launches() {
|
||||
let overrides = linux_appimage_startup_env_overrides_with(|_| None);
|
||||
|
||||
assert!(overrides.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_appimage_startup_env_overrides_disable_unstable_webkit_rendering_for_appimages() {
|
||||
let overrides = linux_appimage_startup_env_overrides_with(|key| match key {
|
||||
"APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
overrides,
|
||||
vec![
|
||||
StartupEnvOverride {
|
||||
key: "WEBKIT_DISABLE_DMABUF_RENDERER",
|
||||
value: "1",
|
||||
},
|
||||
StartupEnvOverride {
|
||||
key: "WEBKIT_DISABLE_COMPOSITING_MODE",
|
||||
value: "1",
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_appimage_startup_env_overrides_preserve_explicit_user_setting_per_variable() {
|
||||
let overrides = linux_appimage_startup_env_overrides_with(|key| match key {
|
||||
"APPDIR" => Some("/tmp/.mount_Tolaria".to_string()),
|
||||
"WEBKIT_DISABLE_DMABUF_RENDERER" => Some("0".to_string()),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
overrides,
|
||||
vec![StartupEnvOverride {
|
||||
key: "WEBKIT_DISABLE_COMPOSITING_MODE",
|
||||
value: "1",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_appimage_wayland_preload_uses_first_available_system_library() {
|
||||
let preload_path = linux_appimage_wayland_client_preload_path_with(
|
||||
|key| match key {
|
||||
"APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()),
|
||||
"XDG_SESSION_TYPE" => Some("wayland".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|path| path == "/lib/x86_64-linux-gnu/libwayland-client.so.0",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
preload_path,
|
||||
Some("/lib/x86_64-linux-gnu/libwayland-client.so.0")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_appimage_wayland_preload_preserves_explicit_ld_preload() {
|
||||
let preload_path = linux_appimage_wayland_client_preload_path_with(
|
||||
|key| match key {
|
||||
"APPDIR" => Some("/tmp/.mount_Tolaria".to_string()),
|
||||
"WAYLAND_DISPLAY" => Some("wayland-0".to_string()),
|
||||
"LD_PRELOAD" => Some("/custom/libwayland-client.so".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|_| true,
|
||||
);
|
||||
|
||||
assert_eq!(preload_path, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_appimage_wayland_preload_is_empty_for_x11_sessions() {
|
||||
let preload_path = linux_appimage_wayland_client_preload_path_with(
|
||||
|key| match key {
|
||||
"APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()),
|
||||
"XDG_SESSION_TYPE" => Some("x11".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|_| true,
|
||||
);
|
||||
|
||||
assert_eq!(preload_path, None);
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[test]
|
||||
fn selected_mcp_bridge_vault_path_uses_persisted_active_vault() {
|
||||
|
||||
284
src-tauri/src/linux_appimage.rs
Normal file
284
src-tauri/src/linux_appimage.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct StartupEnvOverride {
|
||||
key: &'static str,
|
||||
value: &'static str,
|
||||
}
|
||||
|
||||
const LINUX_APPIMAGE_WEBKIT_OVERRIDES: [StartupEnvOverride; 2] = [
|
||||
StartupEnvOverride {
|
||||
key: "WEBKIT_DISABLE_DMABUF_RENDERER",
|
||||
value: "1",
|
||||
},
|
||||
StartupEnvOverride {
|
||||
key: "WEBKIT_DISABLE_COMPOSITING_MODE",
|
||||
value: "1",
|
||||
},
|
||||
];
|
||||
|
||||
const WAYLAND_CLIENT_PRELOAD_CANDIDATES: [&str; 7] = [
|
||||
"/usr/lib64/libwayland-client.so.0",
|
||||
"/usr/lib64/libwayland-client.so",
|
||||
"/lib64/libwayland-client.so.0",
|
||||
"/usr/lib/x86_64-linux-gnu/libwayland-client.so.0",
|
||||
"/lib/x86_64-linux-gnu/libwayland-client.so.0",
|
||||
"/usr/lib/libwayland-client.so.0",
|
||||
"/usr/lib/libwayland-client.so",
|
||||
];
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
const PROCESS_ELF_CLASS: u8 = 2;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
const PROCESS_ELF_CLASS: u8 = 1;
|
||||
|
||||
fn is_linux_appimage_launch<F>(mut get_var: F) -> bool
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
["APPIMAGE", "APPDIR"]
|
||||
.into_iter()
|
||||
.any(|key| get_var(key).is_some_and(|value| !value.trim().is_empty()))
|
||||
}
|
||||
|
||||
fn is_wayland_session<F>(mut get_var: F) -> bool
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
get_var("WAYLAND_DISPLAY").is_some_and(|value| !value.trim().is_empty())
|
||||
|| get_var("XDG_SESSION_TYPE")
|
||||
.is_some_and(|value| value.trim().eq_ignore_ascii_case("wayland"))
|
||||
}
|
||||
|
||||
fn elf_library_matches_process(path: &std::path::Path) -> bool {
|
||||
let Ok(mut file) = std::fs::File::open(path) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut header = [0; 5];
|
||||
if std::io::Read::read_exact(&mut file, &mut header).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
header[..4] == *b"\x7FELF" && header[4] == PROCESS_ELF_CLASS
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
fn wayland_preload_candidate_matches(path: &str) -> bool {
|
||||
let path = std::path::Path::new(path);
|
||||
|
||||
path.is_file() && elf_library_matches_process(path)
|
||||
}
|
||||
|
||||
fn wayland_client_preload_path_with<F, E>(
|
||||
mut get_var: F,
|
||||
mut candidate_matches: E,
|
||||
) -> Option<&'static str>
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
E: FnMut(&str) -> bool,
|
||||
{
|
||||
if !is_linux_appimage_launch(&mut get_var) || !is_wayland_session(&mut get_var) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if get_var("LD_PRELOAD").is_some_and(|value| !value.trim().is_empty())
|
||||
|| get_var("TOLARIA_APPIMAGE_WAYLAND_PRELOAD_ATTEMPTED").is_some_and(|value| value == "1")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
WAYLAND_CLIENT_PRELOAD_CANDIDATES
|
||||
.into_iter()
|
||||
.find(|path| candidate_matches(path))
|
||||
}
|
||||
|
||||
fn startup_env_overrides_with<F>(mut get_var: F) -> Vec<StartupEnvOverride>
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
if !is_linux_appimage_launch(&mut get_var) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
LINUX_APPIMAGE_WEBKIT_OVERRIDES
|
||||
.into_iter()
|
||||
.filter(|env_override| {
|
||||
!get_var(env_override.key).is_some_and(|value| !value.trim().is_empty())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
pub(crate) fn apply_startup_env_overrides() {
|
||||
apply_wayland_client_preload();
|
||||
|
||||
for env_override in startup_env_overrides_with(|key| std::env::var(key).ok()) {
|
||||
std::env::set_var(env_override.key, env_override.value);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
fn apply_wayland_client_preload() {
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
let Some(preload_path) = wayland_client_preload_path_with(
|
||||
|key| std::env::var(key).ok(),
|
||||
wayland_preload_candidate_matches,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let exe = match std::env::current_exe() {
|
||||
Ok(exe) => exe,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Tolaria AppImage Wayland preload skipped: failed to resolve executable ({e})"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let error = std::process::Command::new(exe)
|
||||
.args(std::env::args_os().skip(1))
|
||||
.env("LD_PRELOAD", preload_path)
|
||||
.env("TOLARIA_APPIMAGE_WAYLAND_PRELOAD_ATTEMPTED", "1")
|
||||
.exec();
|
||||
eprintln!("Tolaria AppImage Wayland preload skipped: failed to re-exec ({error})");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
elf_library_matches_process, startup_env_overrides_with, wayland_client_preload_path_with,
|
||||
StartupEnvOverride,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn startup_env_overrides_are_empty_outside_appimage_launches() {
|
||||
let overrides = startup_env_overrides_with(|_| None);
|
||||
|
||||
assert!(overrides.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_env_overrides_disable_unstable_webkit_rendering_for_appimages() {
|
||||
let overrides = startup_env_overrides_with(|key| match key {
|
||||
"APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
overrides,
|
||||
vec![
|
||||
StartupEnvOverride {
|
||||
key: "WEBKIT_DISABLE_DMABUF_RENDERER",
|
||||
value: "1",
|
||||
},
|
||||
StartupEnvOverride {
|
||||
key: "WEBKIT_DISABLE_COMPOSITING_MODE",
|
||||
value: "1",
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_env_overrides_preserve_explicit_user_setting_per_variable() {
|
||||
let overrides = startup_env_overrides_with(|key| match key {
|
||||
"APPDIR" => Some("/tmp/.mount_Tolaria".to_string()),
|
||||
"WEBKIT_DISABLE_DMABUF_RENDERER" => Some("0".to_string()),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
overrides,
|
||||
vec![StartupEnvOverride {
|
||||
key: "WEBKIT_DISABLE_COMPOSITING_MODE",
|
||||
value: "1",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wayland_preload_uses_first_available_system_library() {
|
||||
let preload_path = wayland_client_preload_path_with(
|
||||
|key| match key {
|
||||
"APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()),
|
||||
"XDG_SESSION_TYPE" => Some("wayland".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|path| path == "/lib/x86_64-linux-gnu/libwayland-client.so.0",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
preload_path,
|
||||
Some("/lib/x86_64-linux-gnu/libwayland-client.so.0")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wayland_preload_prefers_fedora_lib64_over_usr_lib() {
|
||||
let preload_path = wayland_client_preload_path_with(
|
||||
|key| match key {
|
||||
"APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()),
|
||||
"XDG_SESSION_TYPE" => Some("wayland".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|path| {
|
||||
path == "/usr/lib/libwayland-client.so.0"
|
||||
|| path == "/usr/lib64/libwayland-client.so.0"
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(preload_path, Some("/usr/lib64/libwayland-client.so.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preload_library_rejects_wrong_elf_class() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let matching = dir.path().join("matching-libwayland-client.so.0");
|
||||
let mismatched = dir.path().join("mismatched-libwayland-client.so.0");
|
||||
let matching_class = if cfg!(target_pointer_width = "64") {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let mismatched_class = if matching_class == 2 { 1 } else { 2 };
|
||||
|
||||
std::fs::write(&matching, [0x7F, b'E', b'L', b'F', matching_class]).unwrap();
|
||||
std::fs::write(&mismatched, [0x7F, b'E', b'L', b'F', mismatched_class]).unwrap();
|
||||
|
||||
assert!(elf_library_matches_process(&matching));
|
||||
assert!(!elf_library_matches_process(&mismatched));
|
||||
assert!(!elf_library_matches_process(&dir.path().join("missing.so")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wayland_preload_preserves_explicit_ld_preload() {
|
||||
let preload_path = wayland_client_preload_path_with(
|
||||
|key| match key {
|
||||
"APPDIR" => Some("/tmp/.mount_Tolaria".to_string()),
|
||||
"WAYLAND_DISPLAY" => Some("wayland-0".to_string()),
|
||||
"LD_PRELOAD" => Some("/custom/libwayland-client.so".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|_| true,
|
||||
);
|
||||
|
||||
assert_eq!(preload_path, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wayland_preload_is_empty_for_x11_sessions() {
|
||||
let preload_path = wayland_client_preload_path_with(
|
||||
|key| match key {
|
||||
"APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()),
|
||||
"XDG_SESSION_TYPE" => Some("x11".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|_| true,
|
||||
);
|
||||
|
||||
assert_eq!(preload_path, None);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { act, fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { FolderTree } from './FolderTree'
|
||||
import { FOLDER_ROW_SINGLE_CLICK_DELAY_MS } from './folder-tree/useFolderRowInteractions'
|
||||
import { FOLDER_ROW_NESTING_INDENT, getFolderConnectorLeft } from './folder-tree/folderTreeLayout'
|
||||
import type { FolderNode, SidebarSelection } from '../types'
|
||||
|
||||
const mockFolders: FolderNode[] = [
|
||||
@@ -67,7 +68,8 @@ describe('FolderTree', () => {
|
||||
expect(screen.getByText('Laputa')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('lets the vault root collapse and expand its nested folders', () => {
|
||||
it('lets the vault root collapse and expand from the row', () => {
|
||||
vi.useFakeTimers()
|
||||
render(
|
||||
<FolderTree
|
||||
folders={mockFolders}
|
||||
@@ -77,19 +79,31 @@ describe('FolderTree', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Collapse Laputa'))
|
||||
fireEvent.click(screen.getByTestId('folder-row:'))
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(FOLDER_ROW_SINGLE_CLICK_DELAY_MS)
|
||||
})
|
||||
expect(screen.queryByText('projects')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Expand Laputa'))
|
||||
fireEvent.click(screen.getByTestId('folder-row:'))
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(FOLDER_ROW_SINGLE_CLICK_DELAY_MS)
|
||||
})
|
||||
expect(screen.getByText('projects')).toBeInTheDocument()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('expands children when clicking the folder chevron', () => {
|
||||
it('expands children when clicking a folder row', () => {
|
||||
vi.useFakeTimers()
|
||||
render(<FolderTree folders={mockFolders} selection={defaultSelection} onSelect={vi.fn()} />)
|
||||
expect(screen.queryByText('laputa')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByLabelText('Expand projects'))
|
||||
fireEvent.click(screen.getByTestId('folder-row:projects'))
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(FOLDER_ROW_SINGLE_CLICK_DELAY_MS)
|
||||
})
|
||||
expect(screen.getByText('laputa')).toBeInTheDocument()
|
||||
expect(screen.getByText('portfolio')).toBeInTheDocument()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('calls onSelect with folder kind when clicking a folder row', () => {
|
||||
@@ -182,32 +196,24 @@ describe('FolderTree', () => {
|
||||
expect(onStartRenameFolder).toHaveBeenCalledWith('projects')
|
||||
})
|
||||
|
||||
it('shows inline rename and delete actions for folders', () => {
|
||||
const onDeleteFolder = vi.fn()
|
||||
const onStartRenameFolder = vi.fn()
|
||||
const onSelect = vi.fn()
|
||||
it('keeps rename and delete out of row hover actions', () => {
|
||||
render(
|
||||
<FolderTree
|
||||
folders={mockFolders}
|
||||
selection={defaultSelection}
|
||||
onSelect={onSelect}
|
||||
onDeleteFolder={onDeleteFolder}
|
||||
onSelect={vi.fn()}
|
||||
onDeleteFolder={vi.fn()}
|
||||
onRenameFolder={vi.fn().mockResolvedValue(true)}
|
||||
onStartRenameFolder={onStartRenameFolder}
|
||||
onStartRenameFolder={vi.fn()}
|
||||
onCancelRenameFolder={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('rename-folder-btn:projects'))
|
||||
fireEvent.click(screen.getByTestId('delete-folder-btn:projects'))
|
||||
|
||||
expect(onSelect).toHaveBeenNthCalledWith(1, { kind: 'folder', path: 'projects' })
|
||||
expect(onStartRenameFolder).toHaveBeenCalledWith('projects')
|
||||
expect(onSelect).toHaveBeenNthCalledWith(2, { kind: 'folder', path: 'projects' })
|
||||
expect(onDeleteFolder).toHaveBeenCalledWith('projects')
|
||||
expect(screen.queryByTestId('rename-folder-btn:projects')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('delete-folder-btn:projects')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not reserve a disclosure slot for leaf folders', () => {
|
||||
it('does not render folder-level disclosure buttons', () => {
|
||||
render(<FolderTree folders={mockFolders} selection={defaultSelection} onSelect={vi.fn()} />)
|
||||
|
||||
const leafRowContainer = screen.getByTestId('folder-row:areas').parentElement
|
||||
@@ -216,7 +222,23 @@ describe('FolderTree', () => {
|
||||
expect(leafRowContainer).not.toBeNull()
|
||||
expect(parentRowContainer).not.toBeNull()
|
||||
expect(within(leafRowContainer as HTMLElement).queryAllByRole('button')).toHaveLength(1)
|
||||
expect(within(parentRowContainer as HTMLElement).queryAllByRole('button')).toHaveLength(2)
|
||||
expect(within(parentRowContainer as HTMLElement).queryAllByRole('button')).toHaveLength(1)
|
||||
expect(screen.queryByLabelText('Expand projects')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('aligns nested folders with the parent folder name and centers connectors on parent icons', () => {
|
||||
render(
|
||||
<FolderTree
|
||||
folders={mockFolders}
|
||||
selection={defaultSelection}
|
||||
onSelect={vi.fn()}
|
||||
vaultRootPath={vaultRootPath}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('folder-row:').parentElement).toHaveStyle({ paddingLeft: '0px' })
|
||||
expect(screen.getByTestId('folder-row:projects').parentElement).toHaveStyle({ paddingLeft: `${FOLDER_ROW_NESTING_INDENT}px` })
|
||||
expect(screen.getByTestId('folder-connector:')).toHaveStyle({ left: `${getFolderConnectorLeft(0)}px` })
|
||||
})
|
||||
|
||||
it('shows the rename input when a folder is being renamed', () => {
|
||||
|
||||
@@ -1170,7 +1170,7 @@ describe('Sidebar', () => {
|
||||
const favoritesHeader = screen.getByText('FAVORITES').closest('div') as HTMLElement
|
||||
const countChip = within(favoritesHeader).getByTestId('sidebar-count-chip')
|
||||
|
||||
expect(favoritesHeader).toHaveStyle({ padding: '8px 8px 8px 16px' })
|
||||
expect(favoritesHeader).toHaveStyle({ padding: '8px 8px 8px 12px' })
|
||||
expect(countChip).toHaveStyle({
|
||||
background: 'var(--muted)',
|
||||
height: '18px',
|
||||
@@ -1441,7 +1441,7 @@ describe('Sidebar', () => {
|
||||
const navItem = viewLabel.closest('[class*="cursor-pointer"]') as HTMLElement
|
||||
const countChip = within(navItem).getByTestId('view-count-chip')
|
||||
|
||||
expect(navItem).toHaveStyle({ padding: '6px 8px 6px 16px' })
|
||||
expect(navItem).toHaveStyle({ padding: '6px 8px 6px 12px' })
|
||||
expect(countChip).toHaveStyle({
|
||||
background: 'var(--muted)',
|
||||
height: '20px',
|
||||
@@ -1460,8 +1460,8 @@ describe('Sidebar', () => {
|
||||
const viewItem = screen.getByText('Active Projects').closest('[class*="cursor-pointer"]') as HTMLElement
|
||||
const viewCount = within(viewItem).getByTestId('view-count-chip')
|
||||
|
||||
expect(topNavItem).toHaveStyle({ padding: '6px 8px 6px 16px' })
|
||||
expect(viewItem).toHaveStyle({ padding: '6px 8px 6px 16px' })
|
||||
expect(topNavItem).toHaveStyle({ padding: '6px 8px 6px 12px' })
|
||||
expect(viewItem).toHaveStyle({ padding: '6px 8px 6px 12px' })
|
||||
expect(topNavCount).toHaveStyle({
|
||||
background: 'var(--muted)',
|
||||
height: '20px',
|
||||
|
||||
@@ -491,7 +491,7 @@ function SectionHeader({ label, type, Icon, sectionColor, sectionLightColor, ite
|
||||
return (
|
||||
<div
|
||||
className={cn("group/section flex cursor-pointer select-none items-center justify-between rounded transition-colors", !isActive && "hover:bg-accent")}
|
||||
style={{ padding: '6px 8px 6px 16px', borderRadius: 4, gap: 4, ...getSectionHeaderBackground(isActive, sectionLightColor) }}
|
||||
style={{ padding: SIDEBAR_ITEM_PADDING.withCount, borderRadius: 4, gap: 4, ...getSectionHeaderBackground(isActive, sectionLightColor) }}
|
||||
{...dragHandleProps}
|
||||
onClick={getSectionSelectHandler(isRenaming, onSelect)}
|
||||
onContextMenu={getSectionContextMenuHandler(isRenaming, onContextMenu)}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react'
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react'
|
||||
import {
|
||||
CaretDown,
|
||||
CaretRight,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
PencilSimple,
|
||||
Trash,
|
||||
} from '@phosphor-icons/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { FolderNode } from '../../types'
|
||||
import { useFolderRowInteractions } from './useFolderRowInteractions'
|
||||
import { translate, type AppLocale } from '../../lib/i18n'
|
||||
|
||||
interface FolderItemRowProps {
|
||||
contentInset: number
|
||||
@@ -19,12 +14,10 @@ interface FolderItemRowProps {
|
||||
isExpanded: boolean
|
||||
isSelected: boolean
|
||||
node: FolderNode
|
||||
onDeleteFolder?: (folderPath: string) => void
|
||||
onOpenMenu: (node: FolderNode, event: ReactMouseEvent<HTMLDivElement>) => void
|
||||
onSelect: () => void
|
||||
onStartRenameFolder?: (folderPath: string) => void
|
||||
onToggle: (path: string) => void
|
||||
locale?: AppLocale
|
||||
}
|
||||
|
||||
export function FolderItemRow({
|
||||
@@ -33,16 +26,12 @@ export function FolderItemRow({
|
||||
isExpanded,
|
||||
isSelected,
|
||||
node,
|
||||
onDeleteFolder,
|
||||
onOpenMenu,
|
||||
onSelect,
|
||||
onStartRenameFolder,
|
||||
onToggle,
|
||||
locale = 'en',
|
||||
}: FolderItemRowProps) {
|
||||
const hasChildren = node.children.length > 0
|
||||
const expandLabel = translate(locale, isExpanded ? 'sidebar.folder.collapse' : 'sidebar.folder.expand', { name: node.name })
|
||||
const hasActions = !!onStartRenameFolder || !!onDeleteFolder
|
||||
const { handleRenameDoubleClick, handleSelectClick } = useFolderRowInteractions({
|
||||
hasChildren,
|
||||
onRenameFolder: onStartRenameFolder ? () => onStartRenameFolder(node.path) : undefined,
|
||||
@@ -64,15 +53,8 @@ export function FolderItemRow({
|
||||
onOpenMenu(node, event)
|
||||
}}
|
||||
>
|
||||
<FolderToggleButton
|
||||
expandLabel={expandLabel}
|
||||
hasChildren={hasChildren}
|
||||
isExpanded={isExpanded}
|
||||
onToggle={() => onToggle(node.path)}
|
||||
/>
|
||||
<FolderSelectButton
|
||||
contentInset={contentInset}
|
||||
hasActions={hasActions}
|
||||
hasChildren={hasChildren}
|
||||
isExpanded={isExpanded}
|
||||
isSelected={isSelected}
|
||||
@@ -80,111 +62,12 @@ export function FolderItemRow({
|
||||
onClick={handleSelectClick}
|
||||
onDoubleClick={handleRenameDoubleClick}
|
||||
/>
|
||||
{hasActions && (
|
||||
<div className="pointer-events-none absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
|
||||
{onStartRenameFolder && (
|
||||
<FolderActionButton
|
||||
ariaLabel={translate(locale, 'sidebar.action.renameFolder')}
|
||||
testId={`rename-folder-btn:${node.path}`}
|
||||
title={translate(locale, 'sidebar.action.renameFolder')}
|
||||
onClick={() => {
|
||||
onSelect()
|
||||
onStartRenameFolder(node.path)
|
||||
}}
|
||||
>
|
||||
<PencilSimple size={12} />
|
||||
</FolderActionButton>
|
||||
)}
|
||||
{onDeleteFolder && (
|
||||
<FolderActionButton
|
||||
ariaLabel={translate(locale, 'sidebar.action.deleteFolder')}
|
||||
testId={`delete-folder-btn:${node.path}`}
|
||||
title={translate(locale, 'sidebar.action.deleteFolder')}
|
||||
destructive
|
||||
onClick={() => {
|
||||
onSelect()
|
||||
onDeleteFolder(node.path)
|
||||
}}
|
||||
>
|
||||
<Trash size={12} />
|
||||
</FolderActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FolderToggleButton({
|
||||
expandLabel,
|
||||
hasChildren,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
}: {
|
||||
expandLabel: string
|
||||
hasChildren: boolean
|
||||
isExpanded: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
if (!hasChildren) return null
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="h-6 w-4 shrink-0 p-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onToggle()
|
||||
}}
|
||||
aria-label={expandLabel}
|
||||
>
|
||||
{isExpanded ? <CaretDown size={12} /> : <CaretRight size={12} />}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function FolderActionButton({
|
||||
ariaLabel,
|
||||
children,
|
||||
destructive = false,
|
||||
onClick,
|
||||
testId,
|
||||
title,
|
||||
}: {
|
||||
ariaLabel: string
|
||||
children: ReactNode
|
||||
destructive?: boolean
|
||||
onClick: () => void
|
||||
testId: string
|
||||
title: string
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label={ariaLabel}
|
||||
title={title}
|
||||
className={cn(
|
||||
'h-5 w-5 rounded p-0 text-muted-foreground',
|
||||
destructive ? 'hover:text-destructive' : 'hover:text-foreground',
|
||||
)}
|
||||
data-testid={testId}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onClick()
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function FolderSelectButton({
|
||||
contentInset,
|
||||
hasActions,
|
||||
hasChildren,
|
||||
isExpanded,
|
||||
isSelected,
|
||||
@@ -193,7 +76,6 @@ function FolderSelectButton({
|
||||
onDoubleClick,
|
||||
}: {
|
||||
contentInset: number
|
||||
hasActions: boolean
|
||||
hasChildren: boolean
|
||||
isExpanded: boolean
|
||||
isSelected: boolean
|
||||
@@ -212,10 +94,11 @@ function FolderSelectButton({
|
||||
style={{
|
||||
paddingTop: 6,
|
||||
paddingBottom: 6,
|
||||
paddingLeft: hasChildren ? 0 : contentInset,
|
||||
paddingRight: hasActions ? 48 : 16,
|
||||
paddingLeft: contentInset,
|
||||
paddingRight: 16,
|
||||
}}
|
||||
title={node.path || node.name}
|
||||
aria-expanded={hasChildren ? isExpanded : undefined}
|
||||
onClick={(event) => onClick(event.detail)}
|
||||
onDoubleClick={onDoubleClick}
|
||||
data-testid={`folder-row:${node.path}`}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { memo, useCallback, type MouseEvent as ReactMouseEvent } from 'react'
|
||||
import type { FolderNode, SidebarSelection } from '../../types'
|
||||
import { FolderNameInput } from './FolderNameInput'
|
||||
import { FolderItemRow } from './FolderItemRow'
|
||||
import { FOLDER_ROW_CONTENT_INSET, getFolderConnectorLeft, getFolderDepthIndent } from './folderTreeLayout'
|
||||
import { translate, type AppLocale } from '../../lib/i18n'
|
||||
|
||||
interface FolderTreeRowProps {
|
||||
@@ -73,10 +74,11 @@ function FolderChildren({
|
||||
if (!isExpanded || !hasChildren) return null
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ paddingLeft: 15 }}>
|
||||
<div className="relative" data-testid={`folder-children:${node.path}`}>
|
||||
<div
|
||||
className="absolute top-0 bottom-0 bg-border"
|
||||
style={{ left: 15 + depth * 16, width: 1, opacity: 0.3 }}
|
||||
data-testid={`folder-connector:${node.path}`}
|
||||
style={{ left: getFolderConnectorLeft(depth), width: 1 }}
|
||||
/>
|
||||
{node.children.map((child) => (
|
||||
<FolderTreeRow
|
||||
@@ -121,8 +123,8 @@ export const FolderTreeRow = memo(function FolderTreeRow({
|
||||
const isRenaming = renamingFolderPath === node.path
|
||||
const isSelected = selection.kind === 'folder' && selection.path === node.path
|
||||
const canMutateFolder = node.path.length > 0
|
||||
const depthIndent = depth * 16
|
||||
const contentInset = 16
|
||||
const depthIndent = getFolderDepthIndent(depth)
|
||||
const contentInset = FOLDER_ROW_CONTENT_INSET
|
||||
const selectFolder = useCallback(() => {
|
||||
onSelect(node.path === '' ? { kind: 'folder', path: '', rootPath } : { kind: 'folder', path: node.path })
|
||||
}, [node.path, onSelect, rootPath])
|
||||
@@ -133,12 +135,10 @@ export const FolderTreeRow = memo(function FolderTreeRow({
|
||||
isExpanded={isExpanded}
|
||||
isSelected={isSelected}
|
||||
node={node}
|
||||
onDeleteFolder={canMutateFolder ? onDeleteFolder : undefined}
|
||||
onOpenMenu={onOpenMenu}
|
||||
onSelect={selectFolder}
|
||||
onStartRenameFolder={canMutateFolder ? onStartRenameFolder : undefined}
|
||||
onToggle={onToggle}
|
||||
locale={locale}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
17
src/components/folder-tree/folderTreeLayout.ts
Normal file
17
src/components/folder-tree/folderTreeLayout.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const FOLDER_ROW_CONTENT_INSET = 12
|
||||
export const FOLDER_ROW_ICON_SIZE = 17
|
||||
export const FOLDER_ROW_ICON_GAP = 8
|
||||
export const FOLDER_ROW_NESTING_INDENT = FOLDER_ROW_ICON_SIZE + FOLDER_ROW_ICON_GAP
|
||||
|
||||
const FOLDER_CONNECTOR_WIDTH = 1
|
||||
|
||||
export function getFolderDepthIndent(depth: number) {
|
||||
return depth * FOLDER_ROW_NESTING_INDENT
|
||||
}
|
||||
|
||||
export function getFolderConnectorLeft(depth: number) {
|
||||
return FOLDER_ROW_CONTENT_INSET
|
||||
+ getFolderDepthIndent(depth)
|
||||
+ FOLDER_ROW_ICON_SIZE / 2
|
||||
- FOLDER_CONNECTOR_WIDTH / 2
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { SlidersHorizontal } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { translate, type AppLocale } from '../../lib/i18n'
|
||||
import { SidebarGroupHeader } from './SidebarGroupHeader'
|
||||
import { SIDEBAR_ITEM_PADDING } from './sidebarStyles'
|
||||
|
||||
interface SidebarLoadingSectionsProps {
|
||||
collapsed: boolean
|
||||
@@ -157,7 +158,7 @@ function SidebarLoadingRow({
|
||||
return (
|
||||
<div
|
||||
className="flex select-none items-center gap-2 rounded"
|
||||
style={{ padding: '6px 8px 6px 16px', borderRadius: 4 }}
|
||||
style={{ padding: SIDEBAR_ITEM_PADDING.withCount, borderRadius: 4 }}
|
||||
>
|
||||
<SidebarLoadingIcon icon={icon} iconColor={iconColor} />
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
export const SIDEBAR_ITEM_PADDING = {
|
||||
regular: '6px 16px',
|
||||
withCount: '6px 8px 6px 16px',
|
||||
compact: '4px 16px',
|
||||
compactWithCount: '4px 8px 4px 16px',
|
||||
regular: '6px 16px 6px 12px',
|
||||
withCount: '6px 8px 6px 12px',
|
||||
compact: '4px 16px 4px 12px',
|
||||
compactWithCount: '4px 8px 4px 12px',
|
||||
} as const
|
||||
|
||||
export const SIDEBAR_GROUP_HEADER_PADDING = {
|
||||
regular: '8px 14px 8px 16px',
|
||||
withCount: '8px 8px 8px 16px',
|
||||
regular: '8px 14px 8px 12px',
|
||||
withCount: '8px 8px 8px 12px',
|
||||
} as const
|
||||
|
||||
export const SIDEBAR_SECTION_CONTENT_PADDING_BOTTOM = 8
|
||||
|
||||
Reference in New Issue
Block a user