diff --git a/src/app/browser/window/tab/item/client/driver/gemini.rs b/src/app/browser/window/tab/item/client/driver/gemini.rs index f027dcf9..6498a82d 100644 --- a/src/app/browser/window/tab/item/client/driver/gemini.rs +++ b/src/app/browser/window/tab/item/client/driver/gemini.rs @@ -34,28 +34,54 @@ impl Gemini { pub fn init(page: &Rc) -> Self { // Init supported protocol libraries let client = Rc::new(ggemini::Client::new()); - // Listen for [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html) updates client.socket.connect_event({ - let page = page.clone(); + let p = page.clone(); move |_, event, _, _| { - page.set_progress(match event { + let mut i = p.info.borrow_mut(); + p.set_progress(match event { // 0.1 reserved for handle begin - SocketClientEvent::Resolving => 0.2, - SocketClientEvent::Resolved => 0.3, - SocketClientEvent::Connecting => 0.4, - SocketClientEvent::Connected => 0.5, - SocketClientEvent::ProxyNegotiating => 0.6, - SocketClientEvent::ProxyNegotiated => 0.7, + SocketClientEvent::Resolving => { + i.add_event("Resolving".to_string()); + 0.2 + } + SocketClientEvent::Resolved => { + i.add_event("Resolved".to_string()); + 0.3 + } + SocketClientEvent::Connecting => { + i.add_event("Connecting".to_string()); + 0.4 + } + SocketClientEvent::Connected => { + i.add_event("Connected".to_string()); + 0.5 + } + SocketClientEvent::ProxyNegotiating => { + i.add_event("Proxy negotiating".to_string()); + 0.6 + } + SocketClientEvent::ProxyNegotiated => { + i.add_event("Proxy negotiated".to_string()); + 0.7 + } // * `TlsHandshaking` | `TlsHandshaked` has effect only for guest connections! - SocketClientEvent::TlsHandshaking => 0.8, - SocketClientEvent::TlsHandshaked => 0.9, - SocketClientEvent::Complete => 1.0, - _ => todo!(), // alert on API change + SocketClientEvent::TlsHandshaking => { + i.add_event("TLS handshaking".to_string()); + 0.8 + } + SocketClientEvent::TlsHandshaked => { + i.add_event("TLS handshaked".to_string()); + 0.9 + } + SocketClientEvent::Complete => { + i.add_event("Complete".to_string()); + 1.0 + } + _ => panic!(), // alert on API change }) } }); - Self { client, redirects: Rc::new(Cell::new(0)), @@ -73,7 +99,6 @@ impl Gemini { is_snap_history: bool, ) { use ggemini::client::connection::Request; - match uri.scheme().as_str() { "gemini" => handle( Request::Gemini { uri }, @@ -153,26 +178,27 @@ fn handle( Ok((response, connection)) => match response { // https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected Response::Input(input) => { - let title = input.to_string(); + let t = input.to_string(); page.set_progress(0.0); - page.set_title(&title); + page.set_title(&t); if is_snap_history { page.snap_history(); } redirects.replace(0); // reset + update_page_info(&page, &uri, "Done"); match input { // https://geminiprotocol.net/docs/protocol-specification.gmi#status-10 Input::Default { message } => page.input.set_new_response( page.item_action.clone(), uri, - Some(message.as_ref().unwrap_or(&title)), + Some(message.as_ref().unwrap_or(&t)), Some(1024), ), // https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input Input::Sensitive { message } => page.input.set_new_sensitive( page.item_action.clone(), uri, - Some(message.as_ref().unwrap_or(&title)), + Some(message.as_ref().unwrap_or(&t)), Some(1024), ) } @@ -181,7 +207,7 @@ fn handle( Response::Success(success) => match *feature { Feature::Download => { // Init download widget - let status = page.content.to_status_download( + let s = page.content.to_status_download( crate::tool::uri_to_title(&uri).trim_matches(MAIN_SEPARATOR), // grab default filename from base URI, // format FS entities &cancellable, @@ -241,7 +267,7 @@ fn handle( }, ); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); if is_snap_history { page.snap_history(); } @@ -260,13 +286,19 @@ fn handle( |_, _| {}, // on chunk (maybe nothing to count yet @TODO) move |result| match result { // on complete Ok((memory_input_stream, total)) => memory_input_stream.read_all_async( - vec![0;total], + vec![0; total], Priority::DEFAULT, Some(&cancellable), move |result| match result { Ok((buffer, _ ,_)) => match std::str::from_utf8(&buffer) { Ok(data) => { - let widget = if matches!(*feature, Feature::Source) { + let mut i = page.info.borrow_mut(); + i + .add_event("Parsing".to_string()) + .set_mime(Some(success.mime().to_string())) + .set_request(Some(uri.to_string())) + .set_size(Some(data.len())); + let w = if matches!(*feature, Feature::Source) { page.content.to_text_source(data) } else { match success.mime() { @@ -275,9 +307,10 @@ fn handle( _ => panic!() // unexpected } }; - page.search.set(Some(widget.text_view)); - page.set_title(&match widget.meta.title { - Some(title) => title.into(), // @TODO + i.add_event("Parsed".to_string()); + page.search.set(Some(w.text_view)); + page.set_title(&match w.meta.title { + Some(t) => t.into(), // @TODO None => crate::tool::uri_to_title(&uri), }); page.set_progress(0.0); @@ -289,39 +322,43 @@ fn handle( page.snap_history(); } redirects.replace(0); // reset + i.add_event("Done".to_string()); }, Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(&e.to_string())); + let s = page.content.to_status_failure(); + s.set_description(Some(&e.to_string())); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); if is_snap_history { page.snap_history(); } redirects.replace(0); // reset + update_page_info(&page, &uri, "Done"); }, }, Err((_, e)) => { - let status = page.content.to_status_failure(); - status.set_description(Some(&e.to_string())); + let s = page.content.to_status_failure(); + s.set_description(Some(&e.to_string())); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); if is_snap_history { page.snap_history(); } redirects.replace(0); // reset + update_page_info(&page, &uri, "Done"); } } ), Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(&e.to_string())); + let s = page.content.to_status_failure(); + s.set_description(Some(&e.to_string())); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); if is_snap_history { page.snap_history(); } redirects.replace(0); // reset + update_page_info(&page, &uri, "Done"); }, } ) @@ -357,11 +394,27 @@ fn handle( Ok(buffer) => { page.set_title(&crate::tool::uri_to_title(&uri)); page.content.to_image(&Texture::for_pixbuf(&buffer)); + { + let mut i = page.info.borrow_mut(); + i + .add_event("Done".to_string()) + .set_mime(Some(success.mime().to_string())) + .set_request(Some(uri.to_string())) + .set_size(Some(buffer.byte_length())); + } } Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(e.message())); - page.set_title(&status.title()); + let s = page.content.to_status_failure(); + s.set_description(Some(e.message())); + page.set_title(&s.title()); + { + let mut i = page.info.borrow_mut(); + i + .add_event("Done".to_string()) + .set_mime(Some(success.mime().to_string())) + .set_request(Some(uri.to_string())) + .set_size(None); + } } } page.set_progress(0.0); @@ -373,14 +426,22 @@ fn handle( ) } Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(&e.to_string())); + let s = page.content.to_status_failure(); + s.set_description(Some(&e.to_string())); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); if is_snap_history { page.snap_history(); } redirects.replace(0); // reset + { + let mut i = page.info.borrow_mut(); + i + .add_event("Done".to_string()) + .set_mime(Some(success.mime().to_string())) + .set_request(Some(uri.to_string())) + .set_size(None); + } } } } @@ -388,16 +449,24 @@ fn handle( ) } mime => { - let status = page + let s = page .content .to_status_mime(mime, Some((&page.item_action, &uri))); - status.set_description(Some(&format!("Content type `{mime}` yet not supported"))); + s.set_description(Some(&format!("Content type `{mime}` yet not supported"))); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); if is_snap_history { page.snap_history(); } redirects.replace(0); // reset + { + let mut i = page.info.borrow_mut(); + i + .add_event("Done".to_string()) + .set_mime(Some(mime.to_string())) + .set_request(Some(uri.to_string())) + .set_size(None); + } }, } }, @@ -410,49 +479,63 @@ fn handle( // > Client MUST limit the number of redirections they follow to 5 redirections // > https://geminiprotocol.net/docs/protocol-specification.gmi#redirection if total > 5 { - let status = page.content.to_status_failure(); - status.set_description(Some("Redirection limit reached")); + let s = page.content.to_status_failure(); + s.set_description(Some("Redirection limit reached")); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); redirects.replace(0); // reset + update_page_info(&page, &uri, "Done"); // Disallow external redirection by default as potentially unsafe // even not specified, require follow confirmation @TODO optional } else if uri.host() != target.host() { - let url = target.to_string(); - let status = page.content.to_status_failure(); - let title = "External redirection"; - status.set_title(title); - status.set_icon_name(Some("dialog-warning-symbolic")); - status.set_description(Some(&url)); - status.set_child(Some(&{ - let button = gtk::Button::builder() + let u = target.to_string(); + let s = page.content.to_status_failure(); + let t = "External redirection"; + s.set_title(t); + s.set_icon_name(Some("dialog-warning-symbolic")); + s.set_description(Some(&u)); + s.set_child(Some(&{ + let b = gtk::Button::builder() .css_classes(["suggested-action"]) .halign(gtk::Align::Center) .label("Follow") .build(); - button.connect_clicked({ - let page = page.clone(); - move |_| page.item_action.load.activate(Some(&url), false) + b.connect_clicked({ + let p = page.clone(); + move |_| p.item_action.load.activate(Some(&u), false) }); - button + b })); page.set_progress(0.0); - page.set_title(title); + page.set_title(t); redirects.replace(0); // reset + update_page_info(&page, &uri, "Done"); } else { + let t = target.to_string(); if matches!(redirect, Redirect::Permanent { .. }) { - page.navigation.set_request(&target.to_string()); + page.navigation.set_request(&t); } redirects.replace(total); - page.item_action.load.activate(Some(&target.to_string()), false); + { + let mut i = page.info.take(); + i + .add_event("Done".to_string()) + .set_mime(None) + .set_request(Some(uri.to_string())) + .set_size(None); + + page.info.replace(i.into_redirect()); + } + page.item_action.load.activate(Some(&t), false); } } Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(&e.to_string())); + let s = page.content.to_status_failure(); + s.set_description(Some(&e.to_string())); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); redirects.replace(0); // reset + update_page_info(&page, &uri, "Done"); } } Response::Certificate(ref certificate) => match certificate { @@ -462,14 +545,15 @@ fn handle( Certificate::NotAuthorized { message } | // https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid Certificate::NotValid { message } => { - let status = page.content.to_status_identity(); - status.set_description(Some(message.as_ref().unwrap_or(&certificate.to_string()))); + let s = page.content.to_status_identity(); + s.set_description(Some(message.as_ref().unwrap_or(&certificate.to_string()))); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); if is_snap_history { page.snap_history(); } redirects.replace(0); // reset + update_page_info(&page, &uri, "Done"); } } Response::Failure(failure) => match failure { @@ -479,14 +563,15 @@ fn handle( Temporary::ProxyError { message } | Temporary::ServerUnavailable { message } | Temporary::SlowDown { message } => { - let status = page.content.to_status_failure(); - status.set_description(Some(message.as_ref().unwrap_or(&temporary.to_string()))); + let s = page.content.to_status_failure(); + s.set_description(Some(message.as_ref().unwrap_or(&temporary.to_string()))); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); if is_snap_history { page.snap_history(); } redirects.replace(0); // reset + update_page_info(&page, &uri, "Done"); if let Some(callback) = on_failure { callback() } @@ -498,14 +583,15 @@ fn handle( Permanent::Gone { message } | Permanent::NotFound { message } | Permanent::ProxyRequestRefused { message } => { - let status = page.content.to_status_failure(); - status.set_description(Some(message.as_ref().unwrap_or(&permanent.to_string()))); + let s = page.content.to_status_failure(); + s.set_description(Some(message.as_ref().unwrap_or(&permanent.to_string()))); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); if is_snap_history { page.snap_history(); } redirects.replace(0); // reset + update_page_info(&page, &uri, "Done"); if let Some(callback) = on_failure { callback() } @@ -514,16 +600,26 @@ fn handle( } } Err(e) => { - let status = page.content.to_status_failure(); - status.set_description(Some(&e.to_string())); + let s = page.content.to_status_failure(); + s.set_description(Some(&e.to_string())); page.set_progress(0.0); - page.set_title(&status.title()); + page.set_title(&s.title()); if is_snap_history { page.snap_history(); } redirects.replace(0); // reset + update_page_info(&page, &uri, "Done") } } }, ) } + +/// Apply common page info pattern +fn update_page_info(page: &Page, uri: &Uri, event_name: &str) { + let mut i = page.info.borrow_mut(); + i.add_event(event_name.to_string()) + .set_mime(None) + .set_request(Some(uri.to_string())) + .set_size(None); +} diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index aee25c3e..2277dc55 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -1,5 +1,6 @@ mod content; mod database; +mod info; mod input; mod navigation; mod search; @@ -8,11 +9,12 @@ use super::{Action as ItemAction, BrowserAction, Profile, TabAction, WindowActio use adw::TabPage; use anyhow::Result; use content::Content; +use info::Info; use input::Input; use navigation::Navigation; use search::Search; use sqlite::Transaction; -use std::{rc::Rc, sync::Arc}; +use std::{cell::RefCell, rc::Rc, sync::Arc}; pub struct Page { pub profile: Arc, @@ -21,6 +23,7 @@ pub struct Page { pub item_action: Rc, pub window_action: Rc, // Components + pub info: Rc>, pub content: Rc, pub input: Rc, pub navigation: Rc, @@ -55,6 +58,7 @@ impl Page { (window_action, tab_action, item_action), )); let input = Rc::new(Input::new()); + let info = Rc::new(RefCell::new(Info::new())); // Done Self { @@ -65,6 +69,7 @@ impl Page { item_action: item_action.clone(), window_action: window_action.clone(), // Components + info, content, input, navigation, diff --git a/src/app/browser/window/tab/item/page/info.rs b/src/app/browser/window/tab/item/page/info.rs new file mode 100644 index 00000000..d293a86b --- /dev/null +++ b/src/app/browser/window/tab/item/page/info.rs @@ -0,0 +1,87 @@ +// Public dependencies + +pub mod event; +pub use event::Event; + +// Local dependencies + +use gtk::gio::NetworkAddress; + +/// Common, shared `Page` information holder +/// * used for the Information dialog window on request indicator activate +/// * collecting by the page driver implementation, using public API +pub struct Info { + /// Hold page events like connection phase and parsing time + event: Vec, + /// Page content type + mime: Option, + /// Hold redirections chain with handled details + /// * the `referrer` member name is reserved for other protocols + redirect: Option>, + /// Optional remote host details + /// * useful also for geo-location feature + remote: Option, + /// Key to relate data collected with the specific request + request: Option, + /// Hold page content size + size: Option, +} + +impl Info { + // Constructors + + /// Create new empty `Self` with expected default capacity + pub fn new() -> Self { + Self { + event: Vec::with_capacity(50), // estimated max events quantity for all drivers + mime: None, + redirect: None, + remote: None, + request: None, + size: None, + } + } + + // Setters + // useful to update `Self` as chain of values + + /// Take `Self`, convert it into the redirect member, + /// then, return new `Self` back + /// * tip: use on driver redirection events + pub fn into_redirect(self) -> Self { + let mut this = Self::new(); + this.redirect = Some(Box::new(self)); + this + } + + pub fn add_event(&mut self, name: String) -> &mut Self { + self.event.push(Event::now(name)); + self + } + + pub fn set_mime(&mut self, mime: Option) -> &mut Self { + self.mime = mime; + self + } + + pub fn set_remote(&mut self, remote: Option) -> &mut Self { + self.remote = remote; + self + } + + pub fn set_request(&mut self, request: Option) -> &mut Self { + self.request = request; + self + } + + pub fn set_size(&mut self, size: Option) -> &mut Self { + self.size = size; + self + } +} + +impl Default for Info { + fn default() -> Self { + Self::new() + } +} diff --git a/src/app/browser/window/tab/item/page/info/event.rs b/src/app/browser/window/tab/item/page/info/event.rs new file mode 100644 index 00000000..d2825da0 --- /dev/null +++ b/src/app/browser/window/tab/item/page/info/event.rs @@ -0,0 +1,22 @@ +// Local dependencies + +use gtk::glib::DateTime; + +/// Single event holder +/// * used in page info dialog to track page load and parse timings +pub struct Event { + name: String, + time: DateTime, +} + +impl Event { + // Constructors + + /// Create new `Self` with auto-completed current local timestamp + pub fn now(name: String) -> Self { + Self { + name, + time: DateTime::now_local().unwrap(), + } + } +} diff --git a/src/app/browser/window/tab/item/page/navigation/request.rs b/src/app/browser/window/tab/item/page/navigation/request.rs index 36538810..f91708bd 100644 --- a/src/app/browser/window/tab/item/page/navigation/request.rs +++ b/src/app/browser/window/tab/item/page/navigation/request.rs @@ -34,7 +34,6 @@ impl Request { // Init main widget let entry = Entry::builder() .placeholder_text("URL or search term...") - .secondary_icon_tooltip_text("Go to the location") .hexpand(true) .build(); @@ -317,11 +316,22 @@ fn update_primary_icon(entry: &Entry, profile: &Profile) { } } +/// GTK `is_focus` / `has_focus` not an option here +/// also, this method requires from the `Entry`` to be not empty +fn is_focused(entry: &Entry) -> bool { + entry.text_length() > 0 && entry.focus_child().is_some_and(|child| child.has_focus()) +} + +/// Secondary icon has two modes: +/// * navigate to the location button (on the entry is focused / has edit mode) +/// * page info button with dialog window activation (on the entry is inactive) fn update_secondary_icon(entry: &Entry) { - if !entry.text().is_empty() && entry.focus_child().is_some_and(|text| text.has_focus()) { + if is_focused(entry) { entry.set_secondary_icon_name(Some("pan-end-symbolic")); + entry.set_secondary_icon_tooltip_text(Some("Go to the location")) } else { - entry.set_secondary_icon_name(None); + entry.set_secondary_icon_name(Some("help-about-symbolic")); + entry.set_secondary_icon_tooltip_text(Some("Page info")); entry.select_region(0, 0); } } @@ -336,12 +346,12 @@ fn show_identity_dialog(entry: &Entry, profile: &Arc) { profile, &uri, &Rc::new({ - let profile = profile.clone(); - let entry = entry.clone(); + let p = profile.clone(); + let e = entry.clone(); move |is_reload| { - update_primary_icon(&entry, &profile); + update_primary_icon(&e, &p); if is_reload { - entry.emit_activate(); + e.emit_activate(); } } }),