use cursive::{ theme::{BaseColor, Color, Effect, Style}, traits::*, utils::markup::StyledString, views::{Dialog, EditView, ResizedView, SelectView, TextView}, Cursive, }; use maj::{self, Response}; use rustls::ClientConfig; use std::str; /// The state of the browser. #[derive(Clone)] pub struct State { url: Option, history: Vec, links: Vec<(url::Url, Option)>, cfg: ClientConfig, } impl Default for State { fn default() -> Self { State { url: None, history: vec![], links: vec![], cfg: crate::tls::config(), } } } pub fn history(siv: &mut Cursive) { let st = siv.user_data::().unwrap(); let mut select = SelectView::new().autojump(); for entry in st.history.iter() { select.add_item( format!("{}{}", entry.host_str().unwrap(), entry.path()), entry.to_string(), ); } select.set_on_submit(open); siv.add_layer( Dialog::around(select.scrollable()) .title("History") .button("Cancel", |s| { s.pop_layer(); }), ); } pub fn links(siv: &mut Cursive) { let st = siv.user_data::().unwrap(); let mut select = SelectView::new().autojump(); for link in st.links.iter() { select.add_item( match &link.1 { None => link.0.to_string(), Some(name) => format!("{}: {}", name, link.0.to_string()), }, link.0.to_string(), ); } select.set_on_submit(open); siv.add_layer( Dialog::around(select.scrollable()) .title("Links") .button("Cancel", |s| { s.pop_layer(); }), ); } pub fn open_prompt(siv: &mut Cursive) { siv.add_layer( Dialog::around( EditView::new() .on_submit(open) .with_name("url") .fixed_width(50), ) .title("Enter a Gemini URL") .button("Ok", |s| { let url = s .call_on_name("url", |view: &mut EditView| view.get_content()) .unwrap(); open(s, &url); }) .button("Cancel", |s| { s.pop_layer(); }), ); } pub fn open(siv: &mut Cursive, url: &str) { siv.pop_layer(); log::debug!("got URL: {}", url); let mut st = siv.user_data::().unwrap(); let mut url: String = url.to_string(); if !url.starts_with("gemini://") { url = format!("gemini://{}", url); } let url = url::Url::parse(url.as_str()).unwrap(); st.url = Some(url.clone()); match smol::run(maj::get(url.to_string(), st.cfg.clone())) { Ok(resp) => { st.history.push(url.clone()); show(siv, url.to_string().as_str(), resp); } Err(why) => { log::error!("got response error: {:?}", why); siv.add_layer(Dialog::info(format!("Error fetching response: {:?}", why))); } } } pub fn show(siv: &mut Cursive, url: &str, resp: Response) { use maj::StatusCode::*; match resp.status { Success => match str::from_utf8(&resp.body) { Ok(content) => { let content: StyledString = if resp.meta.starts_with("text/gemini") { populate_links(siv, content); render(content) } else { StyledString::plain(content) }; siv.add_fullscreen_layer(ResizedView::with_full_screen( Dialog::around(TextView::new(content).scrollable()) .title(format!("{}: {}", url, resp.meta)), )); } Err(why) => { siv.add_layer(Dialog::info(format!( "UTF/8 decoding error for {}: {:?}", url, why ))); } }, TemporaryRedirect => { let st = siv.user_data::().unwrap(); st.history.pop(); open(siv, resp.meta.as_str()); } PermanentRedirect => { let st = siv.user_data::().unwrap(); st.history.pop(); open(siv, resp.meta.as_str()); } Input => { siv.add_layer( Dialog::around(EditView::new().with_name("input").fixed_width(50)) .title(resp.meta) .button("Ok", |s| { let inp = s .call_on_name("input", |view: &mut EditView| view.get_content()) .unwrap(); let url = { let st = s.user_data::().unwrap(); let url = st.url.as_ref().unwrap(); let mut u = url.clone(); u.set_query(Some(&inp)); &u.to_string() }; open(s, url); }), ); } _ => { siv.add_layer(Dialog::info(format!("{:?}: {}", resp.status, resp.meta))); return; } } } pub fn populate_links(siv: &mut Cursive, body: &str) { use maj::gemini::Node::*; let doc = maj::gemini::parse(body); let st = siv.user_data::().unwrap(); st.links.clear(); let u = st.url.as_ref().unwrap(); for node in doc { match node { Link { to, name } => st.links.push((u.join(&to).unwrap(), name)), _ => {} } } } pub fn render(body: &str) -> StyledString { let doc = maj::gemini::parse(body); let mut styled = StyledString::new(); use maj::gemini::Node::*; for node in doc { match node { Text(line) => styled.append(StyledString::plain(line)), Link { to, name } => match name { None => styled.append(StyledString::styled(to, Style::from(Effect::Underline))), Some(name) => { styled.append(StyledString::styled(name, Style::from(Effect::Underline))) } }, Preformatted(data) => styled.append(StyledString::plain(data)), Heading { level, body } => styled.append(StyledString::styled( format!("{} {}", "#".repeat(level as usize), body), Style::from(Effect::Bold), )), ListItem(item) => styled.append(StyledString::plain(format!("* {}", item))), Quote(quote) => styled.append(StyledString::styled( format!("> {}", quote), Style::from(Color::Dark(BaseColor::Green)), )), } styled.append(StyledString::plain("\n")); } styled }