@ -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 < u8 > , content : Vec < u8 > ) {
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 ::< SelectView > ( "main" ) {
Some ( view ) = > view ,
None = > panic! ( "Can't find main view." )
} ;
let mut container = match s . find_id ::< Dialog > ( "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 < u8 > , content : Vec < u8 > ) {
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 ::< Dialog > ( "container" ) {
Some ( view ) = > view ,
None = > panic! ( "Can't find container view." ) ,
} ;
container . set_title ( text ) ;
}
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:{}" , 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 )
}