From f027ebe2aaaa8da1171bba81e750fed494557d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirek=20Wr=C3=B3bel?= Date: Wed, 14 Oct 2026 22:21:09 +0200 Subject: [PATCH] Resizable sidebar remembered in settings.json; nest tags by / like branches --- src-tauri/src/lib.rs | 19 ++++++++++++- src-tauri/src/settings.rs | 26 ++++++++++++++++++ src/App.vue | 55 ++++++++++++++++++++++++++++++++++++++ src/components/RefTree.vue | 14 +++++----- src/components/Sidebar.vue | 5 ++-- src/lib/tree.test.ts | 7 +++++ src/lib/tree.ts | 14 +++++++--- 7 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 src-tauri/src/settings.rs diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f8bc4e4..67fcac3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ mod filters; mod git; +mod settings; use std::path::PathBuf; @@ -21,6 +22,16 @@ fn save_filters(repo: String, value: serde_json::Value) -> Result<(), String> { filters::save(&repo, value) } +#[tauri::command] +fn load_settings() -> Result { + settings::load() +} + +#[tauri::command] +fn save_settings(value: serde_json::Value) -> Result<(), String> { + settings::save(value) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let path = std::env::args_os() @@ -31,7 +42,13 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_clipboard_manager::init()) .manage(RepoArg(path)) - .invoke_handler(tauri::generate_handler![load_repo, load_filters, save_filters]) + .invoke_handler(tauri::generate_handler![ + load_repo, + load_filters, + save_filters, + load_settings, + save_settings + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs new file mode 100644 index 0000000..54ec158 --- /dev/null +++ b/src-tauri/src/settings.rs @@ -0,0 +1,26 @@ +//! App-wide settings, such as the sidebar width, in `~/.local/share/gites/settings.json`. + +use serde_json::Value; +use std::path::PathBuf; + +fn file() -> Result { + Ok(dirs::data_dir().ok_or("no data folder found")?.join("gites").join("settings.json")) +} + +pub fn load() -> Result { + let path = file()?; + match std::fs::read_to_string(&path) { + Ok(text) => Ok(serde_json::from_str(&text).unwrap_or(Value::Null)), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Value::Null), + Err(e) => Err(format!("could not read {}: {e}", path.display())), + } +} + +pub fn save(value: Value) -> Result<(), String> { + let path = file()?; + if let Some(dir) = path.parent() { + std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; + } + let text = serde_json::to_string_pretty(&value).map_err(|e| e.to_string())?; + std::fs::write(&path, text).map_err(|e| format!("could not write {}: {e}", path.display())) +} diff --git a/src/App.vue b/src/App.vue index 42ffb0c..3b2b0bf 100644 --- a/src/App.vue +++ b/src/App.vue @@ -25,6 +25,48 @@ const selected = ref(0); const toast = ref(null); const sidebar = ref>(); +// ---- settings: the sidebar width ------------------------------------------- + +const SIDEBAR_MIN = 160; +const SIDEBAR_MAX = 700; +const sidebarWidth = ref(270); +let settingsLoaded = false; + +async function loadSettings() { + const saved = await invoke<{ sidebarWidth?: number } | null>("load_settings").catch(() => null); + if (typeof saved?.sidebarWidth === "number") { + sidebarWidth.value = Math.min(SIDEBAR_MAX, Math.max(SIDEBAR_MIN, saved.sidebarWidth)); + } + settingsLoaded = true; +} + +let settingsTimer: number | undefined; +watch(sidebarWidth, () => { + if (!settingsLoaded) return; + clearTimeout(settingsTimer); + settingsTimer = window.setTimeout(() => { + invoke("save_settings", { value: { sidebarWidth: sidebarWidth.value } }).catch((e) => + showToast(`could not save settings: ${e}`), + ); + }, 300); +}); + +function startResize(e: PointerEvent) { + const handle = e.currentTarget as HTMLElement; + handle.setPointerCapture(e.pointerId); + const startX = e.clientX; + const startWidth = sidebarWidth.value; + const move = (ev: PointerEvent) => { + sidebarWidth.value = Math.min(SIDEBAR_MAX, Math.max(SIDEBAR_MIN, startWidth + ev.clientX - startX)); + }; + const stop = () => { + handle.removeEventListener("pointermove", move); + handle.removeEventListener("pointerup", stop); + }; + handle.addEventListener("pointermove", move); + handle.addEventListener("pointerup", stop); +} + // ---- loading and saving ------------------------------------------------- let filtersLoadedFor: string | null = null; @@ -228,6 +270,7 @@ onMounted(() => { viewWidth.value = scroller.value?.clientWidth ?? 1000; }); if (scroller.value) resizeObserver.observe(scroller.value); + loadSettings(); load(); }); onUnmounted(() => { @@ -270,8 +313,10 @@ onUnmounted(() => { :filters="filters" :head-ref="repo.head_ref" :time-of="timeOf" + :style="{ width: sidebarWidth + 'px' }" @jump="jumpTo" /> +

