#!/usr/bin/perl -w # connex.pl Nightfall Express (nex://) browser # By Pete Dussin, peteyboy@sdf.org 3/2024 # This program uses telnet instead of nc to make nex requests, because I couldn't get the Perl nc module to work right. #TODO: Some branding (status popup on load?, Connex menu item?), About dialog #TODO: Fill out help Dialog #TODO: Fill out About Dialog #Make status dialog actually useful use FindBin qw($Bin); use lib "$Bin/../lib"; use strict; use warnings; use strict; use Curses::UI; use Net::Telnet; use URI::Split qw(uri_split uri_join); use URI (); use Term::ReadKey; #for fatpacker? #=== INITIALIZE VARIABLES === #my $HOST_DEFAULT = "nightfall.city"; my $PATHSPEC_DEFAULT = ''; my $PORT_DEFAULT = 1900; my $SCHEME_NEX = "nex"; my $host = "nightfall.city"; my $port = $PORT_DEFAULT; my $pathspec = $PATHSPEC_DEFAULT; my $docname; # = ""; #for future use, when you think you can deal with doc types my $doctype = "txt"; my $dot_ext = "."; #default URL to the "home" NEX site #my $HOME_URL = uri_join($SCHEME_NEX,$HOST_DEFAULT); my $HOME_URL = "nex://nightfall.city"; #==== Command Line Initialization ==== # Initialize: See if they specified a starting nex URL from command line #if argument entered, it should be a nex url: my $full_url= $ARGV[0]; if (defined $full_url){ my $uri=URI->new($full_url, $SCHEME_NEX); if ($uri->scheme ne $SCHEME_NEX){ die "Need a $SCHEME_NEX url, or start without supplying URL argument.\n"; } $full_url= $uri->as_string; }else{ $full_url = $HOME_URL; } #Arrays my @history; my @page_links; #=== UI VARIABLES === my $statusbar; my $navwindow; my $win1; my $cui = new Curses::UI( -color_support => 1 ); my $connect = new Net::Telnet (Timeout => 10, Errmode => 'return'); my @menu = ( { -label => 'File', -submenu => [ { -label => 'Choose Link >', -value => \&goto_link_dialog }, { -label => 'Back ^B/<', -value => \&goto_back }, { -label => 'Go to Link ^G', -value => \&navigate_link_dialog }, { -label => 'Page Links ^P', -value => \&page_links_dialog }, { -label => 'History ^Y', -value => \&history_status_dialog }, { -label => 'Exit ^Q', -value => \&exit_dialog } ] }, { -label => 'Help', -submenu => [ { -label => 'Help ^H', -value => \&help_dialog }, { -label => 'About ', -value => \&about_dialog }, ] }, ); #=== DIALOGS === sub exit_dialog() { my $return = $cui->dialog( -title => "Quit Connex?", -message => "Are you sure?", -buttons => ['yes', 'no'], ); exit(0) if $return; } sub links_dialog() { my $return = $cui->dialog( -message => page_links_list(), -title => "Page Links", -buttons => ['ok'], ); } sub unsupported_dialog { my $scheme = shift; my $return = $cui->dialog( -message => "$scheme protocol not supported.", -title => "Bad Navigation", -buttons => ['ok'], ); } sub navigate_link_dialog() { my $return = $cui->question(-question => "This is [$full_url]. Enter destination link:", -answer => $full_url, ); #if not user canceled then navigate if($return){ navigate($return); update_status_bar(); } } sub goto_link_dialog() { my $return = $cui->question(-question => "Enter link #:", ); #if not canceled, or too big or too small, goto link if($return){ my $linkcount = scalar @page_links; if($return <= $linkcount && $return >0){ goto_link($return); update_status_bar(); }else{ #$browser->focus(); my $return1 = $cui->status("there is no link # " . $return); } #do nothing on cancel } } #=== ACTIONS === sub goto_back() { my $fetched = fetch_history(); #if($fetched) { $full_url =$fetched; load($full_url,0); #don't move back to top update_status_bar(); history_status_dialog(); #} } sub goto_link{ my $linknum = shift; my $linkcount = scalar @page_links; #my $element=$linknum -1; #offset for array element #if ($element <= $#page_links){ #such a perlism, this is highest INDEX of array if ($linknum <= $linkcount){ #such a perlism, this is highest INDEX of array navigate($page_links[$linknum-1]); #offset from count to index }else{ #my $browser = $win1->getobj("browser"); #$browser->focus(); my $return = $cui->status("no link # " . $linknum); } } sub navigate{ my $link = shift; add_history($full_url); #add last link to history before going forward! $full_url = $link; load($full_url, 1); #new URL go to top of page } sub update_status_bar { my $browser = $win1->getobj("browser"); my $statusbar = $win1->getobj("status"); #$full_url = construct_valid_url($SCHEME_NEX, $host, $pathspec, $docname); $statusbar->text("$full_url" . " | Press '>' key to enter link #. ctl-x for Menu. '<' to go back."); $statusbar->draw(); $browser->focus(); } sub add_history { my $link = shift; push(@history, $link); history_status_dialog(); return; } sub fetch_history { my $latest; if(@history) { $latest = pop(@history); return $latest; }else{ return($HOME_URL); } } sub history_status_dialog { if(@history){ my $browser = $win1->getobj("browser"); $browser->focus(); my $history_list = join("\n", @history); my $return = $cui->status("history:\n$history_list"); } } sub page_links_dialog { if(@page_links){ my $browser = $win1->getobj("browser"); $browser->focus(); #my $link_list = join("\n", @page_links); my $link_list = page_links_list(); my $return = $cui->status("links on this page:\n$link_list"); } } sub page_links_list { if(@page_links){ my $browser = $win1->getobj("browser"); $browser->focus(); #my $link_list = join("\n", @page_links); my $link_list=""; my $link; my $count=0; foreach $link (@page_links){ $count+=1; $link_list = $link_list . "[$count] $link\n"; } return $link_list; } } sub about_dialog { my $browser = $win1->getobj("browser"); $browser->focus(); my $return = $cui->status("Connex browser\n by gorf\@rawtext.club\n 2024"); } sub help_dialog { my $browser = $win1->getobj("browser"); $browser->focus(); my $message = <<'END_MESSAGE'; Navigation: Press '>' key to select a '[>#]link by # Press '<' key to go back to previous page Program Features: Press ctl-x for menu END_MESSAGE my $return = $cui->dialog( -message => $message, -title => "Connex Help", -buttons => ['ok'], ); } #=== KICKOFF UI === my $menu = $cui->add( 'menu','Menubar', -menu => \@menu, -fg => "blue", ); $win1 = $cui->add( 'win1', 'Window', -title => "Connex, a Nightfall Express (nex://) browser", -border => 1, -y => 1, -bfg => 'red', -vscrollbar => 'right', ); my $texteditor = $win1->add("browser", "TextViewer", -text => "Start Page", -border => 1, -padtop => 0, -padbottom => 3, -showlines => 0, -sbborder => 0, -vscrollbar => 1, -hscrollbar => 1, -showhardreturns => 0, -wrapping => 0, # wrapping slows down the editor :-( ); $statusbar = $win1->add("status", "TextViewer", -border => 1, -bfg => 'red', -y => -1, -height => 1, -width => -1, -reverse => 1, -paddingspaces => 1, -text => "$full_url | Press '>' key to enter link #; ctl-x for menu; '<' to go back.", ); #key bindings, should match menu items $cui->set_binding(sub {$menu->focus()}, "\cX"); $cui->set_binding( \&exit_dialog , "\cQ"); $cui->set_binding( \&navigate_link_dialog , "\cG"); $cui->set_binding( \&goto_back , "\cB"); $cui->set_binding( \&goto_back , "<"); $cui->set_binding( \&goto_link_dialog , ">"); $cui->set_binding( \&page_links_dialog , "\cP"); $cui->set_binding( \&history_status_dialog , "\cY"); $cui->set_binding( \&help_dialog , "\cH"); $cui->set_binding(sub { my $cui = shift; $cui->layout; $cui->draw; }, "\cL"); # There is no need for the editor widget to loose focus, so # the "loose-focus" binding is disabled here. This also enables the # use of the "TAB" key in the editor, which is nice to have. #$texteditor->clear_binding('loose-focus'); #=== START UP === $texteditor->focus(); navigate($full_url); fetch_history(); #navigate places a history entry, will make a duplicate, so remove it $cui->mainloop(); #=== NEX STUFF === #make nex:// urls from parts, relative urls sub construct_valid_url #scheme, host, pathspec, docname { my $scheme = shift; my $host = shift; my $pathspec = shift; my $docname = shift; if (defined($docname)){ $pathspec= $pathspec . '/' . $docname; #this is just local pathspec } my $url = uri_join($scheme, $host, $pathspec); return $url; } sub is_path_index{ my $path = shift; my $result = 1; if ($path !~ /\/$/ && $path =~ /\.[a-zA-Z]*$/){ $result =0; } return($result); } sub add_page_link{ my $linkline = shift; my $base_url = shift; my $uri_object; #my $link= $linkline=~ s/^=>[ ]*(.*$)/$1/r; local $URI::ABS_ALLOW_RELATIVE_SCHEME = 1; local $URI::ABS_REMOTE_LEADING_DOTS = 1; $uri_object=URI->new_abs($linkline=~ /^=>[ ]+([^ ]*)/,$base_url); push(@page_links, $uri_object->as_string); my $count = scalar @page_links; #scalar is size/count of links, but $#page_links is highest INDEX return $linkline =~ s/(^=>.*$)/$1 [\>$count\]/r; } #=== NEX REQUESTS AND RESPONSE PROCESSING === sub load { my @lines; my $ok; my $url = shift; my $top = shift; #my $scheme, $host, $path, $query, $frag; my ($scheme, $host, $path, $query, $frag) = uri_split($url); #what happens with different scheme? if ($scheme eq $SCHEME_NEX){ #A lot of trial and error here, connect->print of path was somehow important to get output to complete... #...also checking the EOF() function $connect->host($host); $connect->port($port); $ok= $connect->open($host); $ok= $connect->print($path); @lines =$connect->getlines(ErrMode=> 'return'); print $connect->eof(); #TODO:handle non-existant request? Die is not pretty to do here die unless $connect->eof(); $connect->close(); my $widget= $texteditor; #loop through nex response and load into text editor. Identify, mark and store link lines for index pages my $page_contents =""; #this is the page contents to be dumped into the browser widget my $currentline; my $count=0; my $is_index= is_path_index($path); undef @page_links; #clear list foreach $currentline (@lines){ if ($currentline =~ m/^=>/){ if ($is_index){ $currentline = add_page_link($currentline, $url); } } $page_contents= $page_contents . $currentline; } $widget->text($page_contents); $widget->draw(); if ($top){ $widget->pos(0); } }else{ #can't load non-nex as of now my $browser = $win1->getobj("browser"); $browser->focus(); my $return = unsupported_dialog($scheme); #pop and revert to history for display #my $fetched = fetch_history(); $full_url = fetch_history(); } }