Skip to content

Commit

Permalink
implement titan header options
Browse files Browse the repository at this point in the history
  • Loading branch information
yggverse committed Feb 6, 2025
1 parent 6267691 commit f6fb73c
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 25 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ GTK 4 / Libadwaita client written in Rust
* [ ] [Titan](https://transjovian.org/titan/page/The%20Titan%20Specification)
* [ ] Binary data (file uploads)
* [x] Text input
* [ ] Custom headers
* [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))
* [ ] [NPS](https://nightfall.city/nps/info/specification.txt)
* [ ] System
Expand Down
10 changes: 4 additions & 6 deletions src/app/browser/window/tab/item/client/driver/gemini.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use ggemini::{
client::{Client, Request, Response},
gio::{file_output_stream, memory_input_stream},
};
use gtk::glib::Bytes;
use gtk::glib::GString;
use gtk::{
gdk::Texture,
Expand Down Expand Up @@ -85,14 +84,13 @@ impl Gemini {
let client = self.client.clone();
let page = self.page.clone();
let redirects = self.redirects.clone();
move |data, on_failure| {
move |header, bytes, on_failure| {
handle(
Request::Titan {
uri: uri.clone(),
data: Bytes::from(data),
// * some servers may reject the request without content type
mime: Some("text/plain".to_string()),
token: None, // @TODO
data: bytes,
mime: header.mime.map(|mime| mime.into()),
token: header.token.map(|token| token.into()),
},
client.clone(),
page.clone(),
Expand Down
4 changes: 2 additions & 2 deletions src/app/browser/window/tab/item/page/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod titan;
use super::ItemAction;
use adw::Clamp;
use gtk::{
glib::Uri,
glib::{Bytes, Uri},
prelude::{IsA, WidgetExt},
Widget,
};
Expand Down Expand Up @@ -74,7 +74,7 @@ impl Input {
self.update(Some(&gtk::Box::sensitive(action, base, title, max_length)));
}

pub fn set_new_titan(&self, on_send: impl Fn(&[u8], Box<dyn Fn()>) + 'static) {
pub fn set_new_titan(&self, on_send: impl Fn(titan::Header, Bytes, Box<dyn Fn()>) + 'static) {
self.update(Some(&gtk::Notebook::titan(on_send)));
}
}
22 changes: 17 additions & 5 deletions src/app/browser/window/tab/item/page/input/titan.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
mod file;
mod header;
mod text;
mod title;

use file::File;
use gtk::{glib::uuid_string_random, prelude::WidgetExt, Label, Notebook};
use gtk::{
glib::{uuid_string_random, Bytes},
Label, Notebook,
};
pub use header::Header;
use text::Text;
use title::Title;

pub trait Titan {
fn titan(callback: impl Fn(&[u8], Box<dyn Fn()>) + 'static) -> Self;
fn titan(callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static) -> Self;
}

impl Titan for Notebook {
fn titan(callback: impl Fn(&[u8], Box<dyn Fn()>) + 'static) -> Self {
fn titan(callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static) -> Self {
use gtk::Box;
use std::{cell::Cell, rc::Rc};

let notebook = Notebook::builder()
.name(format!("s{}", uuid_string_random()))
.show_border(false)
.build();

notebook.append_page(&gtk::Box::text(callback), Some(&Label::title("Text")));
notebook.append_page(&gtk::Box::file(), Some(&Label::title("File")));
let header = Rc::new(Cell::new(Header::new()));

notebook.append_page(&Box::text(&header, callback), Some(&Label::title("Text")));
notebook.append_page(&Box::file(), Some(&Label::title("File")));

notebook_css_patch(&notebook);
notebook
Expand All @@ -29,6 +39,8 @@ impl Titan for Notebook {
// Tools

fn notebook_css_patch(notebook: &Notebook) {
use gtk::prelude::WidgetExt;

let name = notebook.widget_name();
let provider = gtk::CssProvider::new();

Expand Down
75 changes: 75 additions & 0 deletions src/app/browser/window/tab/item/page/input/titan/header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
mod form;

use gtk::{glib::GString, prelude::IsA, Widget};

#[derive(Default)]
pub struct Header {
pub mime: Option<GString>,
pub token: Option<GString>,
}

impl Header {
pub fn new() -> Self {
Self {
mime: None,
token: None,
}
}

/// Show header options dialog for the referrer `widget`
/// * takes ownership of `Self`, return new updated copy in `callback` function
pub fn dialog(self, widget: Option<&impl IsA<Widget>>, callback: impl Fn(Self) + 'static) {
use adw::{
prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual},
AlertDialog, ResponseAppearance,
};
use form::Form;
use std::rc::Rc;

// Response variants
const RESPONSE_APPLY: (&str, &str) = ("apply", "Apply");
const RESPONSE_CANCEL: (&str, &str) = ("cancel", "Cancel");

// Init form components
let form = Rc::new(Form::build(
&self.mime.unwrap_or_default(),
&self.token.unwrap_or_default(),
));

// Init main widget
let alert_dialog = AlertDialog::builder()
.heading("Header")
.body("Custom header options")
.close_response(RESPONSE_CANCEL.0)
.default_response(RESPONSE_APPLY.0)
.extra_child(&form.g_box)
.build();

alert_dialog.add_responses(&[RESPONSE_CANCEL, RESPONSE_APPLY]);

// Decorate default response preset
alert_dialog.set_response_appearance(RESPONSE_APPLY.0, ResponseAppearance::Suggested);
/* contrast issue with Ubuntu orange accents
alert_dialog.set_response_appearance(RESPONSE_CANCEL.0, ResponseAppearance::Destructive); */

// Init events

alert_dialog.connect_response(None, {
let form = form.clone();
move |this, response| {
this.set_response_enabled(response, false); // prevent double-click
if response == RESPONSE_APPLY.0 {
callback(Self {
mime: form.mime(),
token: form.token(),
})
} else {
// @TODO restore
}
}
});

// Show
alert_dialog.present(widget);
}
}
56 changes: 56 additions & 0 deletions src/app/browser/window/tab/item/page/input/titan/header/form.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
mod mime;
mod token;

use mime::Mime;
use token::Token;

use gtk::{
glib::GString,
prelude::{BoxExt, EditableExt},
Box, Entry, Orientation,
};

pub struct Form {
pub g_box: Box,
mime: Entry,
token: Entry,
}

impl Form {
// Constructors

pub fn build(mime_value: &str, token_value: &str) -> Self {
// Init components
let mime = Entry::mime(mime_value);
let token = Entry::token(token_value);

// Init `Self`
let g_box = Box::builder().orientation(Orientation::Vertical).build();

g_box.append(&mime);
g_box.append(&token);

Self { g_box, mime, token }
}

// Getters

pub fn mime(&self) -> Option<GString> {
value(&self.mime)
}

pub fn token(&self) -> Option<GString> {
value(&self.token)
}
}

// Tools

fn value(label: &Entry) -> Option<GString> {
let text = label.text();
if !text.is_empty() {
Some(text)
} else {
None
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
pub trait Mime {
fn mime(text: &str) -> Self;
fn validate(&self);
}

impl Mime for gtk::Entry {
fn mime(text: &str) -> Self {
use gtk::prelude::EditableExt;

let mime = gtk::Entry::builder()
.placeholder_text("Content type (MIME)")
.margin_bottom(8)
.text(text)
.build();

mime.connect_changed(|this| {
this.validate();
});

mime
}

fn validate(&self) {
use gtk::prelude::{EditableExt, WidgetExt};

const CLASS: (&str, &str) = ("error", "success");

self.remove_css_class(CLASS.0);
self.remove_css_class(CLASS.1);

if !self.text().is_empty() {
if gtk::glib::Regex::match_simple(
r"^\w+/\w+$",
self.text(),
gtk::glib::RegexCompileFlags::DEFAULT,
gtk::glib::RegexMatchFlags::DEFAULT,
) {
self.add_css_class(CLASS.1)
} else {
self.add_css_class(CLASS.0)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use gtk::Entry;

pub trait Token {
fn token(text: &str) -> Self;
}

impl Token for Entry {
fn token(text: &str) -> Self {
Entry::builder()
.placeholder_text("Token")
.text(text)
.build()
}
}
41 changes: 31 additions & 10 deletions src/app/browser/window/tab/item/page/input/titan/text.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
mod control;
mod form;

use super::Header;
use control::Control;
use control::Upload;
use form::Form;
use gtk::glib::Bytes;
use gtk::{
prelude::{BoxExt, ButtonExt, TextBufferExt, TextViewExt},
Orientation, TextView,
};
use std::cell::Cell;
use std::rc::Rc;

const MARGIN: i32 = 8;
const SPACING: i32 = 8;

pub trait Text {
fn text(callback: impl Fn(&[u8], Box<dyn Fn()>) + 'static) -> Self;
fn text(
header: &Rc<Cell<Header>>,
callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static,
) -> Self;
}

impl Text for gtk::Box {
fn text(callback: impl Fn(&[u8], Box<dyn Fn()>) + 'static) -> Self {
fn text(
header: &Rc<Cell<Header>>,
callback: impl Fn(Header, Bytes, Box<dyn Fn()>) + 'static,
) -> Self {
// Init components
let control = Rc::new(Control::build());
let control = Rc::new(Control::build(header));
let form = TextView::form();

//header.take().dialog();

// Init widget
let g_box = gtk::Box::builder()
.margin_bottom(MARGIN)
Expand All @@ -37,12 +48,28 @@ impl Text for gtk::Box {
g_box.append(&control.g_box);

// Connect events

form.buffer().connect_changed({
let control = control.clone();
let form = form.clone();
move |this| control.update(this.char_count(), form.text().len())
});

control.upload.connect_clicked({
let form = form.clone();
let header = header.clone();
move |this| {
this.set_uploading();
let header = header.take();
callback(
form.text().as_bytes(),
Header {
mime: match header.mime {
Some(mime) => Some(mime),
None => Some("text/plain".into()), // server may reject the request without content type
},
token: header.token,
},
Bytes::from(form.text().as_bytes()),
Box::new({
let this = this.clone();
move || this.set_resend() // on failure
Expand All @@ -51,12 +78,6 @@ impl Text for gtk::Box {
}
});

form.buffer().connect_changed({
let control = control.clone();
let form = form.clone();
move |this| control.update(this.char_count(), form.text().len())
});

g_box
}
}
Loading

0 comments on commit f6fb73c

Please sign in to comment.