Could not open the repository

@@ -372,6 +417,16 @@ onUnmounted(() => { .collapse:disabled { opacity: 0.4; cursor: default; background: none; } .collapse.on { border-color: var(--accent); color: var(--accent); background: #b197fc1a; } .main { flex: 1; display: flex; min-height: 0; } +.splitter { + flex: none; + width: 5px; + margin-left: -3px; + margin-right: -2px; + cursor: col-resize; + position: relative; + z-index: 5; +} +.splitter:hover, .splitter:active { background: var(--accent); opacity: 0.6; } .list { flex: 1; overflow: auto; position: relative; } .spacer-rows { position: relative; min-width: 700px; } .row { diff --git a/src/components/RefTree.vue b/src/components/RefTree.vue index bd1ebde..a295fa0 100644 --- a/src/components/RefTree.vue +++ b/src/components/RefTree.vue @@ -19,8 +19,10 @@ const emit = defineEmits<{ jump: [ref: Ref]; toggle: [names: string[]] }>(); const allHidden = (n: TreeNode) => refsUnder(n).every((r) => props.filters.hidden.includes(r.name)); const anyVisible = (n: TreeNode) => refsUnder(n).some((r) => isRefVisible(r.name, props.filters)); -function toggleFolder(path: string) { - if (!props.closed.delete(path)) props.closed.add(path); +// Keyed by kind too, so a `backup` tag folder and a `backup` branch folder are separate. +const key = (n: TreeNode) => `${props.kind}:${n.path}`; +function toggleFolder(n: TreeNode) { + if (!props.closed.delete(key(n))) props.closed.add(key(n)); } @@ -30,13 +32,13 @@ function toggleFolder(path: string) { class="item" :class="{ off: !anyVisible(n), head: n.ref?.name === headRef, folder: n.children.length }" :style="{ paddingLeft: 10 + depth * 14 + 'px' }" - @click="n.ref ? emit('jump', n.ref) : toggleFolder(n.path)" + @click="n.ref ? emit('jump', n.ref) : toggleFolder(n)" > ▸ @@ -47,7 +49,7 @@ function toggleFolder(path: string) {
{ const trees = computed((): Record => { const tree = (refs: Ref[]) => buildTree(refs.map((ref) => ({ short: shortName(ref.name), ref }))); - // Tags stay a flat list, newest first; equal times fall back to the higher version. + // Tags are newest first; equal times fall back to the higher version. const tags = [...byKind.value.tag].sort( (a, b) => props.timeOf(b.target) - props.timeOf(a.target) || @@ -43,7 +43,7 @@ const trees = computed((): Record => { return { local: tree(byKind.value.local), remote: tree(byKind.value.remote), - tag: tags.map((ref) => ({ label: shortName(ref.name), path: ref.name, children: [], ref })), + tag: buildTree(tags.map((ref) => ({ short: shortName(ref.name), ref })), true), }; }); @@ -115,7 +115,6 @@ function onPatternKey(e: KeyboardEvent) {