group request history by host names

This commit is contained in:
yggverse 2025-01-13 02:46:53 +02:00
parent c403039abc
commit d10987ff4e
3 changed files with 65 additions and 39 deletions

View file

@ -1,14 +1,16 @@
use super::{BrowserAction, Profile, WindowAction}; use super::{BrowserAction, Profile, WindowAction};
use gtk::{ use gtk::{
gio::{self}, gio::{self},
glib::{GString, Uri},
prelude::{ActionExt, EditableExt, ToVariant}, prelude::{ActionExt, EditableExt, ToVariant},
Align, MenuButton, Align, MenuButton,
}; };
use indexmap::IndexMap;
use std::rc::Rc; use std::rc::Rc;
// Config options // Config options
const LABEL_MAX_LENGTH: usize = 32; const LABEL_MAX_LENGTH: usize = 28;
pub struct Menu { pub struct Menu {
pub menu_button: MenuButton, pub menu_button: MenuButton,
} }
@ -192,7 +194,7 @@ impl Menu {
// Bookmarks // Bookmarks
main_bookmarks.remove_all(); main_bookmarks.remove_all();
for request in profile.bookmark.memory.recent() { 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!( menu_item.set_action_and_target_value(Some(&format!(
"{}.{}", "{}.{}",
window_action.id, window_action.id,
@ -210,7 +212,7 @@ impl Menu {
main_history_tab.remove_all(); main_history_tab.remove_all();
for item in profile.history.memory.tab.recent() { for item in profile.history.memory.tab.recent() {
let item_request = item.page.navigation.request.widget.entry.text(); // @TODO restore entire `Item` 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!( menu_item.set_action_and_target_value(Some(&format!(
"{}.{}", "{}.{}",
window_action.id, window_action.id,
@ -221,16 +223,33 @@ impl Menu {
} }
// Recently visited history // 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(); 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); let mut list: IndexMap<GString, Vec<Uri>> = IndexMap::new();
menu_item.set_action_and_target_value(Some(&format!( 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.id,
window_action.open.simple_action.name() window_action.open.simple_action.name()
)), Some(&request.to_variant())); )), Some(&uri.to_string().to_variant()));
list.append_item(&item);
main_history_request.append_item(&menu_item); }
main_history_request.append_submenu(Some(&group), &list);
} }
} }
}); });
@ -242,14 +261,9 @@ impl Menu {
} }
} }
/// Format dynamically generated strings for menu item labels /// Format dynamically generated strings for menu item label
/// * trim gemini scheme prefix
/// * trim slash postfix
/// * crop resulting string at the middle position on new `value` longer than `limit` /// * crop resulting string at the middle position on new `value` longer than `limit`
fn label(value: &str, limit: usize) -> String { fn ellipsize(value: &str, limit: usize) -> String {
let value = value.trim_start_matches("gemini://");
let value = value.trim_end_matches('/');
if value.len() <= limit { if value.len() <= limit {
return value.to_string(); return value.to_string();
} }
@ -258,3 +272,12 @@ fn label(value: &str, limit: usize) -> String {
format!("{}..{}", &value[..length], &value[value.len() - length..]) 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
}
}

View file

@ -28,7 +28,7 @@ use gtk::{
gdk::Texture, gdk::Texture,
gdk_pixbuf::Pixbuf, gdk_pixbuf::Pixbuf,
gio::SocketClientEvent, gio::SocketClientEvent,
glib::{gformat, DateTime, GString, Priority, Uri, UriFlags, UriHideFlags}, glib::{gformat, GString, Priority, Uri, UriFlags, UriHideFlags},
prelude::{EditableExt, FileExt, SocketClientExt}, prelude::{EditableExt, FileExt, SocketClientExt},
}; };
use sqlite::Transaction; use sqlite::Transaction;
@ -234,7 +234,7 @@ impl Page {
scheme => { scheme => {
// Add history record // Add history record
if is_history { if is_history {
snap_history(&self.profile, &self.navigation); snap_history(&self.profile, &self.navigation, Some(uri));
} }
// Update widget // Update widget
@ -322,7 +322,7 @@ impl Page {
self.navigation.restore(transaction, &record.id)?; self.navigation.restore(transaction, &record.id)?;
// Make initial page history snap using `navigation` values restored // Make initial page history snap using `navigation` values restored
// * just to have back/forward navigation ability // * 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()), Err(e) => return Err(e.to_string()),
@ -478,7 +478,7 @@ impl Page {
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-20 // https://geminiprotocol.net/docs/protocol-specification.gmi#status-20
response::meta::Status::Success => { response::meta::Status::Success => {
if is_history { if is_history {
snap_history(&profile, &navigation); snap_history(&profile, &navigation, Some(&uri));
} }
if is_download { if is_download {
// Init download widget // Init download widget
@ -806,7 +806,7 @@ impl Page {
response::meta::Status::CertificateInvalid => { response::meta::Status::CertificateInvalid => {
// Add history record // Add history record
if is_history { if is_history {
snap_history(&profile, &navigation); snap_history(&profile, &navigation, Some(&uri));
} }
// Update widget // Update widget
@ -831,7 +831,7 @@ impl Page {
_ => { _ => {
// Add history record // Add history record
if is_history { if is_history {
snap_history(&profile, &navigation); snap_history(&profile, &navigation, Some(&uri));
} }
// Update widget // Update widget
@ -854,7 +854,7 @@ impl Page {
Err(e) => { Err(e) => {
// Add history record // Add history record
if is_history { if is_history {
snap_history(&profile, &navigation); snap_history(&profile, &navigation, Some(&uri));
} }
// Update widget // Update widget
@ -921,15 +921,13 @@ fn is_external_uri(subject: &Uri, base: &Uri) -> bool {
/// Make new history record for given `navigation` object /// Make new history record for given `navigation` object
/// * applies on shared conditions match only /// * 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(); let request = navigation.request.widget.entry.text();
// Add new record into the global memory index (used in global menu) // Add new record into the global memory index (used in global menu)
profile if let Some(uri) = uri {
.history profile.history.memory.request.set(uri);
.memory }
.request
.set(request.clone(), DateTime::now_local().unwrap().to_unix());
// Add new record into the page navigation history // Add new record into the page navigation history
if match navigation.history.current() { if match navigation.history.current() {

View file

@ -1,9 +1,10 @@
use gtk::glib::GString; use gtk::glib::{DateTime, GString, Uri};
use itertools::Itertools; use itertools::Itertools;
use std::{cell::RefCell, collections::HashMap}; use std::{cell::RefCell, collections::HashMap};
pub struct Value { pub struct Value {
pub unix_timestamp: i64, pub unix_timestamp: i64,
pub uri: Uri,
} }
/// Recent request history /// Recent request history
@ -31,23 +32,27 @@ impl Request {
/// Add new record with `request` as key and `unix_timestamp` as value /// Add new record with `request` as key and `unix_timestamp` as value
/// * replace with new value if `request` already exists /// * replace with new value if `request` already exists
pub fn set(&self, request: GString, unix_timestamp: i64) { pub fn set(&self, uri: &Uri) {
self.index self.index.borrow_mut().insert(
.borrow_mut() uri.to_str(),
.insert(request, Value { unix_timestamp }); 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 /// * sorted by `unix_timestamp` DESC
pub fn recent(&self) -> Vec<GString> { pub fn recent(&self) -> Vec<Uri> {
let mut recent: Vec<GString> = Vec::new(); let mut recent: Vec<Uri> = Vec::new();
for (request, _) in self for (_, value) in self
.index .index
.borrow() .borrow()
.iter() .iter()
.sorted_by(|a, b| Ord::cmp(&b.1.unix_timestamp, &a.1.unix_timestamp)) .sorted_by(|a, b| Ord::cmp(&b.1.unix_timestamp, &a.1.unix_timestamp))
{ {
recent.push(request.clone()) recent.push(value.uri.clone())
} }
recent recent
} }