diff --git a/Cargo.lock b/Cargo.lock index 2b21b37..640cad9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1429,6 +1429,7 @@ dependencies = [ "gpui-omarchy", "rustc-hash 2.1.3", "tempfile", + "url", "windows-sys 0.61.2", ] diff --git a/README.md b/README.md index 7ee7533..6d0cfac 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,9 @@ disktree --help # options: apparent size, follow links, skip hidden, … what is marked; and the disk, free now and after the marks, with the way to the review screen. Drag its left edge to resize it; double-click the edge to reset. + The search icon beside a selected folder's path opens Google in your + browser with the platform and displayed path, asking what the folder is, + what application it belongs to, and the risks of deleting it. One colour is kept apart: amber marks the selection, the main action, and what can be had back. diff --git a/crates/disktree-app/Cargo.toml b/crates/disktree-app/Cargo.toml index 867d564..8490691 100644 --- a/crates/disktree-app/Cargo.toml +++ b/crates/disktree-app/Cargo.toml @@ -20,6 +20,7 @@ disktree-core = { workspace = true } anyhow = { workspace = true } dunce = "1.0.5" rustc-hash = { workspace = true } +url = "2.5" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = ["Win32_System_Console"] } diff --git a/crates/disktree-app/src/state.rs b/crates/disktree-app/src/state.rs index 27e1846..67e34c8 100644 --- a/crates/disktree-app/src/state.rs +++ b/crates/disktree-app/src/state.rs @@ -2219,6 +2219,26 @@ impl Disktree { cx.notify(); } + /// Research the path shown beside the clicked button in the browser. + pub fn search_folder(&self, path: &Path, cx: &Context<'_, Self>) { + let platform = match std::env::consts::OS { + "macos" => "macOS", + "windows" => "Windows", + "linux" => "Linux", + other => other, + }; + let path = crate::marks::display_path(path, self.home.as_deref()); + let query = format!( + "{platform} \"{path}\" what is this folder, what application \ + does it belong to, and what are the risks of deleting it?" + ); + let mut url = url::Url::parse("https://www.google.com/search") + .expect("the Google search URL is valid"); + // Encode the whole query so punctuation in a path stays search text. + url.query_pairs_mut().append_pair("q", &query); + cx.open_url(url.as_str()); + } + /// Show the tile a key acts on in Finder (or the file manager), selected. pub fn reveal_target(&mut self, cx: &mut Context<'_, Self>) { let crumbs = diff --git a/crates/disktree-app/src/tests.rs b/crates/disktree-app/src/tests.rs index af151ec..246c1d7 100644 --- a/crates/disktree-app/src/tests.rs +++ b/crates/disktree-app/src/tests.rs @@ -162,6 +162,48 @@ fn the_first_scan_shows_what_it_is_doing_then_the_treemap( assert!(hidden_present, "hidden directories are part of the tree"); } +/// Clicking the panel researches its displayed folder, even when punctuation +/// in the name would otherwise split the URL into parameters or a fragment. +#[gpui_kit::test] +fn folder_search_opens_google_with_the_displayed_path(cx: &mut TestAppContext) { + cx.update(gpui_omarchy::init); + let temp = fixture(); + let name = "cache & data+#日本"; + std::fs::create_dir(temp.path().join(name)).expect("mkdir"); + let (view, cx) = view_over(temp.path(), cx); + cx.simulate_resize(gpui_kit::size(px(1400.), px(900.))); + update(&view, cx, |app, cx| { + app.home = Some(temp.path().to_path_buf()); + let folder = child_crumbs(app, &[], name); + app.select(Some(folder), cx); + }); + draw(cx); + assert!(cx.cx.opened_url().is_none()); + let button = cx.debug_bounds("search-folder").expect("search icon"); + cx.simulate_click(button.center(), gpui_kit::Modifiers::none()); + let opened = cx.cx.opened_url().expect("opened browser URL"); + let url = url::Url::parse(&opened).expect("valid URL"); + assert_eq!(url.host_str(), Some("www.google.com")); + assert_eq!(url.path(), "/search"); + assert!(url.fragment().is_none()); + let pairs: Vec<_> = url.query_pairs().collect(); + assert_eq!(pairs.len(), 1); + assert_eq!(pairs[0].0, "q"); + let query = &pairs[0].1; + let separator = std::path::MAIN_SEPARATOR; + assert!(query.contains(&format!("~{separator}{name}"))); + assert!(query.contains("what is this folder")); + assert!(query.contains("what application does it belong to")); + assert!(query.contains("risks of deleting it")); + let platform = match std::env::consts::OS { + "macos" => "macOS", + "windows" => "Windows", + "linux" => "Linux", + other => other, + }; + assert!(query.starts_with(platform)); +} + #[gpui_kit::test] fn keys_walk_the_tree_and_mark_what_is_selected(cx: &mut TestAppContext) { cx.update(gpui_omarchy::init); diff --git a/crates/disktree-app/src/views.rs b/crates/disktree-app/src/views.rs index 8f75253..22e230a 100644 --- a/crates/disktree-app/src/views.rs +++ b/crates/disktree-app/src/views.rs @@ -1066,14 +1066,53 @@ fn selection_section( ) .child( div() - .text_size(text::CAPTION) - .text_color(theme.secondary) - .whitespace_nowrap() - .overflow_hidden() - .text_ellipsis() - .child(path.as_deref().map_or_else(String::new, |path| { - crate::marks::display_path(path, app.home.as_deref()) - })), + .flex() + .flex_row() + .items_center() + .gap(space::XS) + .min_w_0() + .child( + div() + .min_w_0() + .text_size(text::CAPTION) + .text_color(theme.secondary) + .whitespace_nowrap() + .overflow_hidden() + .text_ellipsis() + .child(path.as_deref().map_or_else( + String::new, + |path| { + crate::marks::display_path( + path, + app.home.as_deref(), + ) + }, + )), + ) + .when(node.is_dir(), |row| { + row.children(path.clone().map(|path| { + with_tooltip( + gpui_omarchy::icon_button( + "search-folder", + gpui_omarchy::IconName::Search, + "Look up this folder on Google", + ButtonVariant::Secondary, + cx, + ) + .debug_selector(|| "search-folder".into()) + .tab_stop(false) + .flex_shrink_0() + .on_click(cx.listener( + move |this, _, window, cx| { + this.search_folder(&path, cx); + window.focus(&this.focus, cx); + }, + )), + "Search Google for this folder's purpose, \ + application, and deletion risks", + ) + })) + }), ); let (number, unit) = match app.options.metric {