mirror of
https://github.com/YGGverse/Yoda.git
synced 2025-09-01 17:32:06 +00:00
implement nex protocol driver
This commit is contained in:
parent
aafd3b5db3
commit
02eb8e4a71
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "Yoda"
|
name = "Yoda"
|
||||||
version = "0.11.6"
|
version = "0.11.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -113,7 +113,7 @@ GTK 4 / Libadwaita client written in Rust
|
|||||||
* [x] Header options
|
* [x] Header options
|
||||||
* [x] MIME
|
* [x] MIME
|
||||||
* [x] Token
|
* [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)
|
* [ ] [NPS](https://nightfall.city/nps/info/specification.txt)
|
||||||
* [x] System
|
* [x] System
|
||||||
* [x] `file://` - local files browser
|
* [x] `file://` - local files browser
|
||||||
@ -130,6 +130,7 @@ GTK 4 / Libadwaita client written in Rust
|
|||||||
#### Text
|
#### Text
|
||||||
* [x] `text/gemini`
|
* [x] `text/gemini`
|
||||||
* [x] `text/plain`
|
* [x] `text/plain`
|
||||||
|
* [ ] `text/nex`
|
||||||
|
|
||||||
#### Images
|
#### Images
|
||||||
* [x] `image/gif`
|
* [x] `image/gif`
|
||||||
|
@ -75,6 +75,9 @@ impl Client {
|
|||||||
.gemini
|
.gemini
|
||||||
.handle(uri, feature, cancellable, is_snap_history)
|
.handle(uri, feature, cancellable, is_snap_history)
|
||||||
}
|
}
|
||||||
|
"nex" => driver
|
||||||
|
.nex
|
||||||
|
.handle(uri, feature, cancellable, is_snap_history),
|
||||||
scheme => {
|
scheme => {
|
||||||
// no scheme match driver, complete with failure message
|
// no scheme match driver, complete with failure message
|
||||||
let status = page.content.to_status_failure();
|
let status = page.content.to_status_failure();
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
mod file;
|
mod file;
|
||||||
mod gemini;
|
mod gemini;
|
||||||
|
mod nex;
|
||||||
|
|
||||||
use super::{Feature, Page};
|
use super::{Feature, Page};
|
||||||
use file::File;
|
use file::File;
|
||||||
use gemini::Gemini;
|
use gemini::Gemini;
|
||||||
|
use nex::Nex;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
/// Different protocols implementation
|
/// Different protocols implementation
|
||||||
pub struct Driver {
|
pub struct Driver {
|
||||||
pub file: File,
|
pub file: File,
|
||||||
pub gemini: Gemini,
|
pub gemini: Gemini,
|
||||||
|
pub nex: Nex,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Driver {
|
impl Driver {
|
||||||
@ -20,6 +23,7 @@ impl Driver {
|
|||||||
Driver {
|
Driver {
|
||||||
file: File::init(page),
|
file: File::init(page),
|
||||||
gemini: Gemini::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
|
entry.first_child().unwrap().remove_css_class("success"); // @TODO handle
|
||||||
|
|
||||||
match primary_icon::from(&entry.text()) {
|
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_activatable(false);
|
||||||
entry.set_primary_icon_sensitive(false);
|
entry.set_primary_icon_sensitive(false);
|
||||||
entry.set_primary_icon_name(Some(name));
|
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_name(Some(name));
|
||||||
entry.set_primary_icon_tooltip_text(Some(tooltip));
|
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)
|
/// Try get current request value as [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
||||||
/// * `strip_prefix` on parse
|
/// * `strip_prefix` on parse
|
||||||
fn uri(entry: &Entry) -> Option<Uri> {
|
fn uri(entry: &Entry) -> Option<Uri> {
|
||||||
match Uri::parse(&prefix_less(entry), UriFlags::NONE) {
|
Uri::parse(&prefix_less(entry), UriFlags::NONE).ok()
|
||||||
Ok(uri) => Some(uri),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try build home [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `Self`
|
/// 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,
|
name: &'a str,
|
||||||
tooltip: (&'a str, &'a str),
|
tooltip: (&'a str, &'a str),
|
||||||
},
|
},
|
||||||
|
Nex {
|
||||||
|
name: &'a str,
|
||||||
|
tooltip: &'a str,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from(request: &str) -> PrimaryIcon {
|
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 {
|
return PrimaryIcon::Gemini {
|
||||||
name: "channel-secure-symbolic",
|
name: "channel-secure-symbolic",
|
||||||
tooltip: ("Guest session", "User session"),
|
tooltip: ("Guest session", "User session"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if prefix.starts_with("titan:") {
|
if prefix.starts_with("titan://") {
|
||||||
return PrimaryIcon::Titan {
|
return PrimaryIcon::Titan {
|
||||||
name: "document-send-symbolic",
|
name: "document-send-symbolic",
|
||||||
tooltip: ("Guest titan input", "User titan input"),
|
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 {
|
PrimaryIcon::Search {
|
||||||
name: "system-search-symbolic",
|
name: "system-search-symbolic",
|
||||||
tooltip: "Choose default search provider",
|
tooltip: "Choose default search provider",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user