--- /dev/null
+#!/usr/bin/perl
+# The Virtual Songbook 0.0
+# (c) 2003 Martin Mares <mj@ucw.cz>
+
+use Curses;
+use strict;
+use warnings;
+
+### Interface with Curses ###
+
+my $W;
+my $color_mode;
+
+sub init_terminal() {
+ $W = new Curses;
+ start_color;
+ $color_mode = (has_colors && COLORS >= 8 && COLOR_PAIRS >= 8) unless defined $color_mode;
+ cbreak; noecho;
+ $W->intrflush(0);
+ $W->keypad(1);
+ $W->meta(1);
+}
+
+sub cleanup_terminal() {
+ endwin;
+}
+
+my ($attr_normal, $attr_status, $attr_hilite, $attr_chord);
+
+sub setup_attrs() {
+ if ($color_mode) {
+ init_pair(1, COLOR_YELLOW, COLOR_BLUE);
+ $attr_status = COLOR_PAIR(1) | A_BOLD;
+ init_pair(2, COLOR_YELLOW, COLOR_BLACK);
+ $attr_chord = COLOR_PAIR(2);
+ } else {
+ $attr_status = A_BOLD;
+ $attr_chord = A_BOLD;
+ }
+ $attr_normal = A_NORMAL;
+ $attr_hilite = A_BOLD;
+}
+
+my $try_full_names = 1;
+my $auto_enter = 1;
+my $file_window_width = 20;
+
+my ($term_w, $term_h);
+my @window_list = ();
+my $status_window = new VS::Window::Status;
+my $main_window = new VS::Window::Main;
+my $file_window = new VS::Window::File;
+$file_window->reload;
+my $focused_window_i = 2;
+my $focused_window = $file_window;
+
+sub focus_next() {
+ $focused_window->{"focused"} = 0;
+ do {
+ $focused_window_i++;
+ if ($focused_window_i > $#window_list) { $focused_window_i=0; }
+ $focused_window = $window_list[$focused_window_i];
+ } while (!$focused_window->{"focusable"} || !$focused_window->{"visible"});
+ $focused_window->{"focused"} = 1;
+}
+
+sub recalc_windows() {
+ my $w = COLS;
+ my $h = LINES;
+ $term_w = $w;
+ $term_h = $h;
+ $status_window->place(0, 0, 1, $w);
+ if ($file_window->{"visible"}) {
+ my $fww = $file_window_width;
+ $main_window->place(1, 0, $h-1, $w-$fww-1);
+ $W->attrset($focused_window == $file_window ? $attr_hilite : $attr_normal);
+ $W->vline(2, $w-$fww-1, ACS_VLINE, $h-3);
+ $W->hline(1, $w-$fww, ACS_HLINE, $fww);
+ $W->hline($h-1, $w-$fww, ACS_HLINE, $fww);
+ $W->addch(1, $w-$fww-1, ACS_ULCORNER);
+ $W->addch($h-1, $w-$fww-1, ACS_LLCORNER);
+ $W->attrset($attr_normal);
+ $file_window->place(2, $w-$fww, $h-3, $fww);
+ } else {
+ $main_window->place(1, 0, $h-1, $w);
+ $file_window->place(0, 0, 0, 0);
+ }
+}
+
+sub toggle_window($) {
+ my $win = shift;
+ if ($win->{"visible"} = !$win->{"visible"}) {
+ while ($focused_window != $win) { focus_next; }
+ } else {
+ if ($focused_window == $win) { focus_next; }
+ }
+ recalc_windows;
+}
+
+init_terminal;
+setup_attrs;
+recalc_windows;
+
+for(;;) {
+ $W->move($term_h-1, $term_w-1);
+ $W->refresh;
+ my $key = $W->getch;
+ if ($key eq "\033" || $key eq "q") {
+ cleanup_terminal;
+ exit 0;
+ } elsif ($key eq "f") {
+ toggle_window($file_window);
+ } elsif ($key eq "\t") {
+ focus_next;
+ recalc_windows;
+ } elsif ($key eq "\014") {
+ $curscr->clearok(1);
+ } elsif ($key eq "<" && $file_window_width < $term_w-1) {
+ $file_window_width++;
+ recalc_windows;
+ } elsif ($key eq ">" && $file_window_width > 1) {
+ $file_window_width--;
+ recalc_windows;
+ } else {
+ $focused_window->key($key);
+ }
+}
+
+### Window Objects ###
+
+package VS::Window;
+
+sub new($) {
+ my $w = {
+ "visible" => 1,
+ "focusable" => 1,
+ "focused" => 0,
+ "x" => -1,
+ "y" => -1,
+ "w" => -1,
+ "h" => -1
+ };
+ push @window_list, $w;
+ return bless $w;
+}
+
+sub place($$$$$) {
+ my ($w,$nx,$ny,$nh,$nw) = @_;
+ if ($w->{"visible"}) {
+ if (!defined $w->{"win"} || $w->{"x"} != $nx || $w->{"y"} != $ny
+ || $w->{"w"} != $nw || $w->{"h"} != $nh) {
+ $w->{"win"} = $W->subwin($nh,$nw,$nx,$ny);
+ $w->{"x"} = $nx;
+ $w->{"y"} = $ny;
+ $w->{"w"} = $nw;
+ $w->{"h"} = $nh;
+ $w->redraw;
+ }
+ } else {
+ delete $w->{"win"};
+ }
+}
+
+sub key($) { }
+sub redraw($) { }
+
+package VS::Window::Main;
+use Curses;
+BEGIN { our @ISA = qw(VS::Window); }
+
+sub new($) {
+ my $w = new VS::Window;
+ $w->{"file"} = "";
+ $w->{"attrs"} = {};
+ $w->{"lines"} = ["", "", " The Virtual Songbook 0.0\n", " (c) 2003 Martin Mares <mj\@ucw.cz>"];
+ $w->{"linetype"} = [0,0,0,0];
+ $w->{"n"} = 4;
+ $w->{"top"} = 0;
+ return bless $w;
+}
+
+sub view($$$) {
+ my ($w,$f,$x) = @_;
+ if ($w->{"file"} ne $f) {
+ $w->{"file"} = $f;
+ $w->{"xfile"} = $x;
+ $f =~ s@^./@@;
+ $x =~ s@^./@@;
+ if (open X, $f) {
+ my %attrs = ();
+ while (<X>) {
+ chomp;
+ /^$/ && last;
+ if (/^(\w+):\s*(.*)/ && !defined $attrs{$1}) {
+ $attrs{$1} = $2;
+ }
+ }
+ my @lines = ();
+ my @types = ();
+ while (<X>) {
+ chomp;
+ if (s/^!//) { push @types, 1; } else { push @types, 0; }
+ push @lines, $_;
+ }
+ close X;
+ $w->{"attrs"} = \%attrs;
+ $w->{"lines"} = \@lines;
+ $w->{"linetypes"} = \@types;
+ $w->{"top"} = 0;
+ $w->{"n"} = scalar @lines;
+ $w->redraw;
+ if (defined $attrs{"Name"}) {
+ $x = $attrs{"Name"};
+ $x = $attrs{"Author"} . ": $x" if defined $attrs{"Author"};
+ }
+ $status_window->tell($x);
+ } else {
+ $status_window->tell("Cannot open $f");
+ }
+ }
+}
+
+sub redraw_line($$) {
+ my ($w,$i) = @_;
+ my $win = $w->{"win"};
+ my $l = $w->{"lines"}->[$i];
+ if ($w->{"linetypes"}->[$i]) { $win->attrset($attr_chord); }
+ if (length $l < $w->{"w"}) {
+ $win->addstr($i-$w->{"top"}, 0, $l);
+ $win->clrtoeol;
+ } else {
+ $win->addstr($i-$w->{"top"}, 0, substr($l, 0, $w->{"w"}));
+ }
+ $win->attrset($attr_normal);
+}
+
+sub redraw($) {
+ my $w = shift @_;
+ my $win = $w->{"win"};
+ my $top = $w->{"top"};
+ my $cnt = $w->{"n"} - $w->{"top"};
+ if ($cnt > $w->{"h"}) { $cnt = $w->{"h"}; }
+ for (my $i=$top; $i<$top+$cnt; $i++) { $w->redraw_line($i); }
+ if ($cnt < $w->{"h"}) {
+ $win->move($cnt, 0);
+ $win->clrtobot;
+ }
+ $win->noutrefresh;
+}
+
+sub go($$) {
+ my ($w,$delta) = @_;
+ my $win = $w->{"win"};
+ my $top = $w->{"top"} + $delta;
+ if ($top + $w->{"h"} > $w->{"n"}) { $top = $w->{"n"} - $w->{"h"}; }
+ if ($top < 0) { $top = 0; }
+ my $otop = $w->{"top"};
+ $w->{"top"} = $top;
+ if ($top < $otop - $w->{"h"}/2) {
+ $w->redraw;
+ } elsif ($top < $otop) {
+ my $j = $otop - $top;
+ $win->scrollok(1);
+ $win->scrl(-$j);
+ $win->scrollok(0);
+ for (my $i=0; $i<$j; $i++) { $w->redraw_line($top+$i); }
+ } elsif ($top == $otop) {
+ # Nothing happens
+ } elsif ($top < $otop + $w->{"h"}/2) {
+ my $j = $top - $otop;
+ $win->scrollok(1);
+ $win->scrl($j);
+ $win->scrollok(0);
+ for (my $i=$j; $i>0; $i--) { $w->redraw_line($top+$w->{"h"}-$i); }
+ } else {
+ $w->redraw;
+ }
+ $win->noutrefresh;
+}
+
+sub key($$) {
+ my ($w,$key) = @_;
+ if ($key eq KEY_UP) { $w->go(-1); }
+ elsif ($key eq KEY_DOWN) { $w->go(1); }
+ elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
+ elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
+ elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
+ elsif ($key eq KEY_END) { $w->go(1000000000); }
+ else { $status_window->tell("Unknown key <$key>"); }
+}
+
+package VS::Window::File;
+use Curses;
+BEGIN { our @ISA = qw(VS::Window); }
+
+sub new($) {
+ my $w = new VS::Window;
+ $w->{"dir"} = "./";
+ $w->{"xdir"} = "./";
+ return bless $w;
+}
+
+sub reload($) {
+ my $w = shift;
+ my $p = $w->{"dir"};
+ my @l = `cd $p && ls`;
+ my @fn = ();
+ my @full = ();
+ if ($p ne "./") { push @fn, "../"; push @full, "<parent>"; }
+ foreach my $x (@l) {
+ chomp $x;
+ if (-f "$p/$x") {
+ push @fn, $x;
+ my $fullname = $x;
+ if ($try_full_names && open(X, "$p/$x")) {
+ while (<X>) {
+ chomp;
+ /^$/ && last;
+ if (/^Name:\s*(.*)/) {
+ $fullname = $1;
+ last;
+ }
+ }
+ close X;
+ }
+ push @full, $fullname;
+ } elsif (-d "$p/$x") { push @fn, "$x/"; push @full, "$x/"; }
+ }
+ $w->{"flist"} = \@fn;
+ $w->{"list"} = \@full;
+ $w->{"n"} = scalar @fn;
+ $w->{"i"} = 0;
+ $w->{"1st"} = 0;
+}
+
+sub redraw_line($$) {
+ my ($w,$i) = @_;
+ my $line = $i - $w->{"1st"};
+ if ($line < 0 || $line >= $w->{"h"}) { return; }
+ my $win = $w->{"win"};
+ my $item = ($i < $w->{"n"}) ? substr($w->{"list"}->[$i], 0, $w->{"w"}) : "";
+ if ($i == $w->{"i"}) { $win->bkgdset($attr_status); }
+ $win->addstr($line, 0, $item);
+ $win->clrtoeol if length $item < $w->{"w"};
+ $win->bkgdset($attr_normal);
+}
+
+sub redraw($) {
+ my $w = shift @_;
+ $w->{"win"}->idlok(1);
+ for (my $i=0; $i<$w->{"h"}; $i++) {
+ $w->redraw_line($w->{"1st"} + $i);
+ }
+ $w->{"win"}->noutrefresh;
+}
+
+sub go($$) {
+ my ($w,$delta) = @_;
+ my $i = $w->{"i"};
+ my $oldi = $i;
+ $i += $delta;
+ if ($i < 0) { $i = 0; }
+ if ($i >= $w->{"n"}) { $i = $w->{"n"}-1; }
+ $w->{"i"} = $i;
+ if ($w->{"visible"}) {
+ $w->redraw_line($oldi);
+ if ($i < $w->{"1st"}) {
+ my $j = $w->{"1st"} - $i;
+ $w->{"1st"} = $i;
+ if ($j >= $w->{"h"}/2) {
+ $w->{"win"}->scrollok(1);
+ $w->{"win"}->scrl(-$j);
+ $w->{"win"}->scrollok(0);
+ for (my $k=0; $k<$j; $k++) { $w->redraw_line($i+$k); }
+ } else { $w->redraw; }
+ } elsif ($i >= $w->{"1st"} + $w->{"h"}) {
+ my $j = $i - $w->{"1st"} - $w->{"h"} + 1;
+ $w->{"1st"} += $j;
+ if ($j < $w->{"h"}/2) {
+ $w->{"win"}->scrollok(1);
+ $w->{"win"}->scrl($j);
+ $w->{"win"}->scrollok(0);
+ for (my $k=1; $k<=$j; $k++) { $w->redraw_line($i-$j+$k); }
+ } else { $w->redraw; }
+ } else { $w->redraw_line($i); }
+ $w->{"win"}->noutrefresh;
+ }
+ if ($auto_enter && $i < $w->{"n"} && $w->{"flist"}->[$i] !~ /\/$/) { $w->select; }
+}
+
+sub select($) {
+ my ($w) = @_;
+ if ($w->{"i"} < $w->{"n"}) {
+ my $f = $w->{"flist"}->[$w->{"i"}];
+ my $x = $w->{"list"}->[$w->{"i"}];
+ if ($f =~ /\/$/) {
+ if ($f eq "../") {
+ $w->{"dir"} =~ s@([^/]*/)$@@;
+ my $back = $1;
+ $w->{"xdir"} =~ s@[^/]*/$@@;
+ $w->reload;
+ for (my $i=0; $i<$w->{"n"}; $i++) {
+ if ($w->{"flist"}->[$i] eq $back) {
+ $w->{"i"} = $i;
+ if ($i > $w->{"h"}/2) { $w->{"1st"} = $i - int($w->{"h"}/2); }
+ if ($w->{"1st"} + $w->{"h"} > $w->{"n"}) { $w->{"1st"} = $w->{"n"} - $w->{"h"}; }
+ if ($w->{"1st"} < 0) { $w->{"1st"} = 0; }
+ last;
+ }
+ }
+ } else {
+ $w->{"dir"} .= $f;
+ $w->{"xdir"} .= $x;
+ $w->reload;
+ }
+ $w->redraw;
+ } else {
+ $main_window->view($w->{"dir"} . $f, $w->{"xdir"} . $x);
+ }
+ }
+}
+
+sub key($$) {
+ my ($w,$key) = @_;
+ if ($key eq KEY_UP) { $w->go(-1); }
+ elsif ($key eq KEY_DOWN) { $w->go(1); }
+ elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
+ elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
+ elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
+ elsif ($key eq KEY_END) { $w->go(1000000000); }
+ elsif ($key eq "\n" || $key eq "\r" || $key eq KEY_RIGHT) { $w->select; }
+ elsif ($key eq KEY_LEFT) {
+ if ($w->{"list"}->[0] eq "<parent>") {
+ $w->{"i"} = 0;
+ $w->select;
+ }
+ }
+}
+
+package VS::Window::Status;
+BEGIN { our @ISA = qw(VS::Window); }
+
+sub new($) {
+ my $w = new VS::Window;
+ $w->{"focusable"} = 0;
+ $w->{"msg"} = "";
+ return bless $w;
+}
+
+sub redraw($) {
+ my $w = shift @_;
+ my $win = $w->{"win"};
+ $win->bkgdset($attr_status);
+ $win->addstr(0, 0, $w->{"msg"});
+ $win->clrtoeol;
+ $win->refresh;
+}
+
+sub tell($$) {
+ my ($w,$m) = @_;
+ if ($w->{"msg"} ne $m) {
+ $w->{"msg"} = $m;
+ $w->redraw;
+ }
+}