mod tls;
use egui::{RichText, TextBuffer};
use gemini::Url;
use poll_promise::Promise;
use std::sync::Arc;
use std::io::{self, Read, Write};
use std::net;
use std::thread;
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct GeminiApp {
url: Vec<String>,
url_bar: String,
#[serde(skip)]
reload: bool,
#[serde(skip)]
page: Option<Promise<io::Result<(gemini::response::Response, Option<gemini::Builder>)>>>,
#[serde(skip)]
tls_config: Arc<rustls::ClientConfig>,
}
impl Default for GeminiApp {
fn default() -> Self {
Self {
url: Vec::new(),
url_bar: "gemini://gem.hen6003.xyz".to_owned(),
reload: true,
page: None,
tls_config: Arc::new(tls::config()),
}
}
}
impl GeminiApp {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
if let Some(storage) = cc.storage {
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
}
Default::default()
}
}
impl eframe::App for GeminiApp {
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
let Self {
url,
url_bar,
reload,
page,
tls_config,
} = self;
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
egui::warn_if_debug_build(ui);
ui.menu_button("File", |ui| {
if ui.button("Quit").clicked() {
frame.quit();
}
});
});
ui.horizontal(|ui| {
let response = ui.text_edit_singleline(url_bar);
if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
*reload = true;
url.push(url_bar.to_owned());
}
})
});
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| {
if let Some(page) = page {
match page.ready() {
None => {
ui.spinner();
}
Some(Err(err)) => {
ui.colored_label(egui::Color32::RED, err.to_string());
}
Some(Ok((gemini, body))) => match gemini.status().category() {
gemini::Category::Redirect => {
url.push(gemini.header.meta().to_string());
*reload = true;
}
gemini::Category::Success => {
if let Some(body) = body {
for line in body.iter() {
match line {
gemini::Doc::Blank => ui.add_space(10.0),
gemini::Doc::Heading(level, string) => {
let text_size = match level {
gemini::Level::One => 30.0,
gemini::Level::Two => 27.0,
gemini::Level::Three => 24.0,
};
ui.label(
egui::RichText::new(string).size(text_size),
);
}
gemini::Doc::Link { to, name } => {
if ui
.button(name.as_ref().unwrap_or(to))
.clicked()
{
url.push(match Url::parse(to.as_str()) {
Ok(url) => url.to_string(),
Err(
url::ParseError::RelativeUrlWithoutBase,
) => {
let base = Url::parse(url.last().unwrap()).unwrap();
base.join(to).unwrap().to_string()
}
Err(e) => panic!("{}", e),
});
*reload = true;
}
}
gemini::Doc::ListItem(item) => {
ui.label(format!("• {}", item));
}
gemini::Doc::Preformatted { alt, text } => {
let text = ui.label(
egui::RichText::new(text)
.monospace()
.size(20.0),
);
if let Some(alt) = alt {
text.on_hover_text(alt);
}
}
gemini::Doc::Quote(quote) => {
ui.label(
RichText::new(quote).background_color(
egui::Color32::LIGHT_GRAY,
),
);
}
gemini::Doc::Text(text) => {
ui.label(text);
}
}
}
} else {
todo!();
}
}
gemini::Category::PermanentFailure
| gemini::Category::TemporaryFailure => {
ui.label(
egui::RichText::new(format!(
"Error {} - {}",
gemini.header.status.code_number(),
gemini.header.status.description()
))
.heading()
.color(egui::Color32::RED),
);
ui.colored_label(
egui::Color32::RED,
gemini.header.meta().as_str(),
);
}
_ => println!("{:?}", gemini),
},
}
}
});
});
if *reload {
*reload = false;
let new_url = url.last().unwrap().clone();
let new_url = match new_url.split_once("://") {
Some(_) => new_url,
None => "gemini://".to_string() + &new_url,
};
if let Ok(new_url) = Url::parse(&new_url) {
match new_url.scheme() {
"gemini" => (),
_ => {
// TODO: Mime launch
url.pop();
*reload = true;
return;
}
}
*url_bar = new_url.as_str().to_string();
let ctx = ctx.clone();
let tls_config = tls_config.clone();
let (sender, promise) = Promise::new();
*page = Some(promise);
thread::spawn(move || {
sender.send((|| -> io::Result<(gemini::response::Response, Option<gemini::Builder>)> {
let addr = new_url.socket_addrs(|| match new_url.scheme() {
"gemini" => Some(1965),
_ => None,
})?[0];
let mut conn = rustls::ClientConnection::new(
tls_config,
new_url.host_str()
.ok_or_else(|| io::Error::new(
io::ErrorKind::InvalidInput,
"No hostname",
))?
.try_into()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?,
)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let mut sock = net::TcpStream::connect(addr)?;
let mut tls = rustls::Stream::new(&mut conn, &mut sock);
tls.write_all((new_url.as_str().to_string() + "\r\n").as_bytes())?;
let mut response = Vec::new();
tls.read_to_end(&mut response)?;
let gemini = gemini::parse::parse_response(response)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
// TODO: cleanup
let body =
if let Some(mime_type) = gemini.header.mime_type() {
if mime_type.split(';').next().unwrap() == "text/gemini" {
if let Some(body) = gemini.body_text() {
Some(gemini::parse::parse_gemtext(body).map_err(|err| {
io::Error::new(io::ErrorKind::Other, err)
})?)
} else {
None
}
} else {
None
}
} else {
None
};
Ok((gemini, body))
})());
ctx.request_repaint();
});
}
}
/*
egui::SidePanel::left("side_panel").show(ctx, |ui| {
});
*/
}
}
fn main() {
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init();
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"eframe template",
native_options,
Box::new(|cc| Box::new(GeminiApp::new(cc))),
);
}