diff --git a/Cargo.toml b/Cargo.toml index 559d2be6..98497f02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,14 +15,6 @@ package = "libadwaita" version = "0.7.0" features = ["v1_5"] -[dependencies.gemini] -package = "ggemini" -version = "0.14.0" - -[dependencies.gemtext] -package = "ggemtext" -version = "0.3.0" - [dependencies.gtk] package = "gtk4" version = "0.9.4" @@ -38,6 +30,8 @@ version = "0.9.1" [dependencies] ansi-parser = "0.9.1" +ggemini = "0.14.0" +ggemtext = "0.3.0" indexmap = "2.7.0" itertools = "0.14.0" libspelling = "0.3.0" diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index 0e1ee53a..48f2f63b 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -349,7 +349,7 @@ impl Page { // 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 - gemini::gio::file_output_stream::move_all_from_stream_async( + ggemini::gio::file_output_stream::move_all_from_stream_async( stream.clone(), file_output_stream, cancellable.clone(), @@ -408,7 +408,7 @@ impl Page { // 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 - gemini::gio::memory_input_stream::from_stream_async( + ggemini::gio::memory_input_stream::from_stream_async( stream, cancellable.clone(), Priority::DEFAULT, diff --git a/src/app/browser/window/tab/item/page/client/driver.rs b/src/app/browser/window/tab/item/page/client/driver.rs index 3fb3e833..ea23d20d 100644 --- a/src/app/browser/window/tab/item/page/client/driver.rs +++ b/src/app/browser/window/tab/item/page/client/driver.rs @@ -1,6 +1,7 @@ //! At this moment, the `Driver` contain only one protocol library, //! by extending it features with new protocol, please make sub-module implementation +mod gemini; mod redirect; pub mod status; @@ -13,7 +14,6 @@ use super::{feature, response, Feature, Response}; use crate::{tool::now, Profile}; use gtk::{ gio::{Cancellable, SocketClientEvent}, - glib::{gformat, Priority, Uri}, prelude::SocketClientExt, }; use std::rc::Rc; @@ -24,14 +24,14 @@ pub struct Driver { /// Redirect resolver for different protocols redirect: Rc, /// Supported clients - gemini: gemini::Client, + gemini: ggemini::Client, // other clients here.. } impl Driver { pub fn init(profile: &Rc, callback: impl Fn(Status) + 'static) -> Self { // Init protocol driver libraries - let gemini = gemini::Client::new(); + let gemini = ggemini::Client::new(); // Translate driver status to `Status` @@ -71,7 +71,7 @@ impl Driver { match feature { Feature::Download { request } => match request { feature::Request::Gemini { uri } => { - request_gemini_async(self, uri.clone(), cancellable.clone(), move |result| { + gemini::request_async(self, uri.clone(), cancellable.clone(), move |result| { match result { Ok(response) => callback(Response::Download { base: uri.clone(), @@ -88,8 +88,8 @@ impl Driver { }, Feature::Default { request } => match request { feature::Request::Gemini { uri } => { - request_gemini_async(self, uri.clone(), cancellable.clone(), move |result| { - handle_gemini( + gemini::request_async(self, uri.clone(), cancellable.clone(), move |result| { + gemini::handle( result, uri.clone(), cancellable.clone(), @@ -103,8 +103,8 @@ impl Driver { }, Feature::Source { request } => match request { feature::Request::Gemini { uri } => { - request_gemini_async(self, uri.clone(), cancellable.clone(), move |result| { - handle_gemini( + gemini::request_async(self, uri.clone(), cancellable.clone(), move |result| { + gemini::handle( result, uri.clone(), cancellable.clone(), @@ -119,124 +119,3 @@ impl Driver { } } } - -/// Shared request interface for Gemini protocol -fn request_gemini_async( - driver: &Driver, - uri: Uri, - cancellable: Cancellable, - callback: impl Fn(Result) + 'static, -) { - driver.gemini.request_async( - gemini::client::Request::gemini(uri.clone()), - Priority::DEFAULT, - cancellable.clone(), - // Search for user certificate match request - // * @TODO this feature does not support multi-protocol yet - match driver.profile.identity.gemini.match_scope(&uri.to_string()) { - Some(identity) => match identity.to_tls_certificate() { - Ok(certificate) => Some(certificate), - Err(_) => todo!(), - }, - None => None, - }, - move |result| callback(result), - ) -} - -/// Shared handler for Gemini `Result` -/// * same implementation for Gemini and Titan protocols response -fn handle_gemini( - result: Result, - base: Uri, - cancellable: Cancellable, - is_source_request: bool, // @TODO yet partial implementation - callback: Rc, -) { - use gemini::client::connection::response::{data::Text, meta::Status}; - match result { - Ok(response) => match response.meta.status { - // https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected - Status::Input => callback(Response::Input(response::Input::Response { - base, - title: match response.meta.data { - Some(data) => data.value, - None => gformat!("Input expected"), - }, - })), - Status::SensitiveInput => callback(Response::Input(response::Input::Sensitive { - base, - title: match response.meta.data { - Some(data) => data.value, - None => gformat!("Input expected"), - }, - })), - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-20 - Status::Success => { - let mime = response.meta.mime.unwrap().value.to_lowercase(); - match mime.as_str() { - "text/gemini" => Text::from_stream_async( - response.connection.stream(), - Priority::DEFAULT, - cancellable, - move |result| match result { - Ok(text) => callback(Response::TextGemini { - base, - source: text.data, - is_source_request, - }), - Err(_) => todo!(), - }, - ), - "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { - callback(Response::Stream { - base, - mime, - stream: response.connection.stream(), - cancellable, - }) - } - mime => callback(Response::Failure(response::Failure::Mime { - message: format!("Undefined content type `{mime}`"), - })), - } // @TODO handle `None` - } - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Status::Redirect | Status::PermanentRedirect => todo!(), - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - Status::CertificateRequest => { - callback(Response::Certificate(response::Certificate::Request { - title: match response.meta.data { - Some(data) => data.value, - None => gformat!("Client certificate required"), - }, - })) - } - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - Status::CertificateUnauthorized => { - callback(Response::Certificate(response::Certificate::Request { - title: match response.meta.data { - Some(data) => data.value, - None => gformat!("Certificate not authorized"), - }, - })) - } - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - Status::CertificateInvalid => { - callback(Response::Certificate(response::Certificate::Request { - title: match response.meta.data { - Some(data) => data.value, - None => gformat!("Certificate not valid"), - }, - })) - } - status => callback(Response::Failure(response::Failure::Status { - message: format!("Undefined status code `{:?}`", status), // @TODO implement display trait for `ggemini` lib - })), - }, - Err(e) => callback(Response::Failure(response::Failure::Error { - message: e.to_string(), - })), - } -} diff --git a/src/app/browser/window/tab/item/page/client/driver/gemini.rs b/src/app/browser/window/tab/item/page/client/driver/gemini.rs new file mode 100644 index 00000000..06290a86 --- /dev/null +++ b/src/app/browser/window/tab/item/page/client/driver/gemini.rs @@ -0,0 +1,127 @@ +use super::{response, Driver, Response}; +use gtk::{ + gio::Cancellable, + glib::{gformat, Priority, Uri}, +}; +use std::rc::Rc; + +/// Shared request interface for Gemini protocol +pub fn request_async( + driver: &Driver, + uri: Uri, + cancellable: Cancellable, + callback: impl Fn(Result) + 'static, +) { + driver.gemini.request_async( + ggemini::client::Request::gemini(uri.clone()), + Priority::DEFAULT, + cancellable.clone(), + // Search for user certificate match request + // * @TODO this feature does not support multi-protocol yet + match driver.profile.identity.gemini.match_scope(&uri.to_string()) { + Some(identity) => match identity.to_tls_certificate() { + Ok(certificate) => Some(certificate), + Err(_) => todo!(), + }, + None => None, + }, + move |result| callback(result), + ) +} + +/// Shared handler for Gemini `Result` +/// * same implementation for Gemini and Titan protocols response +pub fn handle( + result: Result, + base: Uri, + cancellable: Cancellable, + is_source_request: bool, // @TODO yet partial implementation + callback: Rc, +) { + use ggemini::client::connection::response::{data::Text, meta::Status}; + match result { + Ok(response) => match response.meta.status { + // https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected + Status::Input => callback(Response::Input(response::Input::Response { + base, + title: match response.meta.data { + Some(data) => data.value, + None => gformat!("Input expected"), + }, + })), + Status::SensitiveInput => callback(Response::Input(response::Input::Sensitive { + base, + title: match response.meta.data { + Some(data) => data.value, + None => gformat!("Input expected"), + }, + })), + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-20 + Status::Success => { + let mime = response.meta.mime.unwrap().value.to_lowercase(); + match mime.as_str() { + "text/gemini" => Text::from_stream_async( + response.connection.stream(), + Priority::DEFAULT, + cancellable, + move |result| match result { + Ok(text) => callback(Response::TextGemini { + base, + source: text.data, + is_source_request, + }), + Err(_) => todo!(), + }, + ), + "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { + callback(Response::Stream { + base, + mime, + stream: response.connection.stream(), + cancellable, + }) + } + mime => callback(Response::Failure(response::Failure::Mime { + message: format!("Undefined content type `{mime}`"), + })), + } // @TODO handle `None` + } + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection + Status::Redirect | Status::PermanentRedirect => todo!(), + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 + Status::CertificateRequest => { + callback(Response::Certificate(response::Certificate::Request { + title: match response.meta.data { + Some(data) => data.value, + None => gformat!("Client certificate required"), + }, + })) + } + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized + Status::CertificateUnauthorized => { + callback(Response::Certificate(response::Certificate::Request { + title: match response.meta.data { + Some(data) => data.value, + None => gformat!("Certificate not authorized"), + }, + })) + } + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid + Status::CertificateInvalid => { + callback(Response::Certificate(response::Certificate::Request { + title: match response.meta.data { + Some(data) => data.value, + None => gformat!("Certificate not valid"), + }, + })) + } + status => callback(Response::Failure(response::Failure::Status { + message: format!("Undefined status code `{:?}`", status), // @TODO implement display trait for `ggemini` lib + })), + }, + Err(e) => callback(Response::Failure(response::Failure::Error { + message: e.to_string(), + })), + } +} diff --git a/src/app/browser/window/tab/item/page/content/text/gemini/reader.rs b/src/app/browser/window/tab/item/page/content/text/gemini/reader.rs index 9da102c1..a0717769 100644 --- a/src/app/browser/window/tab/item/page/content/text/gemini/reader.rs +++ b/src/app/browser/window/tab/item/page/content/text/gemini/reader.rs @@ -11,7 +11,7 @@ use widget::Widget; use super::{TabAction, WindowAction}; use crate::app::browser::window::action::Position; -use gemtext::line::{ +use ggemtext::line::{ code::{Inline, Multiline}, header::{Header, Level}, link::Link,