|
|
|
|
@ -1,8 +1,9 @@
|
|
|
|
|
use cursive::align::HAlign; |
|
|
|
|
use cursive::event::EventResult; |
|
|
|
|
use cursive::traits::*; |
|
|
|
|
use cursive::view::Scrollable; |
|
|
|
|
use cursive::views::{Dialog, EditView, OnEventView, Panel, SelectView, TextView}; |
|
|
|
|
use cursive::views::{Dialog, EditView, Panel, SelectView}; |
|
|
|
|
use cursive::utils::markup::StyledString; |
|
|
|
|
use cursive::theme::Effect; |
|
|
|
|
use cursive::Cursive; |
|
|
|
|
|
|
|
|
|
extern crate native_tls; |
|
|
|
|
@ -20,25 +21,63 @@ use std::str;
|
|
|
|
|
use std::str::FromStr; |
|
|
|
|
use url::Url; |
|
|
|
|
|
|
|
|
|
struct Status { |
|
|
|
|
#[derive(Debug)] |
|
|
|
|
struct Header { |
|
|
|
|
code: i8, |
|
|
|
|
meta: String, |
|
|
|
|
meta: String |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl FromStr for Status { |
|
|
|
|
#[derive(Debug)] |
|
|
|
|
struct Link { |
|
|
|
|
url: String, |
|
|
|
|
label: String |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl FromStr for Header { |
|
|
|
|
type Err = std::string::ParseError; |
|
|
|
|
|
|
|
|
|
// Parses a string into an instance of 'Header'
|
|
|
|
|
fn from_str(line: &str) -> Result<Self, Self::Err> { |
|
|
|
|
let text = format!("{}", line); |
|
|
|
|
let link_regexp = Regex::new(r"^(\d*)\t(.*)$").unwrap(); |
|
|
|
|
|
|
|
|
|
let caps = link_regexp.captures(&text).unwrap(); |
|
|
|
|
let code_str = caps.get(1).map_or("", |m| m.as_str()); |
|
|
|
|
let meta_str = caps.get(2).map_or("", |m| m.as_str()); |
|
|
|
|
|
|
|
|
|
let code = code_str.parse::<i8>().unwrap(); |
|
|
|
|
let meta = meta_str.to_string(); |
|
|
|
|
|
|
|
|
|
Ok(Header {code, meta}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl FromStr for Link { |
|
|
|
|
type Err = std::string::ParseError; |
|
|
|
|
|
|
|
|
|
// Parses a string into an instance of 'Status'
|
|
|
|
|
fn from_str(status_line: &str) -> Result<Self, Self::Err> { |
|
|
|
|
let mut splits = status_line.split("\t"); |
|
|
|
|
let code: i8 = splits.nth(1).unwrap().parse().unwrap(); |
|
|
|
|
let meta: String = String::from(splits.nth(2).unwrap()); |
|
|
|
|
// Parses a string into an instance of 'Link'
|
|
|
|
|
fn from_str(line: &str) -> Result<Self, Self::Err> { |
|
|
|
|
let text = format!("{}", line); |
|
|
|
|
let link_regexp = Regex::new(r"^=>\s(\S*)\s*(.*)?$").unwrap(); |
|
|
|
|
|
|
|
|
|
let caps = link_regexp.captures(&text).unwrap(); |
|
|
|
|
let url_str = caps.get(1).map_or("", |m| m.as_str()); |
|
|
|
|
let label_str = caps.get(2).map_or("", |m| m.as_str()); |
|
|
|
|
|
|
|
|
|
let url = url_str.to_string(); |
|
|
|
|
let label = if label_str.is_empty() { |
|
|
|
|
url_str.to_string() |
|
|
|
|
} else { |
|
|
|
|
label_str.to_string() |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
Ok(Status {code, meta}) |
|
|
|
|
Ok(Link {url, label}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const HELP: &'static str = " |
|
|
|
|
Welcome to Asuka Gemini browser! |
|
|
|
|
|
|
|
|
|
Press g to visit an URL |
|
|
|
|
Press b to go back |
|
|
|
|
Press q to exit |
|
|
|
|
@ -54,16 +93,6 @@ fn main() {
|
|
|
|
|
select.add_all_str(HELP.lines()); |
|
|
|
|
select.set_on_submit(follow_link); |
|
|
|
|
|
|
|
|
|
// let select_view = OnEventView::new(select)
|
|
|
|
|
// .on_pre_event_inner('k', |s, _| {
|
|
|
|
|
// s.select_up(1);
|
|
|
|
|
// Some(EventResult::Consumed(None))
|
|
|
|
|
// })
|
|
|
|
|
// .on_pre_event_inner('j', |s, _| {
|
|
|
|
|
// s.select_down(1);
|
|
|
|
|
// Some(EventResult::Consumed(None))
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
siv.add_fullscreen_layer( |
|
|
|
|
Dialog::around(Panel::new( |
|
|
|
|
select.with_id("main").scrollable().full_screen() |
|
|
|
|
@ -74,11 +103,6 @@ fn main() {
|
|
|
|
|
.with_id("container") |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Show a welcome popup
|
|
|
|
|
// siv.add_layer(Dialog::info(
|
|
|
|
|
// "Welcome to Asuka, the first ncurses Gemini browser!\n",
|
|
|
|
|
// ));
|
|
|
|
|
|
|
|
|
|
// We can quit by pressing q
|
|
|
|
|
siv.add_global_callback('q', |s| s.quit()); |
|
|
|
|
// pressing g prompt for an URL
|
|
|
|
|
@ -97,12 +121,23 @@ fn prompt_for_url(s: &mut Cursive) {
|
|
|
|
|
.padding((1, 1, 1, 0)) |
|
|
|
|
.content( |
|
|
|
|
EditView::new() |
|
|
|
|
.on_submit(visit_url) |
|
|
|
|
.on_submit(goto_url) |
|
|
|
|
.fixed_width(20) |
|
|
|
|
).with_id("url_popup") |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn goto_url(s: &mut Cursive, url: &str) { |
|
|
|
|
// Prepend gemini scheme if needed
|
|
|
|
|
let url_s = if url.starts_with("gemini://") { |
|
|
|
|
url.to_owned() |
|
|
|
|
} else { |
|
|
|
|
format!("gemini://{}", url) |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
visit_url(s, &url_s) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn visit_url(s: &mut Cursive, url_s: &str) { |
|
|
|
|
// Close URL popup if any
|
|
|
|
|
if s.find_id::<Dialog>("url_popup").is_some() { |
|
|
|
|
@ -113,13 +148,7 @@ fn visit_url(s: &mut Cursive, url_s: &str) {
|
|
|
|
|
Ok(url) => { |
|
|
|
|
match get_data(url) { |
|
|
|
|
Ok(new_content) => { |
|
|
|
|
let mut main_view = s.find_id::<SelectView>("main").unwrap(); |
|
|
|
|
let mut container = s.find_id::<Dialog>("container").unwrap(); |
|
|
|
|
|
|
|
|
|
container.set_title(url_s); |
|
|
|
|
main_view.clear(); |
|
|
|
|
main_view.add_all_str(new_content.lines()); |
|
|
|
|
main_view.set_on_submit(follow_link); |
|
|
|
|
draw_content(s, url_s, new_content); |
|
|
|
|
} |
|
|
|
|
Err(msg) => { |
|
|
|
|
s.add_layer(Dialog::info(msg)); |
|
|
|
|
@ -132,21 +161,8 @@ fn visit_url(s: &mut Cursive, url_s: &str) {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn follow_link(s: &mut Cursive, line: &str) { |
|
|
|
|
let text = format!("{}", line); |
|
|
|
|
let link_regexp = Regex::new(r"^=>\s(\S*)(.*)?$").unwrap(); |
|
|
|
|
|
|
|
|
|
if link_regexp.is_match(&text) { |
|
|
|
|
let caps = link_regexp.captures(&text).unwrap(); |
|
|
|
|
let url = caps.get(1).map_or("", |m| m.as_str()); |
|
|
|
|
let next_url = parse_link(url); |
|
|
|
|
visit_url(s, next_url.expect("Not an URL").as_str()) |
|
|
|
|
} else { |
|
|
|
|
() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn parse_link(url: &str) -> Result<url::Url, url::ParseError> { |
|
|
|
|
// Creates an absolute link if needed
|
|
|
|
|
match get_last_host() { |
|
|
|
|
Some(host) => { |
|
|
|
|
let url_s = if url.starts_with("gemini://") { |
|
|
|
|
@ -171,6 +187,60 @@ fn parse_link(url: &str) -> Result<url::Url, url::ParseError> {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn draw_content(s: &mut Cursive, url: &str, content: String) { |
|
|
|
|
let mut main_view = s.find_id::<SelectView>("main").unwrap(); |
|
|
|
|
let mut container = s.find_id::<Dialog>("container").unwrap(); |
|
|
|
|
|
|
|
|
|
container.set_title(url); |
|
|
|
|
main_view.clear(); |
|
|
|
|
|
|
|
|
|
for line in content.lines() { |
|
|
|
|
if is_header(line) { |
|
|
|
|
// let _header = Header::from_str(line);
|
|
|
|
|
} else if is_link(line) { |
|
|
|
|
let link = Link::from_str(line).unwrap(); |
|
|
|
|
let mut formatted = StyledString::new(); |
|
|
|
|
formatted.append(StyledString::styled( |
|
|
|
|
link.label, |
|
|
|
|
Effect::Underline, |
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
main_view.add_item(formatted, link.url) |
|
|
|
|
} else { |
|
|
|
|
main_view.add_item(line, "0".to_owned()) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
main_view.set_on_submit(follow_link); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn is_header(line: &str) -> bool { |
|
|
|
|
let text = format!("{}", line); |
|
|
|
|
let header_regexp = Regex::new(r"^(\d*)\t(\S*)$").unwrap(); |
|
|
|
|
|
|
|
|
|
header_regexp.is_match(&text) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn is_link(line: &str) -> bool { |
|
|
|
|
let text = format!("{}", line); |
|
|
|
|
let link_regexp = Regex::new(r"^=>\s(\S*)(.*)?$").unwrap(); |
|
|
|
|
|
|
|
|
|
link_regexp.is_match(&text) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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 = parse_link(&line); |
|
|
|
|
visit_url(s, next_url.expect("Not an URL").as_str()) |
|
|
|
|
} else { |
|
|
|
|
() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn get_data(url: url::Url) -> Result<String, String> { |
|
|
|
|
let host = url.host_str().unwrap(); |
|
|
|
|
let path = url.path(); |
|
|
|
|
@ -237,6 +307,7 @@ fn get_previous_url() -> Option<String> {
|
|
|
|
|
let lines_count = content.lines().count(); |
|
|
|
|
|
|
|
|
|
if lines_count > 1 { |
|
|
|
|
// Return before last line
|
|
|
|
|
Some(content.lines().nth(lines_count - 2).unwrap().to_owned()) |
|
|
|
|
} else { |
|
|
|
|
None |
|
|
|
|
|