From f7b2bbc432036d334b39cb3f7ee6cade00a895a8 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 06:51:54 +0200 Subject: [PATCH] begin ggemini 0.18 features integration, collect header data in page info holder --- Cargo.toml | 2 +- .../window/tab/item/client/driver/gemini.rs | 330 +++++++++--------- 2 files changed, 172 insertions(+), 160 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 666f03e6..9e4560a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ version = "0.9.1" [dependencies] ansi-parser = "0.9.1" anyhow = "1.0.97" -ggemini = "0.17.3" +ggemini = "0.18.0" ggemtext = "0.6.0" indexmap = "2.7.0" itertools = "0.14.0" 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 786e1bbd..1941c321 100644 --- a/src/app/browser/window/tab/item/client/driver/gemini.rs +++ b/src/app/browser/window/tab/item/client/driver/gemini.rs @@ -1,6 +1,6 @@ use super::{Feature, Page}; use ggemini::client::connection::response::{ - Certificate, Failure, Input, Redirect, + Certificate, Failure, Input, Redirect, Success, failure::{Permanent, Temporary}, }; use ggemini::{ @@ -98,10 +98,13 @@ impl Gemini { cancellable: Cancellable, is_snap_history: bool, ) { - use ggemini::client::connection::Request; + use ggemini::client::connection::request::{Mode, Request}; match uri.scheme().as_str() { "gemini" => handle( - Request::Gemini { uri }, + Request::Gemini { + uri, + mode: Mode::Header, + }, ( self.client.clone(), self.page.clone(), @@ -124,6 +127,7 @@ impl Gemini { data: bytes, mime: header.mime.map(|mime| mime.into()), token: header.token.map(|token| token.into()), + mode: Mode::Header, }, ( client.clone(), @@ -301,152 +305,89 @@ fn handle( } redirects.replace(0); // reset }, - _ => match success.mime() { - "text/gemini" | "text/plain" => memory_input_stream::from_stream_async( - connection.stream(), - Priority::DEFAULT, - cancellable.clone(), - ( - 0x400, // 1024 chunk - 0xfffff, // 1M limit - ), - ( - |_, _| {}, // 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], - Priority::DEFAULT, - Some(&cancellable), - move |result| match result { - Ok((buffer, _ ,_)) => match std::str::from_utf8(&buffer) { - Ok(data) => { - let mut i = page.navigation.request.info.borrow_mut(); - i - .add_event("Parsing".to_string()) - .set_mime(Some(success.mime().to_string())) - .set_size(None, Some(data.len())); - let w = if matches!(*feature, Feature::Source) { - page.content.to_text_source(data) - } else { - match success.mime() { - "text/gemini" => page.content.to_text_gemini(&uri, data), - "text/plain" => page.content.to_text_plain(data), - _ => panic!() // unexpected - } - }; - 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); - page.window_action - .find - .simple_action - .set_enabled(true); - if is_snap_history { - page.snap_history(); - } - redirects.replace(0); // reset - i - .add_event(EVENT_COMPLETED.to_string()) - .commit(); - page.navigation.request.update_secondary_icon(&i) - }, - Err(e) => { - let s = page.content.to_status_failure(); - s.set_description(Some(&e.to_string())); - page.set_progress(0.0); - page.set_title(&s.title()); - if is_snap_history { - page.snap_history(); - } - redirects.replace(0); // reset - update_page_info(&page, EVENT_COMPLETED); - }, - }, - Err((_, e)) => { - let s = page.content.to_status_failure(); - s.set_description(Some(&e.to_string())); - page.set_progress(0.0); - page.set_title(&s.title()); - if is_snap_history { - page.snap_history(); - } - redirects.replace(0); // reset - update_page_info(&page, EVENT_COMPLETED); - } - } + _ => match success { + Success::Default(default) => match default.header.mime() { + Ok(mime) => match mime.as_str() { + "text/gemini" | "text/plain" => memory_input_stream::from_stream_async( + connection.stream(), + Priority::DEFAULT, + cancellable.clone(), + ( + 0x400, // 1024 chunk + 0xfffff, // 1M limit ), - Err(e) => { - let s = page.content.to_status_failure(); - s.set_description(Some(&e.to_string())); - page.set_progress(0.0); - page.set_title(&s.title()); - if is_snap_history { - page.snap_history(); - } - redirects.replace(0); // reset - update_page_info(&page, EVENT_COMPLETED); - }, - } - ) - ), - "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { - // Final image size unknown, show loading widget - let status = page.content.to_status_loading( - Some(Duration::from_secs(1)), // show if download time > 1 second - ); - - // Asynchronously read [IOStream](https://docs.gtk.org/gio/class.IOStream.html) - // to local [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) - // show bytes count in loading widget, validate max size for incoming data - // * no dependency of Gemini library here, feel free to use any other `IOStream` processor - memory_input_stream::from_stream_async( - connection.stream(), - Priority::DEFAULT, - cancellable.clone(), - ( - 0x400, // 1024 bytes per chunk, optional step for images download tracking - 0xA00000 // 10M bytes max to prevent memory overflow if server play with promises - ), - ( - move |_, total| status.set_description(Some(&format!("Download: {total} bytes"))), - { - move | result | match result { - Ok((memory_input_stream, _)) => { - Pixbuf::from_stream_async( - &memory_input_stream, - Some(&cancellable), - move |result| { - match result { - Ok(buffer) => { - page.set_title(&crate::tool::uri_to_title(&uri)); - page.content.to_image(&Texture::for_pixbuf(&buffer)); + ( + |_, _| {}, // 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], + Priority::DEFAULT, + Some(&cancellable), + { + let m = mime.clone(); + move |result| match result { + Ok((buffer, _ ,_)) => match std::str::from_utf8(&buffer) { + Ok(data) => { let mut i = page.navigation.request.info.borrow_mut(); + i + .add_event("Parsing".to_string()) + .set_mime(Some(mime)) + .set_size(Some(default.header.len()), Some(data.len())); + let w = if matches!(*feature, Feature::Source) { + page.content.to_text_source(data) + } else { + match m.as_str() { + "text/gemini" => page.content.to_text_gemini(&uri, data), + "text/plain" => page.content.to_text_plain(data), + _ => panic!() // unexpected + } + }; + 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); + page.window_action + .find + .simple_action + .set_enabled(true); + if is_snap_history { + page.snap_history(); + } + redirects.replace(0); // reset i .add_event(EVENT_COMPLETED.to_string()) - .set_mime(Some(success.mime().to_string())) - .set_size(None, Some(buffer.byte_length())) .commit(); page.navigation.request.update_secondary_icon(&i) - } + }, Err(e) => { let s = page.content.to_status_failure(); - s.set_description(Some(e.message())); + s.set_description(Some(&e.to_string())); + page.set_progress(0.0); page.set_title(&s.title()); + if is_snap_history { + page.snap_history(); + } + redirects.replace(0); // reset update_page_info(&page, EVENT_COMPLETED); + }, + }, + Err((_, e)) => { + let s = page.content.to_status_failure(); + s.set_description(Some(&e.to_string())); + page.set_progress(0.0); + page.set_title(&s.title()); + if is_snap_history { + page.snap_history(); } + redirects.replace(0); // reset + update_page_info(&page, EVENT_COMPLETED); } - page.set_progress(0.0); - if is_snap_history { - page.snap_history(); - } - redirects.replace(0); // reset - }, - ) - } + } + } + ), Err(e) => { let s = page.content.to_status_failure(); s.set_description(Some(&e.to_string())); @@ -457,31 +398,102 @@ fn handle( } redirects.replace(0); // reset update_page_info(&page, EVENT_COMPLETED); - } + }, } - } + ) ), - ) + "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { + // Final image size unknown, show loading widget + let status = page.content.to_status_loading( + Some(Duration::from_secs(1)), // show if download time > 1 second + ); + + // Asynchronously read [IOStream](https://docs.gtk.org/gio/class.IOStream.html) + // to local [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) + // show bytes count in loading widget, validate max size for incoming data + // * no dependency of Gemini library here, feel free to use any other `IOStream` processor + memory_input_stream::from_stream_async( + connection.stream(), + Priority::DEFAULT, + cancellable.clone(), + ( + 0x400, // 1024 bytes per chunk, optional step for images download tracking + 0xA00000 // 10M bytes max to prevent memory overflow if server play with promises + ), + ( + move |_, total| status.set_description(Some(&format!("Download: {total} bytes"))), + { + move | result | match result { + Ok((memory_input_stream, _)) => { + Pixbuf::from_stream_async( + &memory_input_stream, + Some(&cancellable), + move |result| { + match result { + Ok(buffer) => { + page.set_title(&crate::tool::uri_to_title(&uri)); + page.content.to_image(&Texture::for_pixbuf(&buffer)); + let mut i = page.navigation.request.info.borrow_mut(); + i + .add_event(EVENT_COMPLETED.to_string()) + .set_mime(Some(mime)) + .set_size(None, Some(buffer.byte_length())) + .commit(); + page.navigation.request.update_secondary_icon(&i) + } + Err(e) => { + let s = page.content.to_status_failure(); + s.set_description(Some(e.message())); + page.set_title(&s.title()); + update_page_info(&page, EVENT_COMPLETED); + } + } + page.set_progress(0.0); + if is_snap_history { + page.snap_history(); + } + redirects.replace(0); // reset + }, + ) + } + Err(e) => { + let s = page.content.to_status_failure(); + s.set_description(Some(&e.to_string())); + page.set_progress(0.0); + page.set_title(&s.title()); + if is_snap_history { + page.snap_history(); + } + redirects.replace(0); // reset + update_page_info(&page, EVENT_COMPLETED); + } + } + } + ), + ) + } + mime => { + let s = page + .content + .to_status_mime(mime, Some((&page.item_action, &uri))); + s.set_description(Some(&format!("Content type `{mime}` yet not supported"))); + page.set_progress(0.0); + page.set_title(&s.title()); + if is_snap_history { + page.snap_history(); + } + redirects.replace(0); // reset + let mut i = page.navigation.request.info.borrow_mut(); + i + .add_event(EVENT_COMPLETED.to_string()) + .set_mime(Some(mime.to_string())) + .unset_size() + .commit(); + page.navigation.request.update_secondary_icon(&i) + }, + }, + Err(_) => todo!() } - mime => { - let s = page - .content - .to_status_mime(mime, Some((&page.item_action, &uri))); - s.set_description(Some(&format!("Content type `{mime}` yet not supported"))); - page.set_progress(0.0); - page.set_title(&s.title()); - if is_snap_history { - page.snap_history(); - } - redirects.replace(0); // reset - let mut i = page.navigation.request.info.borrow_mut(); - i - .add_event(EVENT_COMPLETED.to_string()) - .set_mime(Some(mime.to_string())) - .unset_size() - .commit(); - page.navigation.request.update_secondary_icon(&i) - }, } }, // https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection