Files
Connex/script/connex.pl

623 lines
16 KiB
Perl
Executable File

#!/usr/bin/perl -w
# connex.pl Nightfall Express (nex://) browser
# By Pete Dussin, peteyboy@sdf.org 3/2024
# updated 6/2026
# This program uses telnet instead of nc to make nex requests, because I couldn't get the Perl nc module to work right.
#TODO: Fill out About Dialog
#TODO: Make status dialog actually useful
#TODO: bookmarks, maybe a quick mark-and-hold toggle to jump between two pages?
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 Scalar::Util 'looks_like_number';
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; # = "";
#new status bar purpose, tracking state
my $S_REQUESTING = "Requesting...";
my $S_READY= " Ready";
my $S_DIDNT= "(page not requested) " . $S_READY;
my $S_NORESPONSE ="(No Response) " . $S_READY;
#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;
$HOME_URL = $full_url; #TODO make sure you want to do this, mostly go home and other defaults will go here instead of nightfall.city
}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 => 'Go Home ^M', -value => \&goto_home },
{ -label => 'Page Links ^P', -value => \&page_links_dialog },
{ -label => 'Browser 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;
if (!defined $scheme){
$scheme="unknown";
}
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);
}
}
sub goto_link_dialog()
{
my $return = $cui->question(-question => "Enter link #:",
);
#if not canceled, or too big or too small, goto link
if($return){
if(looks_like_number($return)){
my $linkcount = scalar @page_links;
if($return <= $linkcount && $return >0){
update_status($S_REQUESTING);
goto_link($return);
}else{
my $return1 = $cui->status("there is no link # " . $return);
}
}else{ #they are using it to navigate to a different site or for some other reason entered text
my $return2 = $cui->status("You did not enter a link #. To Go to a different site, use 'Go' menu or type ctl-g");
}
}
#do nothing on cancel
}
#=== ACTIONS ===
sub goto_back()
{
my $fetched = fetch_history();
#if($fetched) {
$full_url =$fetched;
update_status($S_REQUESTING);
load($full_url,1); #move back to top
history_status_dialog();
#}
}
#let's consider home to be the first item in the history
sub goto_home()
{
if(@history) {
$full_url = $history[0]; #peek at first item
}else{
$full_url=$HOME_URL;
}
update_status($S_REQUESTING);
load($full_url,1); #move back to top
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){
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;
update_status($S_REQUESTING);
load($full_url, 1); #new URL go to top of page
}
sub browser_scroll_reset
{
my $browser = $win1->getobj("browser");
$browser->{-ypos} = 0;
#$browser->{-viewPos} =0; none of this works
$browser->draw();
$browser->focus();
}
sub update_status_bar
{
my $status = shift;
my $browser = $win1->getobj("browser");
my $statusbar = $win1->getobj("status");
$statusbar->text($status . " | Press '>' key to enter link #. ctl-x for Menu. '<' to go back.");
$statusbar->draw();
$browser->focus();
}
sub update_browser_tab
{
my $browser = $win1->getobj("browser");
$browser->title("$full_url");
$browser->draw();
}
sub update_status
{
my $status = shift;
update_status_bar($status);
update_browser_tab();
}
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
{
my $browser = $win1->getobj("browser");
$browser->focus();
#my $link_list = join("\n", @page_links);
if(@page_links){
my $link_list = page_links_list();
my $return = $cui->status("links on this page:\n$link_list");
#}elsif (!is_path_index($browser->title())){ #if not an index, actually is this even important to the user?
# my $return = $cui->status("Not an index page.");
}else{ #there are no links in this index (should be rare, and index with no links?)
my $return = $cui->status("No links on this page.");
}
}
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';
Browsing:
Press '>' key to select a '[>#] link by #
Press '<' key to go back to previous page
Press 'ctl-Y' to see viewed page history
Press 'ctl-P' to see list of links on current page
Scrolling:
Arrow keys to scroll document
Press 'PageDown' to scroll down by page
Press 'PageUp' to scroll up by page
Search:
'less' type search function supported:
Press '/' to start a forward search (type search term, hit <enter>)
search for the next occurance using the 'n' key or the previous
occurance using the 'N' key.
Press '?' to start a reverse search (type search term, hit <enter>)
More 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( \&help_dialog , "\cH");
$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( \&goto_home , "\cM");
$cui->set_binding( \&page_links_dialog , "\cP");
$cui->set_binding( \&history_status_dialog , "\cY");
$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;
#}
sub add_page_link{
my $linkline = shift;
my $base_url = shift;
my $uri_object;
my $uritext;
my $description;
local $URI::ABS_ALLOW_RELATIVE_SCHEME = 1;
local $URI::ABS_REMOTE_LEADING_DOTS = 1;
#remember inside of braces, ^ means NOT, so any NOT SPACE letters are selected, so everything up to next space
if ($linkline=~ /^=> ([^ ]*)/){ #has to be something here or I think URI->new_abs has exception that crashes app
$uri_object=URI->new_abs($linkline=~ /^=> ([^ ]*)/,$base_url);
$uritext = ($linkline=~ s/^=> ([^ ]*).*/$1/r);
chomp($uritext);
push(@page_links, $uri_object->as_string);
my $count = scalar @page_links; #scalar is size/count of links, but $#page_links is highest INDEX
#below puts the [>1] at the end of the description.
#return $linkline =~ s/(^=>.*$)/$1 [\>$count\]/r; don't need to escape on the replace part of a substitution, I guess?
#below puts the [>1] at the beginning of line, replacing =>
#return $linkline =~ s/^=>/[>$count]/r;
#This reconstructs the uri so it can put the [>1] between the uri and the description, but still on the same
#line as the url, if there isn't a description (was unable to figure out how to do in one regex!)
if ($linkline =~ /=> .* [^\n]+/){
$description = ($linkline=~ s/=> [^ ]+ ([^\n]+)/$1/r);
}
else{
$description= "";
}
return "=> " . $uritext . " [>$count] " . $description . "\n";
}else{
return $linkline;
}
}
#=== 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);
#TODO: What if it's not a text file? We need to check and have a download prompt and download if it's not text!
@lines =$connect->getlines(ErrMode=> 'return');
print $connect->eof();
#TODO:handle non-existent request? Die is not pretty to do here
die unless $connect->eof();
$connect->close();
my $widget= $texteditor;
# check if we want to scroll to top
if ($top eq 1){
#For widget programmers: To control the scrollbar, the widget data -vscrolllen (the total length of the content of the widget) and -vscrollpos (the current position in the document) should be set.
# If Curses::UI::Widget::draw is called, the scrollbar will be drawn.
#browser_scroll_reset();
#my $browser = $win1->getobj("browser");
$widget->{-ypos} = 0;
#$browser->{-viewPos} =0; none of this works
#$browser->draw();
#$browser->focus();
}
#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 ($is_index){ #only do this for index pages
if ($currentline =~ m/^=> /){ #check for valid rocket link with space
$currentline = add_page_link($currentline, $url);
}
}
$page_contents= $page_contents . $currentline;
}
$widget->text($page_contents);
$widget->draw();
if ($page_contents eq ''){
update_status($S_NORESPONSE);
}else{
update_status($S_READY);
}
}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();
update_status($S_DIDNT);
}
}