diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-01 15:48:22 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-01 15:48:22 +0300 |
| commit | 207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch) | |
| tree | d7b037ecdf629224a048c841d11659efdc23507d /src/client/shell/preferences.rs | |
| parent | 8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff) | |
refactor: render the shell in the client (#3487)
* feat: add opt-in client-rendered shell
* feat: add client-owned shell mouse controls
* feat: complete client-owned shell chrome
* feat: add client-owned shell settings and notifications
* feat: run client shell custom commands
* feat: render popup terminals in client shell
* feat: add client-owned scrollback and copy mode
* fix: preserve client worktree trust defaults
* feat: render startup diagnostics in client shell
* feat: render onboarding in client shell
* feat: render product announcements in client shell
* feat: render release notes in client shell
* feat: render mobile switcher in client shell
* feat: render kitty graphics in client shell
* fix: preserve client shell lifecycle coherence
* refactor: route client shell commands through endpoint connections
* refactor: make the client-rendered shell the default
* refactor: remove monolithic launch mode
* refactor: complete client-rendered shell cutover
* perf: retain client pane surface updates
* perf: scale retained pane surface updates
* perf: remove remote pane input latency
* test: make client shell checks platform-independent
* fix: prevent client timer starvation
* test: fix client shell platform gates
* test: accept retained surfaces in cross-area checks
* test: make client mode assertions platform-neutral
* fix: badge only outdated integrations
Diffstat (limited to 'src/client/shell/preferences.rs')
| -rw-r--r-- | src/client/shell/preferences.rs | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/src/client/shell/preferences.rs b/src/client/shell/preferences.rs new file mode 100644 index 00000000..dc6cc1ac --- /dev/null +++ b/src/client/shell/preferences.rs @@ -0,0 +1,132 @@ +use std::{ + path::{Path, PathBuf}, + sync::atomic::{AtomicU64, Ordering}, +}; + +static NEXT_TEMP_FILE: AtomicU64 = AtomicU64::new(1); + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(super) struct ClientChromePreferences { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(super) sidebar_width: Option<u16>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(super) sidebar_section_split: Option<f32>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(super) sidebar_collapsed: Option<bool>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(super) agent_panel_sort: Option<crate::config::AgentPanelSortConfig>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub(super) collapsed_groups: Vec<String>, +} + +pub(super) fn path_for_local_endpoint(socket_path: &Path) -> PathBuf { + let mut hash = 0xcbf29ce484222325u64; + for byte in socket_path.to_string_lossy().as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + crate::config::state_dir() + .join("client-shell") + .join(format!("local-{hash:016x}.json")) +} + +pub(super) fn load(path: &Path) -> Option<ClientChromePreferences> { + let content = std::fs::read_to_string(path).ok()?; + serde_json::from_str(&content).ok() +} + +pub(super) fn store(path: &Path, preferences: ClientChromePreferences) -> Result<(), String> { + let parent = path + .parent() + .ok_or_else(|| format!("invalid client shell state path: {}", path.display()))?; + std::fs::create_dir_all(parent) + .map_err(|error| format!("failed to create client shell state directory: {error}"))?; + let content = serde_json::to_vec_pretty(&preferences) + .map_err(|error| format!("failed to encode client shell state: {error}"))?; + let sequence = NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed); + let mut temp_name = path + .file_name() + .ok_or_else(|| format!("invalid client shell state path: {}", path.display()))? + .to_os_string(); + temp_name.push(format!(".tmp-{}-{sequence}", std::process::id())); + let temp_path = parent.join(temp_name); + std::fs::write(&temp_path, content) + .map_err(|error| format!("failed to write client shell state: {error}"))?; + crate::platform::replace_file(&temp_path, path).map_err(|error| { + let _ = std::fs::remove_file(&temp_path); + format!("failed to replace client shell state: {error}") + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn endpoint_paths_are_stable_and_distinct() { + let first = path_for_local_endpoint(Path::new("/run/herdr/one.sock")); + let again = path_for_local_endpoint(Path::new("/run/herdr/one.sock")); + let second = path_for_local_endpoint(Path::new("/run/herdr/two.sock")); + assert_eq!(first, again); + assert_ne!(first, second); + } + + #[test] + fn concurrent_stores_leave_complete_preferences() { + let path = std::env::temp_dir().join(format!( + "herdr-shell-concurrent-preferences-{}.json", + std::process::id() + )); + let _ = std::fs::remove_file(&path); + let writers = (20..28) + .map(|width| { + let path = path.clone(); + std::thread::spawn(move || { + store( + &path, + ClientChromePreferences { + sidebar_width: Some(width), + ..ClientChromePreferences::default() + }, + ) + }) + }) + .collect::<Vec<_>>(); + for writer in writers { + writer.join().expect("preference writer").expect("store"); + } + assert!(load(&path) + .and_then(|saved| saved.sidebar_width) + .is_some_and(|width| (20..28).contains(&width))); + std::fs::remove_file(path).expect("remove preferences"); + } + + #[test] + fn repeated_store_replaces_existing_preferences() { + let path = std::env::temp_dir().join(format!( + "herdr-shell-preferences-{}.json", + std::process::id() + )); + let _ = std::fs::remove_file(&path); + store( + &path, + ClientChromePreferences { + sidebar_width: Some(24), + ..ClientChromePreferences::default() + }, + ) + .expect("first preference store"); + store( + &path, + ClientChromePreferences { + sidebar_width: Some(32), + ..ClientChromePreferences::default() + }, + ) + .expect("replacement preference store"); + assert_eq!(load(&path).and_then(|saved| saved.sidebar_width), Some(32)); + std::fs::remove_file(path).expect("remove preferences"); + } +} |
