From: Martin Mares Date: Tue, 25 May 2004 10:02:30 +0000 (+0000) Subject: Split view, searching, hiding of chords, help screen, lots of minor X-Git-Url: http://mj.ucw.cz/gitweb/?a=commitdiff_plain;h=932a76a508dad736d9bf6235c68b97734debfcaa;p=vs.git Split view, searching, hiding of chords, help screen, lots of minor improvements. --- diff --git a/TODO b/TODO new file mode 100644 index 0000000..d425edf --- /dev/null +++ b/TODO @@ -0,0 +1,4 @@ +- search for song text (grep) +- sorting of file list (use locale;) +- auto-transpose :) +- chord hints window diff --git a/vs.pl b/vs.pl index 6fd0db6..34502e8 100755 --- a/vs.pl +++ b/vs.pl @@ -1,11 +1,51 @@ #!/usr/bin/perl -# The Virtual Songbook 0.0 -# (c) 2003 Martin Mares +# The Virtual Songbook +# (c) 2003--2004 Martin Mares use Curses; use strict; use warnings; +### Help ### + +my @help_message = ( + "", + " The Virtual Songbook 0.9\n", + " (c) 2003--2004 Martin Mares ", + "", + "Control keys:", + "", + " ? display this help text", + " q quit the program", + " 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:", + "", + " select next/previous item", + " go one directory up", + " go to the selected directory", + " like ", + " 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; @@ -25,7 +65,7 @@ sub cleanup_terminal() { 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) { @@ -33,9 +73,12 @@ sub setup_attrs() { $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; @@ -44,25 +87,17 @@ sub setup_attrs() { 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; @@ -72,55 +107,67 @@ sub recalc_windows() { $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") { @@ -129,8 +176,8 @@ for(;;) { $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; } } @@ -161,10 +208,10 @@ BEGIN { "G#" => 8, "Ab" => 8, "A" => 9, - "Bb" => 9, "A#" => 10, "Hb" => 10, "B" => 10, + "Bb" => 10, # common error "H" => 11, "B#" => 11, "Cb" => 11, @@ -216,7 +263,6 @@ package VS::Window; sub new($) { my $w = { "visible" => 1, - "focusable" => 1, "focused" => 0, "x" => -1, "y" => -1, @@ -244,34 +290,107 @@ sub place($$$$$) { } } -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 "]; - $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) { @@ -284,20 +403,20 @@ sub view($$$) { } } my @lines = (); - my @chords = (); + my @text_lines = (); while () { 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"}; @@ -311,17 +430,15 @@ sub view($$$) { 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"}; @@ -330,8 +447,8 @@ sub transpose($) { 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; @@ -343,7 +460,6 @@ sub redraw_line($$) { 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"}; @@ -356,6 +472,31 @@ sub redraw($) { $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"}; @@ -394,16 +535,28 @@ sub key($$) { 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"} = "./"; @@ -411,6 +564,43 @@ sub new($) { 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 () { + chomp; + /^$/ && last; + if (/^Author:\s*(.*)/) { + $full = $1; + last; + } + } + close X; + } + } + } elsif (open(X, $f)) { + while () { + 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"}; @@ -422,26 +612,18 @@ sub reload($) { chomp $x; if (-f "$p/$x") { push @fn, $x; - my $fullname = $x; - if ($try_full_names && open(X, "$p/$x")) { - while () { - 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($$) { @@ -449,8 +631,14 @@ 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); @@ -469,11 +657,9 @@ sub redraw($) { $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; @@ -503,6 +689,30 @@ sub go($$) { 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"}) { @@ -541,13 +751,60 @@ sub key($$) { 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 "") { $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; @@ -555,7 +812,6 @@ BEGIN { our @ISA = qw(VS::Window); } sub new($) { my $w = new VS::Window; - $w->{"focusable"} = 0; $w->{"msg"} = ""; return bless $w; }