#!/usr/bin/perl
-# The Virtual Songbook 0.0
-# (c) 2003 Martin Mares <mj@ucw.cz>
+# The Virtual Songbook
+# (c) 2003--2004 Martin Mares <mj@ucw.cz>
use Curses;
use strict;
use warnings;
+### Help ###
+
+my @help_message = (
+ "",
+ " The Virtual Songbook 0.9\n",
+ " (c) 2003--2004 Martin Mares <mj\@ucw.cz>",
+ "",
+ "Control keys:",
+ "",
+ " ? display this help text",
+ " q quit the program",
+ " <arrows> scroll text in the current window",
+ " [ ] enlarge/shrink the current window",
+ " f display the file list window, focus it if already displayed",
+ " s display the split view window, focus it if already displayed",
+ " m focus the main window",
+ " F S hide the file list / split view window",
+ " Ctrl-L redraw the screen",
+ "",
+ "Main window keys:",
+ "",
+ " + - 0 transpose chords up/down/reset",
+ " = a synonym for +",
+ " c toggle display of chords",
+ "",
+ "File list keys:",
+ "",
+ " <up> <down> select next/previous item",
+ " <left> go one directory up",
+ " <enter> go to the selected directory",
+ " <right> like <enter>",
+ " j k h l synonyms of the arrow keys (like in vi) [works in any window]",
+ " Ctrl-R reload the list",
+ " r toggle resolving of song/directory names",
+ " / incremental search (/=next, \\=previous)",
+ "",
+);
+
+my $help_status = "The Virtual Songbook 0.9";
+
### Interface with Curses ###
my $W;
endwin;
}
-my ($attr_normal, $attr_status, $attr_hilite, $attr_chord);
+my ($attr_normal, $attr_status, $attr_hilite, $attr_chord, $attr_search);
sub setup_attrs() {
if ($color_mode) {
$attr_status = COLOR_PAIR(1) | A_BOLD;
init_pair(2, COLOR_YELLOW, COLOR_BLACK);
$attr_chord = COLOR_PAIR(2);
+ init_pair(3, COLOR_YELLOW, COLOR_GREEN);
+ $attr_search = COLOR_PAIR(3) | A_BOLD;
} else {
$attr_status = A_BOLD;
$attr_chord = A_BOLD;
+ $attr_search = 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 $split_window_height = 10;
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;
+my $main_window = new VS::Window::Main;
+my $split_window = new VS::Window::Main;
+my $status_window = new VS::Window::Status;
+$split_window->{"visible"} = 0;
$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;
-}
+my $focused_window;
sub recalc_windows() {
my $w = COLS;
$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 -= $fww;
+ $W->attrset($file_window->{"focused"} ? $attr_hilite : $attr_normal);
+ $W->vline(2, $w-1, ACS_VLINE, $h-3);
+ $W->hline(1, $w, ACS_HLINE, $fww);
+ $W->hline($h-1, $w, ACS_HLINE, $fww);
+ $W->addch(1, $w-1, ACS_ULCORNER);
+ $W->addch($h-1, $w-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);
- }
+ $file_window->place(2, $w, $h-3, $fww);
+ $w--;
+ } else { $file_window->place(0, 0, 0, 0); }
+ if ($split_window->{"visible"}) {
+ my $swh = $split_window_height;
+ $h -= $swh;
+ $W->attrset($split_window->{"focused"} ? $attr_hilite : $attr_normal);
+ $W->hline($h-1, 0, ($split_window->{"focused"} ? "=" : ACS_HLINE), $w);
+ $W->attrset($attr_normal);
+ $split_window->place($h, 0, $swh, $w);
+ $h--;
+ } else { $split_window->place(0, 0, 0, 0); }
+ $main_window->place(1, 0, $h-1, $w);
}
-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;
+sub focus_next() {
+ my @wins = ( $main_window, $split_window, $file_window ); # [0] is always focusable
+ my $i = 0;
+ while ($i <= $#wins && $wins[$i] != $focused_window) { $i++; }
+ $i++;
+ while ($i <= $#wins && !$wins[$i]->{"visible"}) { $i++; }
+ if ($i > $#wins) { $i=0; }
+ $wins[$i]->focus;
}
init_terminal;
setup_attrs;
-recalc_windows;
+$file_window->focus; # calls recalc_windows
+$main_window->view_help;
for(;;) {
- $W->move($term_h-1, $term_w-1);
+ $focused_window->show_cursor;
$W->refresh;
my $key = $W->getch;
- if ($key eq "\033" || $key eq "q") {
+ if ($focused_window->key($key)) {
+ # key handled by the window
+ } elsif ($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;
+ $file_window->toggle(1);
+ } elsif ($key eq "F") {
+ $file_window->toggle(0);
+ } elsif ($key eq "s") {
+ $split_window->toggle(1);
+ } elsif ($key eq "S") {
+ $split_window->toggle(0);
+ } elsif ($key eq "m") {
+ $main_window->toggle(1);
+ } elsif ($key eq "?") {
+ $main_window->view_help;
} 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;
} elsif ($key eq "j") {
$file_window->key(KEY_DOWN);
} elsif ($key eq "k") {
$file_window->key(KEY_LEFT);
} elsif ($key eq "l") {
$file_window->key(KEY_RIGHT);
- } else {
- $focused_window->key($key);
+ } elsif ($key eq "\t") {
+ focus_next;
}
}
"G#" => 8,
"Ab" => 8,
"A" => 9,
- "Bb" => 9,
"A#" => 10,
"Hb" => 10,
"B" => 10,
+ "Bb" => 10, # common error
"H" => 11,
"B#" => 11,
"Cb" => 11,
sub new($) {
my $w = {
"visible" => 1,
- "focusable" => 1,
"focused" => 0,
"x" => -1,
"y" => -1,
}
}
-sub key($) { }
+sub focus($) {
+ my $w = shift;
+ $focused_window->{"focused"} = 0 if defined $focused_window;
+ if (!$w->{"visible"}) { $w=$main_window; } # Main is always visible
+ $focused_window = $w;
+ $w->{"focused"} = 1;
+ ::recalc_windows;
+}
+
+sub toggle($;$) {
+ my $w = shift;
+ my $vis = shift;
+ if ($vis) {
+ if (!$w->{"visible"} || !$w->{"focused"}) {
+ $w->{"visible"} = 1;
+ $w->focus;
+ }
+ } else {
+ if ($w->{"visible"}) {
+ $w->{"visible"} = 0;
+ $main_window->focus;
+ }
+ }
+}
+
+sub key($) { return 0; }
sub redraw($) { }
+sub reset_cursor($) {
+ $W->move($term_h-1, $term_w-1);
+}
+
+sub set_cursor($$$) {
+ my ($w,$x,$y) = @_;
+ if ($x < 0) {
+ $x = 0;
+ } elsif ($x >= $w->{"h"}) {
+ $x = $w->{"h"} - 1;
+ }
+ if ($y < 0) {
+ $y = 0;
+ } elsif ($y >= $w->{"w"}) {
+ $y = $w->{"w"} - 1;
+ }
+ $W->move($w->{"x"} + $x, $w->{"y"} + $y);
+}
+
+sub show_cursor($) {
+ my ($w) = @_;
+ $w->reset_cursor;
+}
+
package VS::Window::Main;
use Curses;
BEGIN { our @ISA = qw(VS::Window); }
sub new($) {
my $w = new VS::Window;
+ bless $w;
+ $w->empty;
+ return $w;
+}
+
+sub empty($) {
+ my $w = shift;
$w->{"file"} = "";
$w->{"attrs"} = {};
- $w->{"lines"} = ["", "", " The Virtual Songbook 0.0\n", " (c) 2003 Martin Mares <mj\@ucw.cz>"];
- $w->{"chords"} = [0,0,0,0];
- $w->{"n"} = 4;
+ $w->{"lines"} = [];
+ $w->{"text_lines"} = [];
+ $w->{"chords"} = undef;
+ $w->{"visible_lines"} = [];
+ $w->{"n"} = 0;
$w->{"top"} = 0;
- $w->{"chords_analysed"} = 0;
$w->{"current_xpose"} = 0;
$w->{"requested_xpose"} = 0;
- return bless $w;
+ $w->{"chords_visible"} = 1;
+}
+
+sub update_text($) {
+ my $w = shift;
+ my $l = $w->{($w->{"chords_visible"} ? "lines" : "text_lines")};
+ $w->{"visible_lines"} = $l;
+ $w->{"n"} = @$l;
+}
+
+sub view_help($) {
+ my $w = shift;
+ $w->empty;
+ $w->{"text_lines"} = $w->{"lines"} = \@help_message;
+ $w->update_text;
+ $status_window->tell($help_status);
+ $w->update_other;
+ $w->sync;
}
sub view($$$) {
my ($w,$f,$x) = @_;
if ($w->{"file"} ne $f) {
+ $w->empty;
$w->{"file"} = $f;
$w->{"xfile"} = $x;
- $w->{"current_xpose"} = 0;
- $w->{"chords_analysed"} = 0;
$f =~ s@^./@@;
$x =~ s@^./@@;
if (open X, $f) {
}
}
my @lines = ();
- my @chords = ();
+ my @text_lines = ();
while (<X>) {
chomp;
- if (s/^! //) { push @chords, 1; } else { push @chords, 0; }
push @lines, $_;
+ push @text_lines, $_ unless /^!/;
}
close X;
$w->{"attrs"} = \%attrs;
$w->{"lines"} = \@lines;
- $w->{"chords"} = \@chords;
- $w->{"chordtable"} = [];
+ $w->{"text_lines"} = \@text_lines;
$w->{"top"} = 0;
- $w->{"n"} = scalar @lines;
- $w->redraw;
+ $w->update_text;
+ $w->update_other;
+ $w->sync;
if (defined $attrs{"Name"}) {
$x = $attrs{"Name"};
$x = $attrs{"Author"} . ": $x" if defined $attrs{"Author"};
sub transpose($) {
my $w = shift @_;
- if (!$w->{"chords_analysed"}) {
- for (my $i=0; $i<$w->{"n"}; $i++) {
- if ($w->{"chords"}->[$i]) {
- $w->{"chordtable"}->[$i] = VS::Chord::parse_line($w->{"lines"}->[$i]);
- }
+ if (!defined $w->{"chords"}) {
+ $w->{"chords"} = [];
+ for (my $i=0; $i<@{$w->{"lines"}}; $i++) {
+ push @{$w->{"chords"}}, ($w->{"lines"}->[$i] =~/^!(.*)/ ? VS::Chord::parse_line($1) : undef);
}
- $w->{"chords_analysed"} = 1;
}
- for (my $i=0; $i<$w->{"n"}; $i++) {
- if ($w->{"chords"}->[$i]) {
- $w->{"lines"}->[$i] = VS::Chord::synthesize_line($w->{"chordtable"}->[$i], $w->{"requested_xpose"});
+ for (my $i=0; $i<@{$w->{"lines"}}; $i++) {
+ if (defined $w->{"chords"}->[$i]) {
+ $w->{"lines"}->[$i] = "!" . VS::Chord::synthesize_line($w->{"chords"}->[$i], $w->{"requested_xpose"});
}
}
$w->{"current_xpose"} = $w->{"requested_xpose"};
sub redraw_line($$) {
my ($w,$i) = @_;
my $win = $w->{"win"};
- my $l = $w->{"lines"}->[$i];
- if ($w->{"chords"}->[$i]) { $win->attrset($attr_chord); }
+ my $l = $w->{"visible_lines"}->[$i] || "";
+ if ($l =~ s/^!//) { $win->attrset($attr_chord); }
if (length $l < $w->{"w"}) {
$win->addstr($i-$w->{"top"}, 0, $l);
$win->clrtoeol;
sub redraw($) {
my $w = shift @_;
- $w->transpose if $w->{"current_xpose"} != $w->{"requested_xpose"};
my $win = $w->{"win"};
my $top = $w->{"top"};
my $cnt = $w->{"n"} - $w->{"top"};
$win->noutrefresh;
}
+sub other_split($) {
+ my $w = shift @_;
+ return ($w == $main_window) ? $split_window : $main_window;
+}
+
+sub update_other($) {
+ my $w = shift @_;
+ my $other = $w->other_split;
+ foreach $a ("attrs", "lines", "text_lines", "chords", "n", "top", "current_xpose", "requested_xpose") {
+ $other->{$a} = $w->{$a} if defined $w->{$a};
+ }
+ $other->update_text;
+}
+
+sub sync($) {
+ my $w = shift @_;
+ my $other = $w->other_split;
+ if ($w->{"current_xpose"} != $w->{"requested_xpose"}) {
+ $w->transpose;
+ $w->update_other;
+ }
+ $w->redraw;
+ $other->redraw if $other->{"visible"};
+}
+
sub go($$) {
my ($w,$delta) = @_;
my $win = $w->{"win"};
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 "+" || $key eq "=") { $w->{"requested_xpose"} = ($w->{"requested_xpose"}+1)%12; $status_window->redraw; $w->redraw; }
- elsif ($key eq "-") { $w->{"requested_xpose"} = ($w->{"requested_xpose"}+11)%12; $status_window->redraw; $w->redraw; }
- elsif ($key eq "0") { $w->{"requested_xpose"} = 0; $status_window->redraw; $w->redraw; }
- else { $status_window->tell("Unknown key <$key>"); }
+ elsif ($key eq "+" || $key eq "=") { $w->{"requested_xpose"} = ($w->{"requested_xpose"}+1)%12; $w->sync; $status_window->redraw; }
+ elsif ($key eq "-") { $w->{"requested_xpose"} = ($w->{"requested_xpose"}+11)%12; $w->sync; $status_window->redraw; }
+ elsif ($key eq "0") { $w->{"requested_xpose"} = 0; $w->sync; $status_window->redraw; }
+ elsif ($key eq "c") { $w->{"chords_visible"} = !$w->{"chords_visible"}; $w->update_text; $w->redraw; }
+ elsif ($w == $split_window) {
+ if ($key eq "]" && $split_window_height > 1) {
+ $split_window_height--;
+ ::recalc_windows;
+ } elsif ($key eq "[" && $split_window_height < $term_h-2) {
+ $split_window_height++;
+ ::recalc_windows;
+ } else { return 0; }
+ } else { return 0; }
+ return 1;
}
package VS::Window::File;
use Curses;
BEGIN { our @ISA = qw(VS::Window); }
+our %name_cache = ();
+
sub new($) {
my $w = new VS::Window;
$w->{"dir"} = "./";
return bless $w;
}
+sub lookup_full_name($) {
+ my $f = shift;
+ return $name_cache{$f} if exists $name_cache{$f};
+ return undef if (!$try_full_names || ($f =~ /\/$/ && $try_full_names < 2));
+ my $full;
+ if ($f =~ /\/$/) {
+ my @sub = `cd $f && ls`;
+ while (!defined($full) && @sub) {
+ my $z = shift @sub;
+ chomp $z;
+ if (-f "$f/$z" && open(X, "$f/$z")) {
+ while (<X>) {
+ chomp;
+ /^$/ && last;
+ if (/^Author:\s*(.*)/) {
+ $full = $1;
+ last;
+ }
+ }
+ close X;
+ }
+ }
+ } elsif (open(X, $f)) {
+ while (<X>) {
+ chomp;
+ /^$/ && last;
+ if (/^Name:\s*(.*)/) {
+ $full = $1;
+ last;
+ }
+ }
+ close X;
+ }
+ $name_cache{$f} = $full;
+ return $full;
+}
+
sub reload($) {
my $w = shift;
my $p = $w->{"dir"};
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/"; }
+ push @full, (lookup_full_name("$p/$x") || $x);
+ } elsif (-d "$p/$x") {
+ push @fn, "$x/";
+ push @full, (lookup_full_name("$p/$x/") || $x) . "/";
+ }
}
$w->{"flist"} = \@fn;
$w->{"list"} = \@full;
$w->{"n"} = scalar @fn;
$w->{"i"} = 0;
$w->{"1st"} = 0;
+ $w->{"search"} = undef;
}
sub redraw_line($$) {
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); }
+ my $item = ($i < $w->{"n"}) ? $w->{"list"}->[$i] : "";
+ if ($i == $w->{"i"}) {
+ if (defined $w->{"search"}) {
+ $win->bkgdset($attr_search);
+ substr($item, 0, length $w->{"search"}) = $w->{"search"};
+ } else { $win->bkgdset($attr_status); }
+ }
+ $item = substr($item, 0, $w->{"w"});
$win->addstr($line, 0, $item);
$win->clrtoeol if length $item < $w->{"w"};
$win->bkgdset($attr_normal);
$win->noutrefresh;
}
-sub go($$) {
- my ($w,$delta) = @_;
- my $i = $w->{"i"};
- my $oldi = $i;
- $i += $delta;
+sub goto($$) {
+ my ($w,$i) = @_;
+ my $oldi = $w->{"i"};
if ($i < 0) { $i = 0; }
if ($i >= $w->{"n"}) { $i = $w->{"n"}-1; }
$w->{"i"} = $i;
if ($auto_enter && $i < $w->{"n"} && $w->{"flist"}->[$i] !~ /\/$/) { $w->select; }
}
+sub go($$) {
+ my ($w,$delta) = @_;
+ $w->{"search"} = undef;
+ $w->goto($w->{"i"} + $delta);
+}
+
+sub search_next($$) {
+ my ($w,$i,$skip) = @_;
+ $i = ($i + $w->{"n"}) % $w->{"n"};
+ my $start = $i;
+ my $p = lc $w->{"search"};
+ do {
+ my $s = lc substr($w->{"list"}->[$i], 0, length $p);
+ if ($s eq $p) {
+ $w->goto($i);
+ return;
+ }
+ $i += $skip;
+ if ($i < 0) { $i = $w->{"n"}-1; }
+ elsif ($i >= $w->{"n"}) { $i=0; }
+ } while ($i != $start);
+ $w->goto($i);
+}
+
sub select($) {
my ($w) = @_;
if ($w->{"i"} < $w->{"n"}) {
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_RIGHT || $key eq "\r" || $key eq "\n") { $w->{"search"}=undef; $w->goto($w->{"i"}); $w->select; }
elsif ($key eq KEY_LEFT) {
if ($w->{"list"}->[0] eq "<parent>") {
$w->{"i"} = 0;
$w->select;
}
+ } elsif ($key eq "\x12") {
+ $w->reload;
+ $w->redraw;
+ } 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;
+ } elsif (defined $w->{"search"}) {
+ if ($key eq "\033") {
+ $w->go(0);
+ } elsif ($key eq "/") {
+ $w->search_next($w->{"i"}+1, 1);
+ } elsif ($key eq "\\") {
+ $w->search_next($w->{"i"}-1, -1);
+ } elsif ($key eq "?") {
+ return 0;
+ } elsif ($key eq KEY_BACKSPACE) {
+ if (length $w->{"search"}) {
+ $w->{"search"} =~ s/.$//;
+ $w->goto($w->{"i"});
+ } else { $w->go(0); }
+ } elsif (($key ge ' ' && $key lt "\x7f" || $key ge "\xa0" && $key le "\xff") && length $key == 1) {
+ $w->{"search"} .= $key;
+ $w->search_next($w->{"i"}, 1);
+ } else { return 0; }
+ } else {
+ if ($key eq "r") {
+ $try_full_names = ($try_full_names+1) % 3;
+ %name_cache = ();
+ $w->reload;
+ $w->redraw;
+ } elsif ($key eq "/") {
+ $w->{"search"} = "";
+ $w->goto($w->{"i"});
+ } elsif ($key eq "\n" || $key eq "\r") {
+ $w->select;
+ } else { return 0; }
}
+ return 1;
+}
+
+sub show_cursor($) {
+ my ($w) = @_;
+ if (defined $w->{"search"}) {
+ $w->set_cursor($w->{"i"} - $w->{"1st"}, length $w->{"search"});
+ } else { $w->reset_cursor; }
}
package VS::Window::Status;
sub new($) {
my $w = new VS::Window;
- $w->{"focusable"} = 0;
$w->{"msg"} = "";
return bless $w;
}