//! The Storage page: where new worktrees go, after T3 Code's, and the //! folders the app keeps its settings and threads in. use std::{ path::{Path, PathBuf}, process::{Command, Stdio}, }; use gpui_kit::component::IconName; use gpui_kit::*; use super::SettingsView; use super::widgets::{Section, SettingRow, reset, stepper, switch, text_field}; use crate::settings; use crate::ui::chat::display_path; use crate::ui::metal::{self, MetalButton, Palette}; use crate::ui::workspace; impl SettingsView { pub(super) fn render_storage(&mut self, window: &mut Window, cx: &mut Context) -> Vec { let view = cx.weak_entity(); let p = metal::palette(cx); let saved = super::scope::shown_settings(cx); let data = settings::data_dir(); let default_worktrees = data.as_ref().map(|data| data.join("worktrees")); let worktrees = Section::new("Worktrees") .id("storage-worktrees") .row( SettingRow::new("Worktree location") .id("storage-worktrees-location") .description( "Folder where new worktrees are created, on any drive, such as ~/worktrees. Existing worktrees stay where they are. Leave empty to use the app's data folder.", ) .status(match &default_worktrees { Some(path) if saved.worktrees_dir.trim().is_empty() => { format!("Default: {}", display_path(path)) } _ => String::new(), }) .reset(reset(&view, cx, |s| s.worktrees_dir.clone(), |s, dir| s.worktrees_dir = dir)) .control(text_field(&self.worktrees_dir, 288., window, cx)), ) .row( SettingRow::new("Delete worktrees with deleted threads") .id("storage-worktrees-deleted") .description( "Remove unused worktrees when active or archived threads are deleted. Worktrees with local changes are kept.", ) .reset(reset(&view, cx, |s| s.cleanup_deleted_worktrees, |s, on| s.cleanup_deleted_worktrees = on)) .control(switch("cleanup-deleted", saved.cleanup_deleted_worktrees, &view, p, |settings, on| { settings.cleanup_deleted_worktrees = on })), ) .row( SettingRow::new("Delete inactive worktrees") .id("storage-worktrees-inactive") .description( "Remove worktrees after their threads have been inactive for this many days. Branches and thread history are kept.", ) .reset(reset(&view, cx, |s| s.cleanup_inactive_worktrees, |s, days| s.cleanup_inactive_worktrees = days)) .control(retention("cleanup-inactive", saved.cleanup_inactive_worktrees, &view, p)), ) .row( SettingRow::new("Delete merged worktrees") .id("storage-worktrees-merged") .description("Remove worktrees whose pull request is merged and whose commits are included in the default branch.") .reset(reset(&view, cx, |s| s.cleanup_merged_worktrees, |s, on| s.cleanup_merged_worktrees = on)) .control(switch("cleanup-merged", saved.cleanup_merged_worktrees, &view, p, |settings, on| { settings.cleanup_merged_worktrees = on })), ) .row( SettingRow::new("Delete unchanged worktrees") .id("storage-worktrees-unchanged") .description("Remove worktrees with no commits beyond the default branch.") .reset(reset(&view, cx, |s| s.cleanup_unchanged_worktrees, |s, on| s.cleanup_unchanged_worktrees = on)) .control(switch("cleanup-unchanged", saved.cleanup_unchanged_worktrees, &view, p, |settings, on| { settings.cleanup_unchanged_worktrees = on })), ) .render(cx); let settings_dir = settings::path().as_deref().and_then(Path::parent).map(Path::to_path_buf); let folder_row = |title: &'static str, id: &'static str, path: Option| { SettingRow::new(title) .description(path.as_deref().map(display_path).unwrap_or_else(|| "Unavailable".into())) .control( MetalButton::new(id) .compact() .icon(IconName::FolderOpen) .label("Open") .disabled(path.as_ref().is_none_or(|path| !path.exists())) .on_click(move |_, _, _| { if let Some(path) = &path { open_folder(path); } }), ) .into_any_element() }; let files = Section::new("Files") .id("storage-files") .row(folder_row("Settings", "open-settings-dir", settings_dir)) .row(folder_row("Threads", "open-threads-dir", data.as_ref().map(|data| data.join("threads")))) .row(folder_row( "Worktrees", "open-worktrees-dir", Some(settings::get(|settings| settings.worktrees_dir.clone())) .filter(|dir| !dir.trim().is_empty()) .map(|dir| settings::expand_home(dir.trim())) .or(default_worktrees), )) .row(folder_row("Threads without a project", "open-scratch-dir", workspace::scratch_dir())) .render(cx); vec![worktrees, files] } } pub(super) fn open_folder(path: &Path) { let Some(program) = workspace::find_program(&["xdg-open"]) else { return }; let spawned = Command::new(program).arg(path).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()).spawn(); if let Ok(mut child) = spawned { std::thread::spawn(move || child.wait()); } } /// T3 Code's retention control: a switch, and once it's on, how many days. fn retention(id: &'static str, days: Option, view: &WeakEntity, p: &Palette) -> AnyElement { let toggle = metal::toggle(SharedString::from(format!("{id}-toggle")), days.is_some(), p, { let view = view.clone(); move |on, window, cx| { super::widgets::edit(&view, window, cx, |settings| settings.cleanup_inactive_worktrees = on.then_some(8)); } }); match days { None => gpui_kit::component::h_flex() .gap(px(8.)) .child(div().text_size(px(12.)).text_color(p.ink3).child("Off")) .child(toggle) .into_any_element(), Some(days) => gpui_kit::component::h_flex() .gap(px(8.)) .child(stepper(id, u64::from(days), 1, (1, 3650), "days", view, p, |settings, days| { settings.cleanup_inactive_worktrees = Some(days as u32) })) .child(toggle) .into_any_element(), } }