#[macro_use] extern crate lazy_static; extern crate native_tls; extern crate regex; use cursive::align::HAlign; use cursive::theme::Effect; use cursive::traits::*; use cursive::utils::markup::StyledString; use cursive::view::Scrollable; use cursive::views::{Dialog, EditView, Panel, SelectView}; use cursive::Cursive; use std::str::FromStr; use url::Url; mod status; use status::Status; mod link; use link::Link; mod content; mod history; const HELP: &str = "Welcome to Asuka Gemini browser! Press g to visit an URL Press h to show/hide history Press q to exit "; fn main() { history::init(); let mut siv = Cursive::default(); let mut select = SelectView::new(); select.add_all_str(HELP.lines()); select.set_on_submit(|s, link| { follow_link(s, link); }); siv.add_fullscreen_layer( Dialog::around(Panel::new( select.with_id("main").scrollable().full_screen(), )) .title("Asuka Browser") .h_align(HAlign::Center) .button("Quit", |s| s.quit()) .with_id("container"), ); // We can quit by pressing q siv.add_global_callback('q', |s| s.quit()); // pressing g prompt for an URL siv.add_global_callback('g', |s| prompt_for_url(s)); // pressing h shows/hides history siv.add_global_callback('h', |s| show_history(s)); siv.run(); } fn prompt_for_url(s: &mut Cursive) { s.add_layer( Dialog::new() .title("Enter URL") // Padding is (left, right, top, bottom) .padding((1, 1, 1, 0)) .content(EditView::new().on_submit(goto_url).fixed_width(20)) .with_id("url_popup"), ); } fn goto_url(s: &mut Cursive, url: &str) { // Prepend gemini scheme if needed if url.starts_with("gemini://") { visit_url(s, &Url::parse(url).unwrap()) } else { let url = format!("gemini://{}", url); visit_url(s, &Url::parse(&url).unwrap()) }; } fn show_history(s: &mut Cursive) { // Hide popup when pressing h on an opened popup if s.find_id::("history_popup").is_some() { s.pop_layer(); return; } let mut select = SelectView::new(); for url in history::content() { let url_s = url.as_str(); select.add_item_str(url_s); } select.set_on_submit(|s, link| { s.pop_layer(); follow_link(s, link); }); s.add_layer( Dialog::around(select.scrollable().fixed_size((50, 10))) .title("History") .with_id("history_popup"), ); } fn visit_url(s: &mut Cursive, url: &Url) { // Close URL popup if any if s.find_id::("url_popup").is_some() { s.pop_layer(); } match make_absolute(url.as_str()) { Ok(url) => match content::get_data(&url) { Ok(new_content) => { history::append(url.as_str()); draw_content(s, url, new_content); } Err(msg) => { s.add_layer(Dialog::info(msg)); } }, Err(_) => { s.add_layer(Dialog::info(format!("Could not parse {}", url.as_str()))); } } } fn draw_content(s: &mut Cursive, url: Url, content: String) { let mut main_view = s.find_id::("main").unwrap(); let mut container = s.find_id::("container").unwrap(); // handle response status if let Some(status_line) = content.lines().next() { if let Ok(status) = Status::from_str(status_line) { match status { Status::Success(_meta) => {} Status::Gone(_meta) => { s.add_layer(Dialog::info("Sorry page is gone.")); return; } Status::RedirectTemporary(new_url) | Status::RedirectPermanent(new_url) => { follow_link(s, &new_url) } Status::TransientCertificateRequired(_meta) | Status::AuthorisedCertificatedRequired(_meta) => { s.add_layer(Dialog::info( "You need a valid certificate to access this page.", )); return; } other_status => { s.add_layer(Dialog::info(format!("ERROR: {:?}", other_status))); return; } } } } // set title and clear old content container.set_title(url.as_str()); main_view.clear(); // draw new content lines for line in content.lines().skip(1) { match Link::from_str(line) { Ok(link) => match link { Link::Http(_url, label) => { let mut formatted = StyledString::new(); let www_label = format!("[WWW] {}", label); formatted.append(StyledString::styled(www_label, Effect::Italic)); main_view.add_item(formatted, String::from("0")) } Link::Gopher(_url, label) => { let mut formatted = StyledString::new(); let gopher_label = format!("[Gopher] {}", label); formatted.append(StyledString::styled(gopher_label, Effect::Italic)); main_view.add_item(formatted, String::from("0")) } Link::Gemini(url, label) => { let mut formatted = StyledString::new(); formatted.append(StyledString::styled(label, Effect::Underline)); main_view.add_item(formatted, url.to_string()) } Link::Relative(url, label) => { let mut formatted = StyledString::new(); formatted.append(StyledString::styled(label, Effect::Underline)); main_view.add_item(formatted, url.to_string()) } Link::Unknown(_, _) => (), }, Err(_) => main_view.add_item(str::replace(line, "\t", " "), String::from("0")), } } } fn is_gemini_link(line: &str) -> bool { line != "0" } fn follow_link(s: &mut Cursive, line: &str) { if is_gemini_link(line) { let next_url = make_absolute(line).expect("Not an URL"); visit_url(s, &next_url) } } fn make_absolute(url: &str) -> Result { // Creates an absolute link if needed match history::get_current_host() { Some(host) => { if url.starts_with("gemini://") { Url::parse(url) } else if url.starts_with('/') { Url::parse(&format!("gemini://{}{}", host, url)) } else { Url::parse(&format!("gemini://{}/{}", host, url)) } } None => { if url.starts_with("gemini://") { Url::parse(url) } else { Url::parse(&format!("gemini://{}", url)) } } } }