mirror of
https://github.com/YGGverse/Yoda.git
synced 2025-08-31 08:52:14 +00:00
implement nex protocol driver
This commit is contained in:
parent
aafd3b5db3
commit
02eb8e4a71
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "Yoda"
|
||||
version = "0.11.6"
|
||||
version = "0.11.7"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
|
@ -113,7 +113,7 @@ GTK 4 / Libadwaita client written in Rust
|
||||
* [x] Header options
|
||||
* [x] MIME
|
||||
* [x] Token
|
||||
* [ ] [NEX](https://nightfall.city/nex/info/specification.txt) - useful for networks with build-in encryption (e.g. [Yggdrasil](https://yggdrasil-network.github.io))
|
||||
* [x] [NEX](https://nightfall.city/nex/info/specification.txt) - useful for networks with build-in encryption (e.g. [Yggdrasil](https://yggdrasil-network.github.io))
|
||||
* [ ] [NPS](https://nightfall.city/nps/info/specification.txt)
|
||||
* [x] System
|
||||
* [x] `file://` - local files browser
|
||||
@ -130,6 +130,7 @@ GTK 4 / Libadwaita client written in Rust
|
||||
#### Text
|
||||
* [x] `text/gemini`
|
||||
* [x] `text/plain`
|
||||
* [ ] `text/nex`
|
||||
|
||||
#### Images
|
||||
* [x] `image/gif`
|
||||
|
@ -75,6 +75,9 @@ impl Client {
|
||||
.gemini
|
||||
.handle(uri, feature, cancellable, is_snap_history)
|
||||
}
|
||||
"nex" => driver
|
||||
.nex
|
||||
.handle(uri, feature, cancellable, is_snap_history),
|
||||
scheme => {
|
||||
// no scheme match driver, complete with failure message
|
||||
let status = page.content.to_status_failure();
|
||||
|
@ -1,15 +1,18 @@
|
||||
mod file;
|
||||
mod gemini;
|
||||
mod nex;
|
||||
|
||||
use super::{Feature, Page};
|
||||
use file::File;
|
||||
use gemini::Gemini;
|
||||
use nex::Nex;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Different protocols implementation
|
||||
pub struct Driver {
|
||||
pub file: File,
|
||||
pub gemini: Gemini,
|
||||
pub nex: Nex,
|
||||
}
|
||||
|
||||
impl Driver {
|
||||
@ -20,6 +23,7 @@ impl Driver {
|
||||
Driver {
|
||||
file: File::init(page),
|
||||
gemini: Gemini::init(page),
|
||||
nex: Nex::init(page),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
283
src/app/browser/window/tab/item/client/driver/nex.rs
Normal file
283
src/app/browser/window/tab/item/client/driver/nex.rs
Normal file
@ -0,0 +1,283 @@
|
||||
//! https://nightfall.city/nex/info/specification.txt
|
||||
|
||||
use super::{Feature, Page};
|
||||
use gtk::gio::MemoryInputStream;
|
||||
use gtk::prelude::{
|
||||
Cast, IOStreamExt, InputStreamExtManual, OutputStreamExtManual, SocketClientExt,
|
||||
};
|
||||
use gtk::{
|
||||
gdk::Texture,
|
||||
gdk_pixbuf::Pixbuf,
|
||||
gio::{Cancellable, IOStream, SocketClient, SocketClientEvent, SocketProtocol},
|
||||
glib::{Priority, Uri},
|
||||
};
|
||||
use sourceview::prelude::FileExt;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
pub struct Nex {
|
||||
page: Rc<Page>,
|
||||
}
|
||||
|
||||
impl Nex {
|
||||
pub fn init(page: &Rc<Page>) -> Self {
|
||||
Self { page: page.clone() }
|
||||
}
|
||||
|
||||
pub fn handle(
|
||||
&self,
|
||||
uri: Uri,
|
||||
feature: Rc<Feature>,
|
||||
cancellable: Cancellable,
|
||||
is_snap_history: bool,
|
||||
) {
|
||||
if is_snap_history {
|
||||
self.page.snap_history();
|
||||
}
|
||||
|
||||
{
|
||||
self.page
|
||||
.navigation
|
||||
.request
|
||||
.info
|
||||
.borrow_mut()
|
||||
.set_request(Some(uri.to_string()));
|
||||
}
|
||||
|
||||
let socket = SocketClient::new();
|
||||
socket.set_protocol(SocketProtocol::Tcp);
|
||||
socket.set_timeout(30); // @TODO optional
|
||||
|
||||
socket.connect_event({
|
||||
let p = self.page.clone();
|
||||
move |_, e, _, _| {
|
||||
let mut i = p.navigation.request.info.borrow_mut();
|
||||
p.set_progress(match e {
|
||||
// 0.1 reserved for handle begin
|
||||
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
|
||||
}
|
||||
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("Receiving".to_string());
|
||||
1.0
|
||||
}
|
||||
_ => panic!(),
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
socket.connect_to_uri_async(&uri.to_string(), 1900, Some(&cancellable.clone()), {
|
||||
let p = self.page.clone();
|
||||
move |result| match result {
|
||||
Ok(c) => {
|
||||
c.output_stream().write_all_async(
|
||||
format!("{}\r\n", uri.path()),
|
||||
Priority::DEFAULT,
|
||||
Some(&cancellable.clone()),
|
||||
move |r| match r {
|
||||
Ok(_) => {
|
||||
// Is download feature request
|
||||
if matches!(*feature, Feature::Download) {
|
||||
return download(c.upcast::<IOStream>(), (p, uri), cancellable);
|
||||
}
|
||||
|
||||
// Handle renderable types..
|
||||
// Show loading status if handle time > 1 second
|
||||
let status =
|
||||
p.content.to_status_loading(Some(Duration::from_secs(1)));
|
||||
|
||||
// Nex is the header-less protocol, final content size is never known,
|
||||
// borrow ggemini::gio wrapper api to preload it safely by chunks
|
||||
ggemini::gio::memory_input_stream::from_stream_async(
|
||||
c.upcast::<IOStream>(),
|
||||
Priority::DEFAULT,
|
||||
cancellable.clone(),
|
||||
ggemini::gio::memory_input_stream::Size {
|
||||
chunk: 0x400, // 1024 bytes chunk
|
||||
limit: 0xA00000, // 10M limit
|
||||
total: 0, // initial totals
|
||||
},
|
||||
(
|
||||
move |_, t| {
|
||||
status.set_description(Some(&format!(
|
||||
"Preload: {t} bytes"
|
||||
)))
|
||||
},
|
||||
move |r| match r {
|
||||
Ok((m, s)) => {
|
||||
render((m, s), (p, feature, uri), cancellable)
|
||||
}
|
||||
Err(e) => failure(&p, &e.to_string()),
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
Err((_, e)) => failure(&p, &e.to_string()),
|
||||
},
|
||||
)
|
||||
}
|
||||
Err(e) => failure(&p, &e.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn event(p: &Page, e: &str, s: Option<usize>) {
|
||||
let mut i = p.navigation.request.info.borrow_mut();
|
||||
i.add_event(e.to_string()).set_size(s);
|
||||
p.navigation.request.update_secondary_icon(&i)
|
||||
}
|
||||
|
||||
fn failure(p: &Page, d: &str) {
|
||||
let s = p.content.to_status_failure();
|
||||
s.set_description(Some(d));
|
||||
p.set_progress(0.0);
|
||||
p.set_title(&s.title())
|
||||
}
|
||||
|
||||
fn render(
|
||||
(m, s): (MemoryInputStream, usize),
|
||||
(p, f, u): (Rc<Page>, Rc<Feature>, Uri),
|
||||
c: Cancellable,
|
||||
) {
|
||||
use crate::tool::uri_to_title;
|
||||
let q = u.to_string();
|
||||
if q.ends_with(".gif")
|
||||
|| q.ends_with(".jpeg")
|
||||
|| q.ends_with(".jpg")
|
||||
|| q.ends_with(".png")
|
||||
|| q.ends_with(".webp")
|
||||
{
|
||||
p.window_action.find.simple_action.set_enabled(false);
|
||||
Pixbuf::from_stream_async(&m, Some(&c), move |r| match r {
|
||||
Ok(b) => {
|
||||
p.set_title(&uri_to_title(&u));
|
||||
p.content.to_image(&Texture::for_pixbuf(&b));
|
||||
p.set_progress(0.0);
|
||||
event(&p, "Completed", Some(s))
|
||||
}
|
||||
Err(e) => failure(&p, &e.to_string()),
|
||||
})
|
||||
} else {
|
||||
p.window_action.find.simple_action.set_enabled(true);
|
||||
match *f {
|
||||
Feature::Default | Feature::Source => {
|
||||
m.read_all_async(vec![0; s], Priority::DEFAULT, Some(&c), move |r| match r {
|
||||
Ok((b, s, ..)) => match std::str::from_utf8(&b) {
|
||||
Ok(d) => {
|
||||
let t = if matches!(*f, Feature::Source) {
|
||||
p.content.to_text_source(d)
|
||||
} else if q.ends_with(".gmi") || q.ends_with(".gemini") {
|
||||
p.content.to_text_gemini(&u, d)
|
||||
} else {
|
||||
p.content.to_text_plain(d)
|
||||
};
|
||||
event(&p, "Parsed", Some(s));
|
||||
p.search.set(Some(t.text_view));
|
||||
p.set_title(&match t.meta.title {
|
||||
Some(t) => t.into(), // @TODO
|
||||
None => uri_to_title(&u),
|
||||
});
|
||||
p.set_progress(0.0);
|
||||
event(&p, "Completed", Some(s))
|
||||
}
|
||||
Err(e) => failure(&p, &e.to_string()),
|
||||
},
|
||||
Err((_, e)) => failure(&p, &e.to_string()),
|
||||
})
|
||||
}
|
||||
Feature::Download => panic!(), // unexpected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn download(s: IOStream, (p, u): (Rc<Page>, Uri), c: Cancellable) {
|
||||
use crate::tool::Format;
|
||||
use ggemini::gio::file_output_stream;
|
||||
event(&p, "Download begin", None);
|
||||
let t = crate::tool::uri_to_title(&u)
|
||||
.trim_matches(std::path::MAIN_SEPARATOR)
|
||||
.to_string();
|
||||
p.content.to_status_download(&t, &c, {
|
||||
let c = c.clone();
|
||||
let p = p.clone();
|
||||
let t = t.clone();
|
||||
move |f, a| match f.replace(None, false, gtk::gio::FileCreateFlags::NONE, Some(&c)) {
|
||||
Ok(file_output_stream) => {
|
||||
file_output_stream::from_stream_async(
|
||||
s.clone(),
|
||||
file_output_stream,
|
||||
c.clone(),
|
||||
Priority::DEFAULT,
|
||||
file_output_stream::Size {
|
||||
chunk: 0x100000, // 1M bytes per chunk
|
||||
limit: None, // unlimited
|
||||
total: 0, // initial totals
|
||||
},
|
||||
(
|
||||
// on chunk
|
||||
{
|
||||
let a = a.clone();
|
||||
let p = p.clone();
|
||||
move |_, total| {
|
||||
const T: &str = "Received";
|
||||
let t = format!("{T} {}...", total.bytes());
|
||||
event(&p, T, Some(total));
|
||||
p.set_title(&t);
|
||||
a.update.activate(&t)
|
||||
}
|
||||
},
|
||||
// on complete
|
||||
{
|
||||
let a = a.clone();
|
||||
let p = p.clone();
|
||||
let t = t.clone();
|
||||
move |result| match result {
|
||||
Ok((_, total)) => {
|
||||
a.complete.activate(&format!(
|
||||
"Saved to {} ({} total)",
|
||||
f.parse_name(),
|
||||
total.bytes()
|
||||
));
|
||||
p.set_progress(0.0);
|
||||
p.set_title(&t);
|
||||
event(&p, "Completed", Some(total))
|
||||
}
|
||||
Err(e) => a.cancel.activate(&e.to_string()),
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
Err(e) => a.cancel.activate(&e.to_string()),
|
||||
}
|
||||
});
|
||||
}
|
@ -309,7 +309,10 @@ fn update_primary_icon(entry: &Entry, profile: &Profile) {
|
||||
entry.first_child().unwrap().remove_css_class("success"); // @TODO handle
|
||||
|
||||
match primary_icon::from(&entry.text()) {
|
||||
PrimaryIcon::Download { name, tooltip } | PrimaryIcon::File { name, tooltip } => {
|
||||
PrimaryIcon::Download { name, tooltip }
|
||||
| PrimaryIcon::File { name, tooltip }
|
||||
| PrimaryIcon::Source { name, tooltip }
|
||||
| PrimaryIcon::Nex { name, tooltip } => {
|
||||
entry.set_primary_icon_activatable(false);
|
||||
entry.set_primary_icon_sensitive(false);
|
||||
entry.set_primary_icon_name(Some(name));
|
||||
@ -332,12 +335,6 @@ fn update_primary_icon(entry: &Entry, profile: &Profile) {
|
||||
entry.set_primary_icon_name(Some(name));
|
||||
entry.set_primary_icon_tooltip_text(Some(tooltip));
|
||||
}
|
||||
PrimaryIcon::Source { name, tooltip } => {
|
||||
entry.set_primary_icon_activatable(false);
|
||||
entry.set_primary_icon_sensitive(false);
|
||||
entry.set_primary_icon_name(Some(name));
|
||||
entry.set_primary_icon_tooltip_text(Some(tooltip));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -415,10 +412,7 @@ fn prefix_less(entry: &Entry) -> GString {
|
||||
/// Try get current request value as [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
||||
/// * `strip_prefix` on parse
|
||||
fn uri(entry: &Entry) -> Option<Uri> {
|
||||
match Uri::parse(&prefix_less(entry), UriFlags::NONE) {
|
||||
Ok(uri) => Some(uri),
|
||||
_ => None,
|
||||
}
|
||||
Uri::parse(&prefix_less(entry), UriFlags::NONE).ok()
|
||||
}
|
||||
|
||||
/// Try build home [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `Self`
|
||||
|
@ -23,6 +23,10 @@ pub enum PrimaryIcon<'a> {
|
||||
name: &'a str,
|
||||
tooltip: (&'a str, &'a str),
|
||||
},
|
||||
Nex {
|
||||
name: &'a str,
|
||||
tooltip: &'a str,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn from(request: &str) -> PrimaryIcon {
|
||||
@ -49,20 +53,27 @@ pub fn from(request: &str) -> PrimaryIcon {
|
||||
};
|
||||
}
|
||||
|
||||
if prefix.starts_with("gemini:") {
|
||||
if prefix.starts_with("gemini://") {
|
||||
return PrimaryIcon::Gemini {
|
||||
name: "channel-secure-symbolic",
|
||||
tooltip: ("Guest session", "User session"),
|
||||
};
|
||||
}
|
||||
|
||||
if prefix.starts_with("titan:") {
|
||||
if prefix.starts_with("titan://") {
|
||||
return PrimaryIcon::Titan {
|
||||
name: "document-send-symbolic",
|
||||
tooltip: ("Guest titan input", "User titan input"),
|
||||
};
|
||||
}
|
||||
|
||||
if prefix.starts_with("nex://") {
|
||||
return PrimaryIcon::Nex {
|
||||
name: "network-server-symbolic",
|
||||
tooltip: "Nex protocol connection",
|
||||
};
|
||||
}
|
||||
|
||||
PrimaryIcon::Search {
|
||||
name: "system-search-symbolic",
|
||||
tooltip: "Choose default search provider",
|
||||
|
Loading…
x
Reference in New Issue
Block a user