asuka/src/main.rs

239 lines
6.9 KiB
Rust

#[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::<Dialog>("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::<Dialog>("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::<SelectView>("main").unwrap();
let mut container = s.find_id::<Dialog>("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<url::Url, url::ParseError> {
// 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))
}
}
}
}