diff --git a/Cargo.lock b/Cargo.lock index c6f7894..bd28038 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,12 +20,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "asuka" -version = "0.5.0" +version = "0.6.0" dependencies = [ "cursive 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "json 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "open 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -109,7 +112,7 @@ dependencies = [ "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam-channel 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "enum-map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "enumset 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "enumset 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", @@ -187,16 +190,16 @@ dependencies = [ [[package]] name = "enumset" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "enumset_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "enumset_derive 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "enumset_derive" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "darling 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -253,6 +256,11 @@ dependencies = [ "unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "json" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -380,6 +388,14 @@ dependencies = [ "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "open" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "openssl" version = "0.10.24" @@ -777,8 +793,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum enum-map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bc0515b284e6ce2cbacd123b339d9c5a0ce49059baa4d9e584ab3803b3dc973" "checksum enum-map-derive 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e57001dfb2532f5a103ff869656887fae9a8defa7d236f3e39d2ee86ed629ad7" "checksum enum-map-internals 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8d7f81655c75281b36ddb9c2a1502afcac9db780859cc5b2eba08efcccb4c510" -"checksum enumset 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cac0a22e173f6570a7d69a2ab9e3fe79cf0dcdd0fdb162bfc932b97158f2b2a7" -"checksum enumset_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "01d93b926a992a4a526c2a14e2faf734fdef5bf9d0a52ba69a2ca7d4494c284b" +"checksum enumset 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4293261d4f3472132ffdeb1c97be5f5de5267c4a764c6cc10066aeff35a54c" +"checksum enumset_derive 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aeece157d0a6cda3f6015d7f16c570d4ba958161477448a9a6ec49851ccd8ee0" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" "checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" @@ -786,6 +802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e1de41fb8dba9714efd92241565cdff73f78508c95697dd56787d3cba27e2353" "checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +"checksum json 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3ca41abbeb7615d56322a984e63be5e5d0a117dfaca86c14393e32a762ccac1" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" @@ -801,6 +818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum num-iter 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "76bd5272412d173d6bf9afdf98db8612bbabc9a7a830b7bfc9c188911716132e" "checksum num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f2885278d5fe2adc2f75ced642d52d879bffaceb5a2e0b1d4309ffdfb239b454" "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" +"checksum open 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "94b424e1086328b0df10235c6ff47be63708071881bead9e76997d9291c0134b" "checksum openssl 0.10.24 (registry+https://github.com/rust-lang/crates.io-index)" = "8152bb5a9b5b721538462336e3bef9a539f892715e5037fda0f984577311af15" "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" "checksum openssl-sys 0.9.49 (registry+https://github.com/rust-lang/crates.io-index)" = "f4fad9e54bd23bd4cbbe48fdc08a1b8091707ac869ef8508edea2fec77dcc884" diff --git a/Cargo.toml b/Cargo.toml index 552214d..a6559dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "asuka" -version = "0.5.0" +version = "0.6.0" authors = ["Julien Blanchard "] edition = "2018" @@ -10,3 +10,6 @@ native-tls = "*" url = "*" regex = "*" lazy_static = "*" +open = "*" +json = "*" +tempfile = "*" diff --git a/src/absolute.rs b/src/absolute.rs new file mode 100644 index 0000000..7e5bfcf --- /dev/null +++ b/src/absolute.rs @@ -0,0 +1,89 @@ +use url::Url; + +pub fn make(url: &str) -> Result { + // Creates an absolute link if needed + match super::history::get_current_host() { + Some(host) => { + if url.starts_with("gemini://") { + Url::parse(url) + } else if url.starts_with("//") { + Url::parse(&format!("gemini:{}", url)) + } else if url.starts_with('/') { + Url::parse(&format!("gemini://{}{}", host, url)) + } else { + let current_host_path = super::history::get_current_url().unwrap(); + Url::parse(&format!("{}{}", current_host_path, url)) + } + } + None => { + if url.starts_with("gemini://") { + Url::parse(url) + } else if url.starts_with("//") { + Url::parse(&format!("gemini:{}", url)) + } else { + Url::parse(&format!("gemini://{}", url)) + } + } + } +} + +#[test] +fn test_make_absolute_full_url() { + super::history::append("gemini://typed-hole.org"); + let url = "gemini://typed-hole.org/foo"; + let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); + let absolute_url = make(&url).unwrap(); + assert_eq!(expected_url, absolute_url); +} +#[test] +fn test_make_absolute_full_url_no_protocol() { + super::history::append("gemini://typed-hole.org"); + let url = "//typed-hole.org/foo"; + let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); + let absolute_url = make(&url).unwrap(); + assert_eq!(expected_url, absolute_url); +} +#[test] +fn test_make_absolute_slash_path() { + super::history::append("gemini://typed-hole.org"); + let url = "/foo"; + let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); + let absolute_url = make(&url).unwrap(); + assert_eq!(expected_url, absolute_url); +} +#[test] +fn test_make_absolute_just_path() { + super::history::append("gemini://typed-hole.org"); + let url = "foo"; + let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); + let absolute_url = make(&url).unwrap(); + assert_eq!(expected_url, absolute_url); +} +#[test] +fn test_make_absolute_full_url_no_current_host() { + let url = "gemini://typed-hole.org/foo"; + let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); + let absolute_url = make(&url).unwrap(); + assert_eq!(expected_url, absolute_url); +} +#[test] +fn test_make_absolute_full_url_no_protocol_no_current_host() { + let url = "//typed-hole.org/foo"; + let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); + let absolute_url = make(&url).unwrap(); + assert_eq!(expected_url, absolute_url); +} +#[test] +fn test_make_absolute_slash_path_no_current_host() { + let url = "/foo"; + let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); + let absolute_url = make(&url).unwrap(); + assert_eq!(expected_url, absolute_url); +} +#[test] +fn test_make_absolute_just_path_no_current_host() { + let url = "foo"; + let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); + let absolute_url = make(&url).unwrap(); + assert_eq!(expected_url, absolute_url); +} diff --git a/src/content.rs b/src/content.rs index a7234ce..1606dab 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1,10 +1,11 @@ +use tempfile::NamedTempFile; use std::io::{Read, Write}; use native_tls::TlsConnector; use std::net::{TcpStream, ToSocketAddrs}; use std::time::Duration; -pub fn get_data(url: &url::Url) -> Result { +pub fn get_data(url: &url::Url) -> Result<(Vec, Vec), String> { let host = url.host_str().unwrap(); let urlf = format!("{}:1965", host); @@ -28,7 +29,11 @@ pub fn get_data(url: &url::Url) -> Result { stream.write_all(url.as_bytes()).unwrap(); let mut res = vec![]; stream.read_to_end(&mut res).unwrap(); - Ok(String::from_utf8_lossy(&res).to_string()) + + let clrf_idx = find_subsequence(&res, b"\r\n"); + let content = res.split_off(clrf_idx.unwrap() + 2); + + Ok((res, content)) } Err(e) => Err(format!("Could not connect to {}\n{}", urlf, e)), } @@ -41,3 +46,19 @@ pub fn get_data(url: &url::Url) -> Result { Err(e) => Err(format!("Could not connect to {}\n{}", urlf, e)) } } + +pub fn download(content: Vec) { + let path = write_tmp_file(content); + open::that(path).unwrap(); +} + +fn write_tmp_file(content: Vec) -> std::path::PathBuf { + let mut tmp_file = NamedTempFile::new().unwrap(); + tmp_file.write_all(&content).unwrap(); + let (_file, path) = tmp_file.keep().unwrap(); + path +} + +fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option { + haystack.windows(needle.len()).position(|window| window == needle) +} diff --git a/src/link.rs b/src/link.rs index 35dfb9d..a1aa1ff 100644 --- a/src/link.rs +++ b/src/link.rs @@ -2,6 +2,7 @@ extern crate regex; use regex::Regex; use url::Url; use std::str::FromStr; +use json::JsonValue; #[derive(Debug)] pub enum Link { @@ -64,3 +65,7 @@ fn make_link(url: String, label: String) -> Option { _ => None } } + +pub fn is_gemini(line: &JsonValue) -> bool { + line["type"] == "gemini" +} diff --git a/src/main.rs b/src/main.rs index fc42187..4ee3a51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,13 +5,15 @@ extern crate native_tls; extern crate regex; use cursive::align::HAlign; -use cursive::theme::Effect; +use cursive::theme::{BaseColor, Color, Effect, PaletteColor, Style, Theme}; use cursive::traits::*; use cursive::utils::markup::StyledString; use cursive::view::Scrollable; use cursive::views::{Dialog, EditView, Panel, SelectView}; use cursive::Cursive; +use json::object; + use std::str::FromStr; use url::Url; @@ -23,12 +25,13 @@ use link::Link; mod content; mod history; +mod absolute; const HELP: &str = "Welcome to Asuka Gemini browser! -Press g to visit an URL -Press h to show/hide history -Press q to exit + Press g to visit an URL + Press h to show/hide history + Press q to exit "; fn main() { @@ -36,11 +39,14 @@ fn main() { let mut siv = Cursive::default(); + let theme = custom_colors(&siv); + siv.set_theme(theme); + let mut select = SelectView::new(); select.add_all_str(HELP.lines()); - select.set_on_submit(|s, link| { - follow_link(s, link); + select.set_on_submit(|s, line| { + follow_line(s, line); }); siv.add_fullscreen_layer( @@ -49,7 +55,9 @@ fn main() { )) .title("Asuka Browser") .h_align(HAlign::Center) - .button("Quit", |s| s.quit()) + .button("Go To URL (g)", |s| prompt_for_url(s)) + .button("History (h)", |s| show_history(s)) + .button("Quit (q)", |s| s.quit()) .with_id("container"), ); @@ -63,6 +71,13 @@ fn main() { siv.run(); } +fn custom_colors(s: &Cursive) -> Theme { + // We'll return the current theme with a small modification. + let mut theme = s.current_theme().clone(); + theme.palette[PaletteColor::Highlight] = Color::Rgb(120, 120, 120); + theme +} + fn prompt_for_url(s: &mut Cursive) { s.add_layer( Dialog::new() @@ -83,10 +98,11 @@ fn prompt_for_answer(s: &mut Cursive, url: Url, message: String) { .content( EditView::new() .on_submit(move |s, response| { - let link = format!("{}?query={}", url.to_string(), response); + let link = format!("{}?{}", url.to_string(), response); s.pop_layer(); follow_link(s, &link); - }).fixed_width(60) + }) + .fixed_width(60), ) .with_id("url_query"), ); @@ -134,14 +150,16 @@ fn visit_url(s: &mut Cursive, url: &Url) { 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)); + match absolute::make(url.as_str()) { + Ok(url) => { + match content::get_data(&url) { + Ok((meta, new_content)) => { + history::append(url.as_str()); + draw_content(s, &url, meta, new_content); + } + Err(msg) => { + s.add_layer(Dialog::info(msg)); + } } }, Err(_) => { @@ -150,81 +168,78 @@ fn visit_url(s: &mut Cursive, url: &Url) { } } -fn draw_content(s: &mut Cursive, url: Url, content: String) { - let url_copy = url.clone(); +fn draw_content(s: &mut Cursive, url: &Url, meta: Vec, content: Vec) { + let content_str = String::from_utf8_lossy(&content).to_string(); - // 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) => { - return 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; - } - Status::Input(message) => { - prompt_for_answer(s, url_copy, message); - } - other_status => { - s.add_layer(Dialog::info(format!("ERROR: {:?}", other_status))); - return; - } - } - } - } + // handle meta header + handle_response_status(s, url, meta, content); let mut main_view = match s.find_id::("main") { Some(view) => view, - None => panic!("Can't find main view.") - }; - let mut container = match s.find_id::("container") { - Some(view) => view, - None => panic!("Can't find container view.") + None => panic!("Can't find main view."), }; // set title and clear old content - container.set_title(url.as_str()); + set_title(s, url.as_str()); main_view.clear(); // draw new content lines - for line in content.lines().skip(1) { + for line in content_str.lines() { match Link::from_str(line) { Ok(link) => match link { - Link::Http(_url, label) => { + Link::Http(url, label) => { let mut formatted = StyledString::new(); - let www_label = format!("[WWW] {}", label); - formatted.append(StyledString::styled(www_label, Effect::Italic)); + let www_label = format!("{} [WWW]", label); + formatted.append(StyledString::styled( + www_label, + Style::from(Color::Dark(BaseColor::Green)).combine(Effect::Bold), + )); - main_view.add_item(formatted, String::from("0")) + let data = object! { + "type" => "www", + "url" => url.to_string() + }; + main_view.add_item(formatted, json::stringify(data)) } - Link::Gopher(_url, label) => { + Link::Gopher(url, label) => { let mut formatted = StyledString::new(); - let gopher_label = format!("[Gopher] {}", label); - formatted.append(StyledString::styled(gopher_label, Effect::Italic)); + let gopher_label = format!("{} [Gopher]", label); + formatted.append(StyledString::styled( + gopher_label, + Style::from(Color::Light(BaseColor::Magenta)).combine(Effect::Bold), + )); - main_view.add_item(formatted, String::from("0")) + let data = object! { + "type" => "gopher", + "url" => url.to_string() + }; + main_view.add_item(formatted, json::stringify(data)) } Link::Gemini(url, label) => { let mut formatted = StyledString::new(); - formatted.append(StyledString::styled(label, Effect::Underline)); + formatted.append(StyledString::styled( + label, + Style::from(Color::Light(BaseColor::Blue)).combine(Effect::Bold), + )); - main_view.add_item(formatted, url.to_string()) + let data = object! { + "type" => "gemini", + "url" => url.to_string() + }; + main_view.add_item(formatted, json::stringify(data)) } Link::Relative(url, label) => { let mut formatted = StyledString::new(); - formatted.append(StyledString::styled(label, Effect::Underline)); + formatted.append(StyledString::styled( + label, + Style::from(Color::Light(BaseColor::Blue)).combine(Effect::Bold), + )); - main_view.add_item(formatted, url.to_string()) + let data = object! { + "type" => "gemini", + "url" => url.to_string() + }; + main_view.add_item(formatted, json::stringify(data)) } Link::Unknown(_, _) => (), }, @@ -233,101 +248,72 @@ fn draw_content(s: &mut Cursive, url: Url, content: String) { } } -fn is_gemini_link(line: &str) -> bool { - line != "0" +fn handle_response_status(s: &mut Cursive, url: &Url, meta: Vec, content: Vec) { + let url_copy = url.clone(); + let meta_str = String::from_utf8_lossy(&meta).to_string(); + + if let Ok(status) = Status::from_str(&meta_str) { + match status { + Status::Success(meta) => { + if meta.starts_with("text/") { + // display text files. + {} + } else { + // download and try to open the rest. + content::download(content); + return; + } + } + Status::Gone(_meta) => { + s.add_layer(Dialog::info("Sorry page is gone.")); + return; + } + Status::RedirectTemporary(new_url) | Status::RedirectPermanent(new_url) => { + return 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; + } + Status::Input(message) => { + prompt_for_answer(s, url_copy, message); + } + other_status => { + s.add_layer(Dialog::info(format!("ERROR: {:?}", other_status))); + return; + } + } + } } -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 set_title(s: &mut Cursive, text: &str) { + let mut container = match s.find_id::("container") { + Some(view) => view, + None => panic!("Can't find container view."), + }; + container.set_title(text); } -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:{}", url)) - } else if url.starts_with('/') { - Url::parse(&format!("gemini://{}{}", host, url)) - } else { - let current_host_path = history::get_current_url().unwrap(); - Url::parse(&format!("{}{}", current_host_path, url)) - } - } - None => { - if url.starts_with("gemini://") { - Url::parse(url) - } else if url.starts_with("//") { - Url::parse(&format!("gemini:{}", url)) +fn follow_line(s: &mut Cursive, line: &str) { + let parsed = json::parse(line); + + match parsed { + Ok(data) => { + if link::is_gemini(&data) { + let next_url = absolute::make(&data["url"].to_string()).expect("Not an URL"); + visit_url(s, &next_url) } else { - Url::parse(&format!("gemini://{}", url)) + open::that(data["url"].to_string()).unwrap(); } } + Err(_) => (), } } -#[test] -fn test_make_absolute_full_url() { - history::append("gemini://typed-hole.org"); - let url = "gemini://typed-hole.org/foo"; - let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); - let absolute_url = make_absolute(&url).unwrap(); - assert_eq!(expected_url, absolute_url); -} -#[test] -fn test_make_absolute_full_url_no_protocol() { - history::append("gemini://typed-hole.org"); - let url = "//typed-hole.org/foo"; - let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); - let absolute_url = make_absolute(&url).unwrap(); - assert_eq!(expected_url, absolute_url); -} -#[test] -fn test_make_absolute_slash_path() { - history::append("gemini://typed-hole.org"); - let url = "/foo"; - let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); - let absolute_url = make_absolute(&url).unwrap(); - assert_eq!(expected_url, absolute_url); -} -#[test] -fn test_make_absolute_just_path() { - history::append("gemini://typed-hole.org"); - let url = "foo"; - let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); - let absolute_url = make_absolute(&url).unwrap(); - assert_eq!(expected_url, absolute_url); -} -#[test] -fn test_make_absolute_full_url_no_current_host() { - let url = "gemini://typed-hole.org/foo"; - let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); - let absolute_url = make_absolute(&url).unwrap(); - assert_eq!(expected_url, absolute_url); -} -#[test] -fn test_make_absolute_full_url_no_protocol_no_current_host() { - let url = "//typed-hole.org/foo"; - let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); - let absolute_url = make_absolute(&url).unwrap(); - assert_eq!(expected_url, absolute_url); -} -#[test] -fn test_make_absolute_slash_path_no_current_host() { - let url = "/foo"; - let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); - let absolute_url = make_absolute(&url).unwrap(); - assert_eq!(expected_url, absolute_url); -} -#[test] -fn test_make_absolute_just_path_no_current_host() { - let url = "foo"; - let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap(); - let absolute_url = make_absolute(&url).unwrap(); - assert_eq!(expected_url, absolute_url); +fn follow_link(s: &mut Cursive, link: &str) { + let next_url = absolute::make(link).expect("Not an URL"); + visit_url(s, &next_url) } diff --git a/src/status.rs b/src/status.rs index 11fa614..93d69d5 100644 --- a/src/status.rs +++ b/src/status.rs @@ -32,7 +32,7 @@ pub enum Status { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ParseError; -const STATUS_REGEX: &str = r"^(\d{1,3})[ \t](.*)$"; +const STATUS_REGEX: &str = r"^(\d{1,3})[ \t](.*)\r\n$"; impl FromStr for Status { type Err = ParseError;