Vectrum
Sign In
149 lines · 7.2 KB · Rust
//! 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<Self>) -> Vec<AnyElement> {
        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<PathBuf>| {
            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<u32>, view: &WeakEntity<SettingsView>, 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(),
    }
}