3 # (c) 2003--2005 Martin Mares <mj@ucw.cz>
13 " The Virtual Songbook 0.9.1\n",
14 " (c) 2003--2005 Martin Mares <mj\@ucw.cz>",
18 " ? display this help text",
19 " q quit the program",
20 " <arrows> scroll text in the current window",
21 " [ ] enlarge/shrink the current window",
22 " f display the file list window, focus it if already displayed",
23 " s display the split view window, focus it if already displayed",
24 " m focus the main window",
25 " F S hide the file list / split view window",
26 " Ctrl-L redraw the screen",
30 " + - 0 transpose chords up/down/reset",
32 " c toggle display of chords",
36 " <up> <down> select next/previous item",
37 " <left> go one directory up",
38 " <enter> go to the selected directory",
39 " <right> like <enter>",
40 " j k h l synonyms of the arrow keys (like in vi) [works in any window]",
41 " Ctrl-R reload the list",
42 " r toggle resolving of song/directory names",
43 " / incremental search (/=next, \\=previous, ?=search over all songs)",
47 my $help_status = "The Virtual Songbook 0.9.1";
49 ### Interface with Curses ###
57 $color_mode = (has_colors && COLORS >= 8 && COLOR_PAIRS >= 8) unless defined $color_mode;
64 sub cleanup_terminal() {
68 my ($attr_normal, $attr_status, $attr_hilite, $attr_chord, $attr_search);
72 init_pair(1, COLOR_YELLOW, COLOR_BLUE);
73 $attr_status = COLOR_PAIR(1) | A_BOLD;
74 init_pair(2, COLOR_YELLOW, COLOR_BLACK);
75 $attr_chord = COLOR_PAIR(2);
76 init_pair(3, COLOR_YELLOW, COLOR_GREEN);
77 $attr_search = COLOR_PAIR(3) | A_BOLD;
79 $attr_status = A_BOLD;
81 $attr_search = A_BOLD;
83 $attr_normal = A_NORMAL;
84 $attr_hilite = A_BOLD;
87 my $try_full_names = 1;
89 my $file_window_width = 20;
90 my $split_window_height = 10;
92 my ($term_w, $term_h);
94 my $file_window = new VS::Window::File;
95 my $main_window = new VS::Window::Main;
96 my $split_window = new VS::Window::Main;
97 my $status_window = new VS::Window::Status;
98 $split_window->{"visible"} = 0;
102 sub recalc_windows() {
107 $status_window->place(0, 0, 1, $w);
108 if ($file_window->{"visible"}) {
109 my $fww = $file_window_width;
111 $W->attrset($file_window->{"focused"} ? $attr_hilite : $attr_normal);
112 $W->vline(2, $w-1, ACS_VLINE, $h-3);
113 $W->hline(1, $w, ACS_HLINE, $fww);
114 $W->hline($h-1, $w, ACS_HLINE, $fww);
115 $W->addch(1, $w-1, ACS_ULCORNER);
116 $W->addch($h-1, $w-1, ACS_LLCORNER);
117 $W->attrset($attr_normal);
118 $file_window->place(2, $w, $h-3, $fww);
120 } else { $file_window->place(0, 0, 0, 0); }
121 if ($split_window->{"visible"}) {
122 my $swh = $split_window_height;
124 $W->attrset($split_window->{"focused"} ? $attr_hilite : $attr_normal);
125 $W->hline($h-1, 0, ($split_window->{"focused"} ? "=" : ACS_HLINE), $w);
126 $W->attrset($attr_normal);
127 $split_window->place($h, 0, $swh, $w);
129 } else { $split_window->place(0, 0, 0, 0); }
130 $main_window->place(1, 0, $h-1, $w);
134 my @wins = ( $main_window, $split_window, $file_window ); # [0] is always focusable
136 while ($i <= $#wins && $wins[$i] != $focused_window) { $i++; }
138 while ($i <= $#wins && !$wins[$i]->{"visible"}) { $i++; }
139 if ($i > $#wins) { $i=0; }
145 $file_window->focus; # calls recalc_windows
146 $main_window->view_help;
149 $focused_window->show_cursor;
152 if ($focused_window->key($key)) {
153 # key handled by the window
154 } elsif ($key eq "\033" || $key eq "q") {
157 } elsif ($key eq "f") {
158 $file_window->toggle(1);
159 } elsif ($key eq "F") {
160 $file_window->toggle(0);
161 } elsif ($key eq "s") {
162 $split_window->toggle(1);
163 } elsif ($key eq "S") {
164 $split_window->toggle(0);
165 } elsif ($key eq "m") {
166 $main_window->toggle(1);
167 } elsif ($key eq "?") {
168 $main_window->view_help;
169 } elsif ($key eq "\014") {
171 } elsif ($key eq "j") {
172 $file_window->key(KEY_DOWN);
173 } elsif ($key eq "k") {
174 $file_window->key(KEY_UP);
175 } elsif ($key eq "h") {
176 $file_window->key(KEY_LEFT);
177 } elsif ($key eq "l") {
178 $file_window->key(KEY_RIGHT);
179 } elsif ($key eq "\t") {
188 # Internal representation of chords: <base-tone>:<type> (so C="0:", C#="1:", Dmi="2:mi" etc.)
189 # but usually they are accompanied by position info after a second colon
214 "Bb" => 10, # common error
220 @n2t = ( "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "B", "H" );
227 while (my ($spaces,$chord,$rest) = $r =~ /(\s*)(\S+)(.*)/) {
228 $pos += length $spaces;
229 if (my ($tone,$sh,$mod) = ($chord =~ /^([CDEFGABH](#|b|))(.*)$/)) {
231 push @l, "$k:$mod:$pos"
233 push @l, "0:?$chord:$pos";
235 $pos += length $chord;
241 sub synthesize_line($$) {
245 for (my $i=0; $i<@$l; $i++) {
246 my ($tone,$mod,$pos) = split(/:/, $l->[$i]);
247 $tone = ($tone + $xpos) % 12;
248 my $chord = $n2t[$tone] . "$mod ";
250 $result .= " " x ($pos - $pp);
254 $pp += length $chord;
259 ### Window Objects ###
272 push @window_list, $w;
277 my ($w,$nx,$ny,$nh,$nw) = @_;
278 if ($w->{"visible"}) {
279 if (!defined $w->{"win"} || $w->{"x"} != $nx || $w->{"y"} != $ny
280 || $w->{"w"} != $nw || $w->{"h"} != $nh) {
281 $w->{"win"} = $W->subwin($nh,$nw,$nx,$ny);
295 $focused_window->{"focused"} = 0 if defined $focused_window;
296 if (!$w->{"visible"}) { $w=$main_window; } # Main is always visible
297 $focused_window = $w;
306 if (!$w->{"visible"} || !$w->{"focused"}) {
311 if ($w->{"visible"}) {
318 sub key($) { return 0; }
321 sub reset_cursor($) {
322 $W->move($term_h-1, $term_w-1);
325 sub set_cursor($$$) {
329 } elsif ($x >= $w->{"h"}) {
334 } elsif ($y >= $w->{"w"}) {
337 $W->move($w->{"x"} + $x, $w->{"y"} + $y);
345 package VS::Window::Main;
347 BEGIN { our @ISA = qw(VS::Window); }
350 my $w = new VS::Window;
361 $w->{"text_lines"} = [];
362 $w->{"chords"} = undef;
363 $w->{"visible_lines"} = [];
366 $w->{"current_xpose"} = 0;
367 $w->{"requested_xpose"} = 0;
368 $w->{"chords_visible"} = 1;
373 my $l = $w->{($w->{"chords_visible"} ? "lines" : "text_lines")};
374 $w->{"visible_lines"} = $l;
381 $w->{"text_lines"} = $w->{"lines"} = \@help_message;
383 $status_window->tell($help_status);
390 if ($w->{"file"} ne $f) {
401 if (/^(\w+):\s*(.*)/ && !defined $attrs{$1}) {
410 push @text_lines, $_ unless /^!/;
413 $w->{"attrs"} = \%attrs;
414 $w->{"lines"} = \@lines;
415 $w->{"text_lines"} = \@text_lines;
420 if (defined $attrs{"Name"}) {
422 $x = $attrs{"Author"} . ": $x" if defined $attrs{"Author"};
424 $status_window->tell($x);
426 $status_window->tell("Cannot open $f");
433 if (!defined $w->{"chords"}) {
435 for (my $i=0; $i<@{$w->{"lines"}}; $i++) {
436 push @{$w->{"chords"}}, ($w->{"lines"}->[$i] =~/^!(.*)/ ? VS::Chord::parse_line($1) : undef);
439 for (my $i=0; $i<@{$w->{"lines"}}; $i++) {
440 if (defined $w->{"chords"}->[$i]) {
441 $w->{"lines"}->[$i] = "!" . VS::Chord::synthesize_line($w->{"chords"}->[$i], $w->{"requested_xpose"});
444 $w->{"current_xpose"} = $w->{"requested_xpose"};
447 sub redraw_line($$) {
449 my $win = $w->{"win"};
450 my $l = $w->{"visible_lines"}->[$i] || "";
451 if ($l =~ s/^!//) { $win->attrset($attr_chord); }
452 if (length $l < $w->{"w"}) {
453 $win->addstr($i-$w->{"top"}, 0, $l);
456 $win->addstr($i-$w->{"top"}, 0, substr($l, 0, $w->{"w"}));
458 $win->attrset($attr_normal);
463 my $win = $w->{"win"};
464 my $top = $w->{"top"};
465 my $cnt = $w->{"n"} - $w->{"top"};
466 if ($cnt > $w->{"h"}) { $cnt = $w->{"h"}; }
467 for (my $i=$top; $i<$top+$cnt; $i++) { $w->redraw_line($i); }
468 if ($cnt < $w->{"h"}) {
477 return ($w == $main_window) ? $split_window : $main_window;
480 sub update_other($) {
482 my $other = $w->other_split;
483 foreach $a ("attrs", "lines", "text_lines", "chords", "n", "top", "current_xpose", "requested_xpose") {
484 $other->{$a} = $w->{$a} if defined $w->{$a};
491 my $other = $w->other_split;
492 if ($w->{"current_xpose"} != $w->{"requested_xpose"}) {
497 $other->redraw if $other->{"visible"};
502 my $win = $w->{"win"};
503 my $top = $w->{"top"} + $delta;
504 if ($top + $w->{"h"} > $w->{"n"}) { $top = $w->{"n"} - $w->{"h"}; }
505 if ($top < 0) { $top = 0; }
506 my $otop = $w->{"top"};
508 if ($top < $otop - $w->{"h"}/2) {
510 } elsif ($top < $otop) {
511 my $j = $otop - $top;
515 for (my $i=0; $i<$j; $i++) { $w->redraw_line($top+$i); }
516 } elsif ($top == $otop) {
518 } elsif ($top < $otop + $w->{"h"}/2) {
519 my $j = $top - $otop;
523 for (my $i=$j; $i>0; $i--) { $w->redraw_line($top+$w->{"h"}-$i); }
532 if ($key eq KEY_UP) { $w->go(-1); }
533 elsif ($key eq KEY_DOWN) { $w->go(1); }
534 elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
535 elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
536 elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
537 elsif ($key eq KEY_END) { $w->go(1000000000); }
538 elsif ($key eq "+" || $key eq "=") { $w->{"requested_xpose"} = ($w->{"requested_xpose"}+1)%12; $w->sync; $status_window->redraw; }
539 elsif ($key eq "-") { $w->{"requested_xpose"} = ($w->{"requested_xpose"}+11)%12; $w->sync; $status_window->redraw; }
540 elsif ($key eq "0") { $w->{"requested_xpose"} = 0; $w->sync; $status_window->redraw; }
541 elsif ($key eq "c") { $w->{"chords_visible"} = !$w->{"chords_visible"}; $w->update_text; $w->redraw; }
542 elsif ($w == $split_window) {
543 if ($key eq "]" && $split_window_height > 1) {
544 $split_window_height--;
546 } elsif ($key eq "[" && $split_window_height < $term_h-2) {
547 $split_window_height++;
554 package VS::Window::File;
556 BEGIN { our @ISA = qw(VS::Window); }
558 our %name_cache = ();
561 my $w = new VS::Window;
567 sub lookup_full_name($) {
569 return $name_cache{$f} if exists $name_cache{$f};
570 return undef if (!$try_full_names || ($f =~ /\/$/ && $try_full_names < 2));
573 my @sub = `cd $f && ls`;
574 while (!defined($full) && @sub) {
577 if (-f "$f/$z" && open(X, "$f/$z")) {
581 if (/^Author:\s*(.*)/) {
589 } elsif (open(X, $f)) {
593 if (/^Name:\s*(.*)/) {
600 $name_cache{$f} = $full;
607 my @l = `cd $p && ls`;
610 if ($p ne "./") { push @fn, "../"; push @full, "<parent>"; }
615 push @full, (lookup_full_name("$p/$x") || $x);
616 } elsif (-d "$p/$x") {
618 push @full, (lookup_full_name("$p/$x/") || $x) . "/";
621 $w->{"flist"} = \@fn;
622 $w->{"list"} = \@full;
623 $w->{"n"} = scalar @fn;
626 $w->{"search"} = undef;
627 $w->{"search-results"} = 0;
630 sub redraw_line($$) {
632 my $line = $i - $w->{"1st"};
633 if ($line < 0 || $line >= $w->{"h"}) { return; }
634 my $win = $w->{"win"};
635 my $item = ($i < $w->{"n"}) ? $w->{"list"}->[$i] : "";
636 if ($i == $w->{"i"}) {
637 if (defined $w->{"search"}) {
638 $win->bkgdset($attr_search);
639 substr($item, 0, length $w->{"search"}) = $w->{"search"};
640 } else { $win->bkgdset($attr_status); }
642 $item = substr($item, 0, $w->{"w"});
643 $win->addstr($line, 0, $item);
644 $win->clrtoeol if length $item < $w->{"w"};
645 $win->bkgdset($attr_normal);
650 my $win = $w->{"win"};
651 # Window size might have changed...
652 if ($w->{"1st"} + $w->{"h"} > $w->{"n"}) { $w->{"1st"} = $w->{"n"} - $w->{"h"}; }
653 if ($w->{"1st"} < 0) { $w->{"1st"} = 0; }
655 for (my $i=0; $i<$w->{"h"}; $i++) {
656 $w->redraw_line($w->{"1st"} + $i);
663 my $oldi = $w->{"i"};
664 if ($i < 0) { $i = 0; }
665 if ($i >= $w->{"n"}) { $i = $w->{"n"}-1; }
667 if ($w->{"visible"}) {
668 $w->redraw_line($oldi);
669 if ($i < $w->{"1st"}) {
670 my $j = $w->{"1st"} - $i;
672 if ($j >= $w->{"h"}/2) {
673 $w->{"win"}->scrollok(1);
674 $w->{"win"}->scrl(-$j);
675 $w->{"win"}->scrollok(0);
676 for (my $k=0; $k<$j; $k++) { $w->redraw_line($i+$k); }
677 } else { $w->redraw; }
678 } elsif ($i >= $w->{"1st"} + $w->{"h"}) {
679 my $j = $i - $w->{"1st"} - $w->{"h"} + 1;
681 if ($j < $w->{"h"}/2) {
682 $w->{"win"}->scrollok(1);
683 $w->{"win"}->scrl($j);
684 $w->{"win"}->scrollok(0);
685 for (my $k=1; $k<=$j; $k++) { $w->redraw_line($i-$j+$k); }
686 } else { $w->redraw; }
687 } else { $w->redraw_line($i); }
688 $w->{"win"}->noutrefresh;
690 if ($auto_enter) { $w->select(0); }
695 $w->{"search"} = undef;
696 $w->goto($w->{"i"} + $delta);
699 sub search_next($$) {
700 my ($w,$i,$skip) = @_;
701 $i = ($i + $w->{"n"}) % $w->{"n"};
703 my $p = lc $w->{"search"};
705 my $s = lc substr($w->{"list"}->[$i], 0, length $p);
711 if ($i < 0) { $i = $w->{"n"}-1; }
712 elsif ($i >= $w->{"n"}) { $i=0; }
713 } while ($i != $start);
719 for (my $i=0; $i<$w->{"n"}; $i++) {
720 if ($w->{"flist"}->[$i] eq $name) {
722 $w->{"1st"} = $i - int($w->{"h"}/2);
729 my ($w, $explicit) = @_;
730 if ($w->{"i"} < $w->{"n"}) {
731 my $f = $w->{"flist"}->[$w->{"i"}];
732 my $x = $w->{"list"}->[$w->{"i"}];
736 $w->{"dir"} =~ s@([^/]*/)$@@;
738 $w->{"xdir"} =~ s@[^/]*/$@@;
740 $w->goto_name($back);
749 if ($w->{"search-results"} && $explicit) {
750 $f =~ m@^(.*/)([^/]+)$@ or die;
753 $w->{"xdir"} = "???"; ### FIXME
758 $main_window->view($w->{"dir"} . $f, $w->{"xdir"} . $x);
763 sub global_search() {
765 my @full = ( "<back>" );
766 my @fn = ( $w->{"dir"} );
770 my $query = $w->{"search"};
771 $query =~ tr/"'\\\`//d;
772 my @resp = `../search/songsearch -C ../search/config -i ../search/index -n 20 $query`;
776 $r =~ /^\d+\. \[\d+\] (.*)/ or next;
778 my $name = shift @resp;
779 $name =~ s/^\t// or $name = $file;
780 while (@resp && $resp[0] =~ /^\t/) {
789 $w->{"flist"} = \@fn;
790 $w->{"list"} = \@full;
791 $w->{"n"} = scalar @fn;
794 $w->{"search"} = undef;
795 $w->{"search-results"} = 1;
801 if ($key eq KEY_UP) { $w->go(-1); }
802 elsif ($key eq KEY_DOWN) { $w->go(1); }
803 elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
804 elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
805 elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
806 elsif ($key eq KEY_END) { $w->go(1000000000); }
807 elsif ($key eq KEY_RIGHT || $key eq "\r" || $key eq "\n") {
808 $w->{"search"}=undef;
811 } elsif ($key eq KEY_LEFT) {
812 if ($w->{"list"}->[0] =~ /^<(parent|back)>$/) {
816 } elsif ($key eq "\x12") {
819 } elsif ($key eq "[" && $file_window_width < $term_w-1) {
820 $file_window_width++;
822 } elsif ($key eq "]" && $file_window_width > 1) {
823 $file_window_width--;
825 } elsif (defined $w->{"search"}) {
826 if ($key eq "\033") {
828 } elsif ($key eq "/") {
829 $w->search_next($w->{"i"}+1, 1);
830 } elsif ($key eq "\\") {
831 $w->search_next($w->{"i"}-1, -1);
832 } elsif ($key eq "?") {
834 } elsif ($key eq KEY_BACKSPACE) {
835 if (length $w->{"search"}) {
836 $w->{"search"} =~ s/.$//;
838 } else { $w->go(0); }
839 } elsif (($key ge ' ' && $key lt "\x7f" || $key ge "\xa0" && $key le "\xff") && length $key == 1) {
840 $w->{"search"} .= $key;
841 $w->search_next($w->{"i"}, 1);
845 $try_full_names = ($try_full_names+1) % 3;
849 } elsif ($key eq "/") {
859 if (defined $w->{"search"}) {
860 $w->set_cursor($w->{"i"} - $w->{"1st"}, length $w->{"search"});
861 } else { $w->reset_cursor; }
864 package VS::Window::Status;
865 BEGIN { our @ISA = qw(VS::Window); }
868 my $w = new VS::Window;
875 my $win = $w->{"win"};
876 $win->bkgdset($attr_status);
877 $win->addstr(0, 0, $w->{"msg"});
880 $aux = "T=" . $main_window->{"requested_xpose"} if ($main_window->{"requested_xpose"});
881 $win->addstr(0, $w->{"w"}-length $aux, $aux) if $aux ne "";
887 if ($w->{"msg"} ne $m) {