#!/usr/bin/perl
# Perl-language replacement for 'ssfe'
# 1999-08-12
# (c) 1999 unSlider, all rights under copyright reserved.
# ABSOLUTELY NO WARRANTY.

use Curses;
use FileHandle;
use IPC::Open2;

#$sblimit=666; #limit on number of scrollback lines
#$histlimit=666; #limit on number of lines of command listory

%bindings=("\cM"=>sub {
               push @history, $inbuf;
	       if($histlimit!=0 && $#history>$histlimit) {
		   splice(@history,0,$#history-$histlimit);
	       }
	       if($escesc && ($inbuf=~/^\@ssfe\@/)) {
		   $inbuf="\@ssfe\@!q$inbuf";
	       }
	       print WRITER "$inbuf\n";
	       $inbuf='';
	       $incsr=0;
	       undef $noecho;
	       undef $prompt;
	       &drawinbuf();
           },
	   "\cC"=>sub {
	       kill HUP => $kidpid;
	   },
	   "\x1C"=>\&gotohell, # ^\
	   "\cA"=>sub {
	       $incsr=0;
	       &drawinbuf();
	   },
	   "\cB"=>\&ipleft,
	   @{[KEY_LEFT]}=>\&ipleft,
	   "\cD"=>\&delunder,
	   @{[KEY_DC]}=>\&delunder,
	   "\cE"=>sub {
	       $incsr=length($inbuf);
	       &drawinbuf();
	   },
	   "\cF"=>\&ipright,
	   @{[KEY_RIGHT]}=>\&ipright,
	   "\cH"=>\&backspace,
	   @{[KEY_BACKSPACE]}=>\&backspace,
	   "\c?"=>\&backspace,
	   "\cI"=>sub {
	       unshift(@tablines,$inbuf=pop(@tablines));
	       $incsr=length($inbuf);
	       &drawinbuf();
	   },
	   "\cK"=>sub {
	       $cutbuffer=substr($inbuf,$incsr);
	       substr($inbuf,$incsr)='';
	       &drawinbuf();
	   },
	   "\cL"=>sub {
	       &drawsb();
	       &statbar();
	       &drawinbuf();
	   },
	   "\cO"=>sub {
	       &insertchar($cotext);
	   },
	   "\cT"=>sub {
	       print WRITER "$cttext\n";
	   },
	   "\cU"=>sub {
	       $incsr=0;
	       $cutbuffer=$inbuf;
	       $inbuf='';
	       &drawinbuf();
	   },
	   "\cY"=>sub {
	       &insertchar($cutbuffer);
	   },
	   "\cZ"=>sub {
	       endwin;
	       kill STOP => $$;
	       refresh;
	   },
	   @{[KEY_PPAGE]}=>sub {
	       $bslines+=10;
	       &drawsb();
	   },
	   @{[KEY_NPAGE]}=>sub {
	       $bslines-=10;
	       $bslines=0 if($bslines<0);
	       &drawsb();
	   },
	   @{[KEY_END]}=>sub {
	       $bslines=0;
	       &drawsb();
	   },
	   "\cN"=>\&histfwd,
	   @{[KEY_DOWN]}=>\&histfwd,
	   "\cP"=>\&histback,
	   @{[KEY_UP]}=>\&histback,
	   "\cV"=>sub {$charesc=2},
	   "\cX"=>sub {$charesc=2},
	   "\cXb"=>sub {
	       $beepflag^=1;
	   },
	   "\cXc"=>\&gotohell,
	   "\cXh"=>sub {
	       &addtobuf("pretend i just toggled hold-mode");
	   },
	   "\cXi"=>sub {
	       if($ssfemode eq 'cooked') {
		   $ssfemode='irc';
	       } else {
		   $ssfemode='cooked';
	       }
	   }
	   );

%ssfeescs=('i'=>sub {
               $ssfemode='irc';
	       print WRITER "\@ssfe\@i\n";
	   },
	   'c'=>sub {
	       $ssfemode='cooked';
	       print WRITER "\@ssfe\@c\n";
	   },
	   's'=>sub {
	       &statbar($_[0]);
	   },
	   'T'=>sub {
	       $cttext=$_[0];
	   },
	   'o'=>sub {
	       $cotext=$_[0];
	   },
	   't'=>sub {
	       push @tablines, $_[0];
	       shift @tablines if $#tablines>19;
	   },
	   'l'=>sub {
	       push @scrollback, (undef)x($LINES-2);
	       push @scrollattr, (undef)x($LINES-2);
	       &drawsb;
	   },
	   'p'=>sub {
	       $prompt=$_[0] if(length($prompt)<=8);
	       &drawinbuf;
	   },
	   'P'=>sub {
	       $prompt=$_[0] if(length($prompt)<=8);
	       $noecho=1;
	       &drawinbuf;
	   },
	   'n'=>sub {
	       &insertchar($_[0]);
	   },
	   '!q'=>sub {
	       &addtobuf($_[0]);
	   },
	   '!e'=>sub {
	       $escesc=1;
	       print WRITER "\@ssfe\@!e\n";
	   },
	   '!p'=>sub {
	       $paranoid=1;
	   },
	   '!`'=>sub {
	       if($escesc && !$paranoid) {
		   eval($_[0]);
	       }
	   },
	   '!b'=>sub {
	       if($_[0]=~/^$/) {
		   $bindhook=1;
	       } else {
		   &dokey($_[0],0);
	       }
	   },
	   );

%cchars=("\cB"=>sub {$currattr^=A_BOLD;},
	 "\cV"=>sub {$currattr^=A_REVERSE;},
	 "\c_"=>sub {$currattr^=A_UNDERLINE;},
	 "\cO"=>sub {$currattr^=0;},
	 );

$ssfemode='cooked';

initscr;
cbreak;                         # disable buffering and erase/kill
raw;				# live dangerously
nonl;                           # no newline->\n munging
noecho;                         # disable getch echoing
timeout(-1);                    # getch blocks for input
keypad(stdscr,1);               # decode extended keys to curses constants

$command=join(' ',@ARGV);

&statbar($command);

$kidpid=open2(\*READER,\*WRITER,$command);

for(;;) {
	$rin='';
	vec($rin,fileno(READER),1)=1;
	vec($rin,fileno(STDIN),1)=1;
	select($rout=$rin,undef,undef,undef);
	if(vec($rout,fileno(READER),1)) {
	    if(!sysread(READER,$char,1)) {
		endwin;
		exit;
	    }
	    if($char eq "\n") {
		if((($escape,$args)=$gotline=~
		   /^`#ssfe#(\!?.)(.*)/) #`)# emacs sucks
		   && defined($ssfeescs{$escape})) {
		    &{$ssfeescs{$escape}}($args);
		} else {
		    &addtobuf($gotline);
		}
		$gotline='';
	    } else {
		$gotline.=$char;
	    }
	}
	if(vec($rout,fileno(STDIN),1)) {
	    undef $char if(!$charesc);
	    $char.=getch;
	    &dokey($char,$bindhook);
	    $charesc-- if($charesc);
	}
}

# Display the specified string on the status line. If no string is provided,
# use the previously specified string, stored in the global $statbar.
# also displays an indication of scollback status on the rhs of the bar
sub statbar {
    my($foo,$bar);

    if(defined($_[0])) {
	$statbar=substr($_[0].' 'x$COLS,0,$COLS);
    }

    if($bsnew!=0) {
	$bar="($bsnew)";
	substr($statbar,0-length($bar))=$bar;
    } elsif($bslines!=0) {
	substr($statbar,-1)="v";
    }

    $foo=getattrs();
    attrset(A_REVERSE);
    addstr($LINES-2,0,$statbar);
    clrtoeol;
    attrset($foo);
    refresh;
}

# At a specified x,y, add a specified string with specified attributes.
sub addstrattr {
    my $y=shift;
    my $x=shift;
    my @text=split(//,shift);
    my @attr=@{shift()};
    my $char;
    my $foo;

# unfortunately, Curses.pm lacks a chgat(), so we have to do things the slow
# way.
    $foo=getattrs();
    move($y,$x);
    foreach $char (@text) {
	attrset(shift(@attr));
	addch($char);
    }
    clrtoeol;
    attrset($foo);
}

# Update the scrollback window on the screen to reflect the current state of
# the scrollback buffer.
sub drawsb {
    my $windstart;

    $windstart=($#scrollback-($LINES-2))+1;
    $windstart-=$bslines+$bsnew;
    if($windstart<0) {
	$windstart=0;
    }
    for($i=0;$i<$LINES-2;$i++) {
	&addstrattr($i,0,$scrollback[$i+$windstart],
		    $scrollattr[$i+$windstart]);
    }
    &drawinbuf();
    refresh;
}

# Add a line to the end of the scrollback buffer.
# word-wraps the line into multiple lines if need be
sub addtobuf {
    my @foo;
    my $char;
    my($linetext, @lineattr);
    my($foo, $bar);
    my($outline, $outattr);
    my @bat;

    $currattr=0;

    beep if($beepflag && ($_[0]=~/\cG/));

    foreach $char (split(//,$_[0])) {
	if($ssfemode eq 'irc' && defined($cchars{$char})) {
	    &{$cchars{$char}};
	} elsif($char lt ' ') {
	    $linetext.=chr(ord($char)+0x40);
	    push @lineattr, $currattr^A_REVERSE;
	} elsif($char eq '\cI') {
	    $linetext.=' ';
	    push @lineattr, $currattr;
	} else {
	    $linetext.=$char;
	    push @lineattr, $currattr;
	}
    }

    @bat=map {"$_ "} split(/ /,$linetext);
    chop($bat[$#bat]);
    while(defined($foo=shift(@bat))) {
	if(length($foo)>$COLS) {
	    unshift @bat, substr($foo,$COLS);
	    $foo=substr($foo,0,$COLS);
	}
	if(length($outline)+length($foo)>$COLS) {
	    push @scrollback, $outline;
	    push @scrollattr, $outattr;
	    $bsnew++ if($bslines!=0);
	    undef $outline;
	    undef $outattr;
	}
	$outline.=$foo;
	$bar=length($foo)-1;
	undef @foo;
	(@foo[0..$bar], @lineattr)=@lineattr;
	push @$outattr, @foo;
    }
    push @scrollback, $outline;
    push @scrollattr, $outattr;
    $bsnew++ if($bslines!=0);

    if($sblimit!=0 && $#scrollback>$sblimit) {
	splice(@scrollback,0,$#scrollback-$sblimit);
	splice(@scrollattr,0,$#scrollback-$sblimit);
    }
    &drawsb();
    refresh;
}

# Update the input line on the screen to reflect the current state of the input
# buffer.
sub drawinbuf {
    my $char;

    if($incsr>($inpan+$COLS)-(length($prompt)+5) || $inpan>$incsr) {
	$inpan=$incsr-($incsr%(($inpan+$COLS)-(length($prompt)+5)));
#	$inpan=$incsr-5;
#    } elsif($inpan>$incsr) {
#	$inpan=$incsr;
    }
    addstr($LINES-1,0,$prompt);
    if($noecho) {
	addstr("*"x+length(substr($inbuf,$inpan,$COLS)));
    } else {
	foreach $char (split(//,substr($inbuf,$inpan,$COLS))) {
	    if($char lt ' ') {
		attrset(getattrs()^A_REVERSE);
		addch(chr(ord($char)+0x40));
		attrset(getattrs()^A_REVERSE);
	    } else {
		addch($char);
	    }
	}
    }
    clrtoeol;
    move($LINES-1,($incsr-$inpan)+length($prompt));
    refresh;
}

# Insert a character (or multiple characters) into the input buffer.
sub insertchar {
    substr($inbuf,$incsr,0)=$_[0];
    $incsr+=length($_[0]);
    &drawinbuf();
}

# delete the character to the left of the cursor in the input buffer.
sub backspace {
    if($incsr>0) {
	substr($inbuf,$incsr-1,1)='';
	$incsr--;
	&drawinbuf();
    }
}

# delete the character under the insertion point in the input buffer
sub delunder {
    substr($inbuf,$incsr,1)='';
    &drawinbuf();
}

# move the input buffer insertion point to the left
sub ipleft {
    $incsr-- if($incsr>0);
    &drawinbuf();
}

# move the input buffer insertion point to the right
sub ipright {
    $incsr++ if($incsr<length($inbuf));
    &drawinbuf();
}

# scroll forward through the command history entries
sub histfwd {
    push @history, $inbuf;
    $inbuf=shift(@history);
    $incsr=0;
    &drawinbuf();
}

# scroll backward through the command history entries
sub histback {
    unshift @history, $inbuf;
    $inbuf=pop(@history);
    $incsr=0;
    &drawinbuf();
}

# exit cleanly
sub gotohell {
    endwin;
    exit;
}

# Process a keystroke of user input.
sub dokey {
    my $char=shift;
    my $bindhook=shift;

    if($bindhook==1 && $char lt ' ') {
	print WRITER "\@ssfe\@!b$char\n";
    } elsif(defined($bindings{$char})) {
	&{$bindings{$char}}();
    } else {
	$char=~s/^\cV(.)$/$1/;
	&insertchar($char);
    }
}

=pod

=for man
.TH PSSFE 1 "" "August 1999"

=cut

=head1 NAME

pssfe -- a split-screen front end for sirc

=head1 SYNOPSIS

B<pssfe> I<shell-command>

=head1 DESCRIPTION

Bakes Satan a peanut-butter and jelly sandwich.

=head1 BASELINE PROTOCOL

The baseline ssfe protocol, as implemented in the 7 June 97 version of ssfe.c,
implements the following escapes, which must start at the beginning of a line,
be preceeded by string "`#ssfe#", and terminate with a newline. Unrecognized
escapes should be ignored. Commands may not be longer than 512 bytes in
length. Note that this section applies to ssfe.c, and B<pssfe> is slightly
different, as is explained in the next section.

=over 4

=item i

Enter IRC mode, and acknowledge by sending "@ssfe@i" and a newline. In IRC
mode, the ^B, ^V and ^_ characters in the child process's output toggle
bold, inverse and underlined mode, respectively; the ^O character in the
child process's output turns off bold, inverse and underlined mode; ^I
characters skip to the next 8-character tab stop; words that do not fit on a
line are wrapped to the next line; and control characters are displayed as
inverse characters.

=item c

Enter cooked mode, and acknowledge by sending "@ssfe@c" and a newline. Cooked
mode is like IRC mode except that the ^B, ^V, ^_ and ^O characters are not
processed specially. Cooked mode is the default mode.

=item sI<status>

Display the specified string in the status bar.

=item TI<cttext>

Store I<cttext>, which is truncated if it is longer than 126 characters.
Whenever the user presses ^T, I<cttext> is sent to the child process,
followed by a newline. Pressing ^T does not affect the input line.

=item oI<cotext>

Store I<cotext>. Whenever the user presses ^O, I<cotext> is inserted in the
input line.

=item tI<tabtext>

Add I<tabtext> to the list of "tab lines". Each time the user presses the
tab key, the contents of the input line are obliterated and replaced with one
of the "tab lines". Repeatedly pressing the tab key causes the list of
"tab lines" to be cycled through. There is no length limit imposed on
I<tabtext>, but there is a limit of 20 "tab lines", after which new "t" escapes
replace old "tab lines" rather than adding new ones.

=item l

Clear the screen and turn off boldface, inverse and underlined modes.

=item pI<prompt>

Display I<prompt> on the status line until the user has entered a line of
input. The user is not allowed to use the input history functions until a line
has been entered. I<prompt> may be null but may not be greater than 8
characters. The child process may continue displaying lines and sending
escapes while waiting for the user to respond. Sending another "p" escape
before the user has entered a line in response to the first prompt will only
cause the prompt to be replaced with the new prompt; the new prompt will NOT
be queued until the first prompt has been responded to. The user's response to
the prompt is not treated specially, i.e. it is sent to the child process
just as is any text the user enters. If I<prompt> is greater than 8 characters,
the escape is silently ignored.

=item PI<prompt>

Identical in functionality to the "p" escape, except that any characters the
user enters in response to the prompt will be displayed on the input line as
asterisks instead of echoed back. Sending a "p" escape before the user has
responded to the "P" escape's prompt will NOT re-enable normal echoing of
characters. Normal echoing of characters is only resumed once the user has
responded to the prompt.

=item nI<text>

Insert I<text> in the input buffer, as if the user had typed it. Control
characters and the like are inserted literally into the buffer.

=back

=head1 PROTOCOL EXTENSIONS

B<pssfe> recognizes the changes and extensions to the escapes listed below.
Also, B<pssfe> places no restriction on the length of the commands it can
process. Unrecognized escapes are treated differently, too: B<ssfe> ignores
them by silently dropping them, while B<pssfe> ignores them by displaying them
like a non-escape input line.

=over 4

=item TI<cttext>

The "T" escape differs from the baseline protocol in that I<cttext> is not
limited to 126 characters.

=item l

Since boldface, inverse and underlined modes do not carry over between multiple
lines of the child process's output, the "l" escape does not turn off
boldface, inverse and underlined modes.

=item p

=item P

Command history is not disabled during the reading of a response to a prompt.

=item !b

When used with no arguments, enables bindhook mode. In bindhook mode, any
control characters typed by the user cause "@ssfe@!bI<char>" and a newline to
be sent to the child process, where I<char> is the character the user typed.
Thus, the internal key bindings are bypassed, allowing the child process to
take over processing of keystrokes. No confirmation is sent in response to this
escape; programs should first try a "!e" escape to see if B<pssfe> or another
similarly extended front-end is in use. Note that I<char> may actually be more
than one character if you are chaining the ^X and ^V escapes back to the
default handlers.

=item !bI<char>

Processes the given character as if the user had pressed it, except that in
bindhook mode the built-in bindings are always used instead of sending a line
to the child process. This can be used when the child process doesn't recognize
a keypress and wants B<pssfe> to process it as usual.

=item !e

The "!e" escape enables escape-escaping mode and acknowledges by sending
"@ssfe@!e" to the child process. When escape-escaping mode is enabled, any
line of user input that begins with "@ssfe@" is prefixed with "@ssfe@!q" before
sending it to the child process. This prevents user input from being
mistaken as a message from B<pssfe>, like SMTP's dot-escaping. This escape also
enables the "!`" escape, which is disabled by default.

=item !p

Disable the "!`" escape. Disabling the "!`" escape with the "!p" escape will
result in it being permanently disabled for the lifetime of this B<pssfe>
process, no matter how loudly you send the "!e" escape.

=item !qI<text>

Displays I<text> as if the child process had printed it without the escape.
This can be used to display lines that start with "`#ssfe#"; otherwise, such
lines are unprintable since they are interpreted as escapes.

=item !`I<code>

Evaluate I<code> in the Perl interpreter B<pssfe> is running in. This escape is
only recognized when it has been enabled with the "!e" escape, and can be
permanently disabled with the "!p" escape.
B<THIS ESCAPE IS FOR TESTING PURPOSES ONLY, PLEASE DON'T DEPEND ON IT.>

=back

=head1 BUGS

Hold mode is not supported.

We don't even try to deal with WINCHes.

Control-L doesn't appear to work.

Words wider than the screen aren't broken in the most space-efficient fashion
possible.

invoking 'sh -i' doesn't seem to work?

tabs? (in text and on keyboard): ^I in input lines should go to the nearest
8 character tab stop. Pressing tab on the keyboard does something similar to
what ssfe.c does, but exactness needs to be compared. Also, documentation.

sblimit and histlimit untested

scroll beyond top of buffer: Doesn't follow the principle of least surprise.
Scroll position keeps moving up but the text stays the same.

bindhook is incomplete and untested.
 - now complete, still untested

Documentation is incomplete.

A status bar or similar indication of scrollback status Would Be Nice.
 - this is now done but everything that modifies scrollback status now needs to
   make sure that the status bar is updated

bsnew
 - should be done now, might even work
 - only thing is, when the scrollback is set to 0, bslines MUST be cleared, but
   it isn't. need to fix this.

More bindings, e.g. alt-P for previous screen

length limits on input lines?

#type >80 chars, try backspacing. the screen doesn't scroll left.

need to review perlpod, compare with that on ppt, i think there's a better way

best way to deal with scroll back/forward issues is probably make one routine
that takes a + or - offset and does all the horny boojum

=head1 LICENSE

This is, uh, well.. use/distribute it under either the Perl Artistic License or
the GNU Lesser General Pubic Virus, at your option.

=cut

