From d10987ff4e90b5909f6bdd34a36ac304f83b3802 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 13 Jan 2025 02:46:53 +0200 Subject: [PATCH] group request history by host names --- src/app/browser/window/header/bar/menu.rs | 55 ++++++++++++++++------- src/app/browser/window/tab/item/page.rs | 24 +++++----- src/profile/history/memory/request.rs | 25 ++++++----- 3 files changed, 65 insertions(+), 39 deletions(-) diff --git a/src/app/browser/window/header/bar/menu.rs b/src/app/browser/window/header/bar/menu.rs index a63668f8..d21f4bfb 100644 --- a/src/app/browser/window/header/bar/menu.rs +++ b/src/app/browser/window/header/bar/menu.rs @@ -1,14 +1,16 @@ use super::{BrowserAction, Profile, WindowAction}; use gtk::{ gio::{self}, + glib::{GString, Uri}, prelude::{ActionExt, EditableExt, ToVariant}, Align, MenuButton, }; +use indexmap::IndexMap; use std::rc::Rc; // Config options -const LABEL_MAX_LENGTH: usize = 32; +const LABEL_MAX_LENGTH: usize = 28; pub struct Menu { pub menu_button: MenuButton, } @@ -192,7 +194,7 @@ impl Menu { // Bookmarks main_bookmarks.remove_all(); for request in profile.bookmark.memory.recent() { - let menu_item = gio::MenuItem::new(Some(&label(&request, LABEL_MAX_LENGTH)), None); + let menu_item = gio::MenuItem::new(Some(&ellipsize(&request, LABEL_MAX_LENGTH)), None); menu_item.set_action_and_target_value(Some(&format!( "{}.{}", window_action.id, @@ -210,7 +212,7 @@ impl Menu { main_history_tab.remove_all(); for item in profile.history.memory.tab.recent() { let item_request = item.page.navigation.request.widget.entry.text(); // @TODO restore entire `Item` - let menu_item = gio::MenuItem::new(Some(&label(&item_request, LABEL_MAX_LENGTH)), None); + let menu_item = gio::MenuItem::new(Some(&ellipsize(&item_request, LABEL_MAX_LENGTH)), None); menu_item.set_action_and_target_value(Some(&format!( "{}.{}", window_action.id, @@ -221,16 +223,33 @@ impl Menu { } // Recently visited history + // * in first iteration, group records by it hostname + // * in second iteration, collect uri path as the menu sub-item label main_history_request.remove_all(); - for request in profile.history.memory.request.recent() { - let menu_item = gio::MenuItem::new(Some(&label(&request, LABEL_MAX_LENGTH)), None); - menu_item.set_action_and_target_value(Some(&format!( + + let mut list: IndexMap> = IndexMap::new(); + for uri in profile.history.memory.request.recent() { + list.entry(match uri.host() { + Some(host) => host, + None => uri.to_str(), + }).or_default().push(uri); + } + + for (group, items) in list { + let list = gio::Menu::new(); + for uri in items { + let item = gio::MenuItem::new(Some(&ellipsize( + &uri_to_label(&uri), + LABEL_MAX_LENGTH + )), None); + item.set_action_and_target_value(Some(&format!( "{}.{}", window_action.id, window_action.open.simple_action.name() - )), Some(&request.to_variant())); - - main_history_request.append_item(&menu_item); + )), Some(&uri.to_string().to_variant())); + list.append_item(&item); + } + main_history_request.append_submenu(Some(&group), &list); } } }); @@ -242,14 +261,9 @@ impl Menu { } } -/// Format dynamically generated strings for menu item labels -/// * trim gemini scheme prefix -/// * trim slash postfix +/// Format dynamically generated strings for menu item label /// * crop resulting string at the middle position on new `value` longer than `limit` -fn label(value: &str, limit: usize) -> String { - let value = value.trim_start_matches("gemini://"); - let value = value.trim_end_matches('/'); - +fn ellipsize(value: &str, limit: usize) -> String { if value.len() <= limit { return value.to_string(); } @@ -258,3 +272,12 @@ fn label(value: &str, limit: usize) -> String { format!("{}..{}", &value[..length], &value[value.len() - length..]) } + +fn uri_to_label(uri: &Uri) -> GString { + let path = uri.path(); + if path == "/" { + uri.host().unwrap_or(uri.to_str()) + } else { + path + } +} diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index 2564cbed..4ffa60e9 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -28,7 +28,7 @@ use gtk::{ gdk::Texture, gdk_pixbuf::Pixbuf, gio::SocketClientEvent, - glib::{gformat, DateTime, GString, Priority, Uri, UriFlags, UriHideFlags}, + glib::{gformat, GString, Priority, Uri, UriFlags, UriHideFlags}, prelude::{EditableExt, FileExt, SocketClientExt}, }; use sqlite::Transaction; @@ -234,7 +234,7 @@ impl Page { scheme => { // Add history record if is_history { - snap_history(&self.profile, &self.navigation); + snap_history(&self.profile, &self.navigation, Some(uri)); } // Update widget @@ -322,7 +322,7 @@ impl Page { self.navigation.restore(transaction, &record.id)?; // Make initial page history snap using `navigation` values restored // * just to have back/forward navigation ability - snap_history(&self.profile, &self.navigation); + snap_history(&self.profile, &self.navigation, None); } } Err(e) => return Err(e.to_string()), @@ -478,7 +478,7 @@ impl Page { // https://geminiprotocol.net/docs/protocol-specification.gmi#status-20 response::meta::Status::Success => { if is_history { - snap_history(&profile, &navigation); + snap_history(&profile, &navigation, Some(&uri)); } if is_download { // Init download widget @@ -806,7 +806,7 @@ impl Page { response::meta::Status::CertificateInvalid => { // Add history record if is_history { - snap_history(&profile, &navigation); + snap_history(&profile, &navigation, Some(&uri)); } // Update widget @@ -831,7 +831,7 @@ impl Page { _ => { // Add history record if is_history { - snap_history(&profile, &navigation); + snap_history(&profile, &navigation, Some(&uri)); } // Update widget @@ -854,7 +854,7 @@ impl Page { Err(e) => { // Add history record if is_history { - snap_history(&profile, &navigation); + snap_history(&profile, &navigation, Some(&uri)); } // Update widget @@ -921,15 +921,13 @@ fn is_external_uri(subject: &Uri, base: &Uri) -> bool { /// Make new history record for given `navigation` object /// * applies on shared conditions match only -fn snap_history(profile: &Profile, navigation: &Navigation) { +fn snap_history(profile: &Profile, navigation: &Navigation, uri: Option<&Uri>) { let request = navigation.request.widget.entry.text(); // Add new record into the global memory index (used in global menu) - profile - .history - .memory - .request - .set(request.clone(), DateTime::now_local().unwrap().to_unix()); + if let Some(uri) = uri { + profile.history.memory.request.set(uri); + } // Add new record into the page navigation history if match navigation.history.current() { diff --git a/src/profile/history/memory/request.rs b/src/profile/history/memory/request.rs index 2537f1a7..1fd64e39 100644 --- a/src/profile/history/memory/request.rs +++ b/src/profile/history/memory/request.rs @@ -1,9 +1,10 @@ -use gtk::glib::GString; +use gtk::glib::{DateTime, GString, Uri}; use itertools::Itertools; use std::{cell::RefCell, collections::HashMap}; pub struct Value { pub unix_timestamp: i64, + pub uri: Uri, } /// Recent request history @@ -31,23 +32,27 @@ impl Request { /// Add new record with `request` as key and `unix_timestamp` as value /// * replace with new value if `request` already exists - pub fn set(&self, request: GString, unix_timestamp: i64) { - self.index - .borrow_mut() - .insert(request, Value { unix_timestamp }); + pub fn set(&self, uri: &Uri) { + self.index.borrow_mut().insert( + uri.to_str(), + Value { + unix_timestamp: DateTime::now_local().unwrap().to_unix(), + uri: uri.clone(), + }, + ); } - /// Get recent requests vector + /// Get recent records vector /// * sorted by `unix_timestamp` DESC - pub fn recent(&self) -> Vec { - let mut recent: Vec = Vec::new(); - for (request, _) in self + pub fn recent(&self) -> Vec { + let mut recent: Vec = Vec::new(); + for (_, value) in self .index .borrow() .iter() .sorted_by(|a, b| Ord::cmp(&b.1.unix_timestamp, &a.1.unix_timestamp)) { - recent.push(request.clone()) + recent.push(value.uri.clone()) } recent }