]> mj.ucw.cz Git - vs.git/blob - vs.pl
Added global search (experimental).
[vs.git] / vs.pl
1 #!/usr/bin/perl
2 # The Virtual Songbook
3 # (c) 2003--2005 Martin Mares <mj@ucw.cz>
4
5 use Curses;
6 use strict;
7 use warnings;
8
9 ### Help ###
10
11 my @help_message = (
12         "",
13         "   The Virtual Songbook 0.9.1\n",
14         "   (c) 2003--2005 Martin Mares <mj\@ucw.cz>",
15         "",
16         "Control keys:",
17         "",
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",
27         "",
28         "Main window keys:",
29         "",
30         "   + - 0        transpose chords up/down/reset",
31         "   =            a synonym for +",
32         "   c            toggle display of chords",
33         "",
34         "File list keys:",
35         "",
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)",
44         "",
45 );
46
47 my $help_status = "The Virtual Songbook 0.9.1";
48
49 ### Interface with Curses ###
50
51 my $W;
52 my $color_mode;
53
54 sub init_terminal() {
55         $W = new Curses;
56         start_color;
57         $color_mode = (has_colors && COLORS >= 8 && COLOR_PAIRS >= 8) unless defined $color_mode;
58         cbreak; noecho;
59         $W->intrflush(0);
60         $W->keypad(1);
61         $W->meta(1);
62 }
63
64 sub cleanup_terminal() {
65         endwin;
66 }
67
68 my ($attr_normal, $attr_status, $attr_hilite, $attr_chord, $attr_search);
69
70 sub setup_attrs() {
71         if ($color_mode) {
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;
78         } else {
79                 $attr_status = A_BOLD;
80                 $attr_chord = A_BOLD;
81                 $attr_search = A_BOLD;
82         }
83         $attr_normal = A_NORMAL;
84         $attr_hilite = A_BOLD;
85 }
86
87 my $try_full_names = 1;
88 my $auto_enter = 1;
89 my $file_window_width = 20;
90 my $split_window_height = 10;
91
92 my ($term_w, $term_h);
93 my @window_list = ();
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;
99 $file_window->reload;
100 my $focused_window;
101
102 sub recalc_windows() {
103         my $w = COLS;
104         my $h = LINES;
105         $term_w = $w;
106         $term_h = $h;
107         $status_window->place(0, 0, 1, $w);
108         if ($file_window->{"visible"}) {
109                 my $fww = $file_window_width;
110                 $w -= $fww;
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);
119                 $w--;
120         } else { $file_window->place(0, 0, 0, 0); }
121         if ($split_window->{"visible"}) {
122                 my $swh = $split_window_height;
123                 $h -= $swh;
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);
128                 $h--;
129         } else { $split_window->place(0, 0, 0, 0); }
130         $main_window->place(1, 0, $h-1, $w);
131 }
132
133 sub focus_next() {
134         my @wins = ( $main_window, $split_window, $file_window );       # [0] is always focusable
135         my $i = 0;
136         while ($i <= $#wins && $wins[$i] != $focused_window) { $i++; }
137         $i++;
138         while ($i <= $#wins && !$wins[$i]->{"visible"}) { $i++; }
139         if ($i > $#wins) { $i=0; }
140         $wins[$i]->focus;
141 }
142
143 init_terminal;
144 setup_attrs;
145 $file_window->focus;    # calls recalc_windows
146 $main_window->view_help;
147
148 for(;;) {
149         $focused_window->show_cursor;
150         $W->refresh;
151         my $key = $W->getch;
152         if ($focused_window->key($key)) {
153                 # key handled by the window
154         } elsif ($key eq "\033" || $key eq "q") {
155                 cleanup_terminal;
156                 exit 0;
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") {
170                 $curscr->clearok(1);
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") {
180                 focus_next;
181         }
182 }
183
184 ### Chords ###
185
186 package VS::Chord;
187
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
190
191 our (%t2n, @n2t);
192
193 BEGIN {
194 %t2n = (
195         "C" => 0,
196         "C#" => 1,
197                 "Db" => 1,
198         "D" => 2,
199         "D#" => 3,
200                 "Eb" => 3,
201         "E" => 4,
202                 "Fb" => 4,
203                 "E#" => 5,
204         "F" => 5,
205         "F#" => 6,
206                 "Gb" => 6,
207         "G" => 7,
208         "G#" => 8,
209                 "Ab" => 8,
210         "A" => 9,
211         "A#" => 10,
212                 "Hb" => 10,
213                 "B" => 10,
214                 "Bb" => 10,     # common error
215         "H" => 11,
216                 "B#" => 11,
217                 "Cb" => 11,
218                 "H#" => 0
219 );
220 @n2t = ( "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "B", "H" );
221 }
222
223 sub parse_line($) {
224         my $r = shift @_;
225         my @l = ();
226         my $pos = 0;
227         while (my ($spaces,$chord,$rest) = $r =~ /(\s*)(\S+)(.*)/) {
228                 $pos += length $spaces;
229                 if (my ($tone,$sh,$mod) = ($chord =~ /^([CDEFGABH](#|b|))(.*)$/)) {
230                         my $k = $t2n{$tone};
231                         push @l, "$k:$mod:$pos"
232                 } else {
233                         push @l, "0:?$chord:$pos";
234                 }
235                 $pos += length $chord;
236                 $r = $rest;
237         }
238         return \@l;
239 }
240
241 sub synthesize_line($$) {
242         my ($l,$xpos) = @_;
243         my $pp = 0;
244         my $result = "";
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 ";
249                 if ($pp < $pos) {
250                         $result .= " " x ($pos - $pp);
251                         $pp = $pos;
252                 }
253                 $result .= $chord;
254                 $pp += length $chord;
255         }
256         return $result;
257 }
258
259 ### Window Objects ###
260
261 package VS::Window;
262
263 sub new($) {
264         my $w = {
265                 "visible" => 1,
266                 "focused" => 0,
267                 "x" => -1,
268                 "y" => -1,
269                 "w" => -1,
270                 "h" => -1
271         };
272         push @window_list, $w;
273         return bless $w;
274 }
275
276 sub place($$$$$) {
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);
282                         $w->{"x"} = $nx;
283                         $w->{"y"} = $ny;
284                         $w->{"w"} = $nw;
285                         $w->{"h"} = $nh;
286                         $w->redraw;
287                 }
288         } else {
289                 delete $w->{"win"};
290         }
291 }
292
293 sub focus($) {
294         my $w = shift;
295         $focused_window->{"focused"} = 0 if defined $focused_window;
296         if (!$w->{"visible"}) { $w=$main_window; }      # Main is always visible
297         $focused_window = $w;
298         $w->{"focused"} = 1;
299         ::recalc_windows;
300 }
301
302 sub toggle($;$) {
303         my $w = shift;
304         my $vis = shift;
305         if ($vis) {
306                 if (!$w->{"visible"} || !$w->{"focused"}) {
307                         $w->{"visible"} = 1;
308                         $w->focus;
309                 }
310         } else {
311                 if ($w->{"visible"}) {
312                         $w->{"visible"} = 0;
313                         $main_window->focus;
314                 }
315         }
316 }
317
318 sub key($) { return 0; }
319 sub redraw($) { }
320
321 sub reset_cursor($) {
322         $W->move($term_h-1, $term_w-1);
323 }
324
325 sub set_cursor($$$) {
326         my ($w,$x,$y) = @_;
327         if ($x < 0) {
328                 $x = 0;
329         } elsif ($x >= $w->{"h"}) {
330                 $x = $w->{"h"} - 1;
331         }
332         if ($y < 0) {
333                 $y = 0;
334         } elsif ($y >= $w->{"w"}) {
335                 $y = $w->{"w"} - 1;
336         }
337         $W->move($w->{"x"} + $x, $w->{"y"} + $y);
338 }
339
340 sub show_cursor($) {
341         my ($w) = @_;
342         $w->reset_cursor;
343 }
344
345 package VS::Window::Main;
346 use Curses;
347 BEGIN { our @ISA = qw(VS::Window); }
348
349 sub new($) {
350         my $w = new VS::Window;
351         bless $w;
352         $w->empty;
353         return $w;
354 }
355
356 sub empty($) {
357         my $w = shift;
358         $w->{"file"} = "";
359         $w->{"attrs"} = {};
360         $w->{"lines"} = [];
361         $w->{"text_lines"} = [];
362         $w->{"chords"} = undef;
363         $w->{"visible_lines"} = [];
364         $w->{"n"} = 0;
365         $w->{"top"} = 0;
366         $w->{"current_xpose"} = 0;
367         $w->{"requested_xpose"} = 0;
368         $w->{"chords_visible"} = 1;
369 }
370
371 sub update_text($) {
372         my $w = shift;
373         my $l = $w->{($w->{"chords_visible"} ? "lines" : "text_lines")};
374         $w->{"visible_lines"} = $l;
375         $w->{"n"} = @$l;
376 }
377
378 sub view_help($) {
379         my $w = shift;
380         $w->empty;
381         $w->{"text_lines"} = $w->{"lines"} = \@help_message;
382         $w->update_text;
383         $status_window->tell($help_status);
384         $w->update_other;
385         $w->sync;
386 }
387
388 sub view($$$) {
389         my ($w,$f,$x) = @_;
390         if ($w->{"file"} ne $f) {
391                 $w->empty;
392                 $w->{"file"} = $f;
393                 $w->{"xfile"} = $x;
394                 $f =~ s@^./@@;
395                 $x =~ s@^./@@;
396                 if (open X, $f) {
397                         my %attrs = ();
398                         while (<X>) {
399                                 chomp;
400                                 /^$/ && last;
401                                 if (/^(\w+):\s*(.*)/ && !defined $attrs{$1}) {
402                                         $attrs{$1} = $2;
403                                 }
404                         }
405                         my @lines = ();
406                         my @text_lines = ();
407                         while (<X>) {
408                                 chomp;
409                                 push @lines, $_;
410                                 push @text_lines, $_ unless /^!/;
411                         }
412                         close X;
413                         $w->{"attrs"} = \%attrs;
414                         $w->{"lines"} = \@lines;
415                         $w->{"text_lines"} = \@text_lines;
416                         $w->{"top"} = 0;
417                         $w->update_text;
418                         $w->update_other;
419                         $w->sync;
420                         if (defined $attrs{"Name"}) {
421                                 $x = $attrs{"Name"};
422                                 $x = $attrs{"Author"} . ": $x" if defined $attrs{"Author"};
423                         }
424                         $status_window->tell($x);
425                 } else {
426                         $status_window->tell("Cannot open $f");
427                 }
428         }
429 }
430
431 sub transpose($) {
432         my $w = shift @_;
433         if (!defined $w->{"chords"}) {
434                 $w->{"chords"} = [];
435                 for (my $i=0; $i<@{$w->{"lines"}}; $i++) {
436                         push @{$w->{"chords"}}, ($w->{"lines"}->[$i] =~/^!(.*)/ ? VS::Chord::parse_line($1) : undef);
437                 }
438         }
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"});
442                 }
443         }
444         $w->{"current_xpose"} = $w->{"requested_xpose"};
445 }
446
447 sub redraw_line($$) {
448         my ($w,$i) = @_;
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);
454                 $win->clrtoeol;
455         } else {
456                 $win->addstr($i-$w->{"top"}, 0, substr($l, 0, $w->{"w"}));
457         }
458         $win->attrset($attr_normal);
459 }
460
461 sub redraw($) {
462         my $w = shift @_;
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"}) {
469                 $win->move($cnt, 0);
470                 $win->clrtobot;
471         }
472         $win->noutrefresh;
473 }
474
475 sub other_split($) {
476         my $w = shift @_;
477         return ($w == $main_window) ? $split_window : $main_window;
478 }
479
480 sub update_other($) {
481         my $w = shift @_;
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};
485         }
486         $other->update_text;
487 }
488
489 sub sync($) {
490         my $w = shift @_;
491         my $other = $w->other_split;
492         if ($w->{"current_xpose"} != $w->{"requested_xpose"}) {
493                 $w->transpose;
494                 $w->update_other;
495         }
496         $w->redraw;
497         $other->redraw if $other->{"visible"};
498 }
499
500 sub go($$) {
501         my ($w,$delta) = @_;
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"};
507         $w->{"top"} = $top;
508         if ($top < $otop - $w->{"h"}/2) {
509                 $w->redraw;
510         } elsif ($top < $otop) {
511                 my $j = $otop - $top;
512                 $win->scrollok(1);
513                 $win->scrl(-$j);
514                 $win->scrollok(0);
515                 for (my $i=0; $i<$j; $i++) { $w->redraw_line($top+$i); }
516         } elsif ($top == $otop) {
517                 # Nothing happens
518         } elsif ($top < $otop + $w->{"h"}/2) {
519                 my $j = $top - $otop;
520                 $win->scrollok(1);
521                 $win->scrl($j);
522                 $win->scrollok(0);
523                 for (my $i=$j; $i>0; $i--) { $w->redraw_line($top+$w->{"h"}-$i); }
524         } else {
525                 $w->redraw;
526         }
527         $win->noutrefresh;
528 }
529
530 sub key($$) {
531         my ($w,$key) = @_;
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--;
545                         ::recalc_windows;
546                 } elsif ($key eq "[" && $split_window_height < $term_h-2) {
547                         $split_window_height++;
548                         ::recalc_windows;
549                 } else { return 0; }
550         } else { return 0; }
551         return 1;
552 }
553
554 package VS::Window::File;
555 use Curses;
556 BEGIN { our @ISA = qw(VS::Window); }
557
558 our %name_cache = ();
559
560 sub new($) {
561         my $w = new VS::Window;
562         $w->{"dir"} = "./";
563         $w->{"xdir"} = "./";
564         return bless $w;
565 }
566
567 sub lookup_full_name($) {
568         my $f = shift;
569         return $name_cache{$f} if exists $name_cache{$f};
570         return undef if (!$try_full_names || ($f =~ /\/$/ && $try_full_names < 2));
571         my $full;
572         if ($f =~ /\/$/) {
573                 my @sub = `cd $f && ls`;
574                 while (!defined($full) && @sub) {
575                         my $z = shift @sub;
576                         chomp $z;
577                         if (-f "$f/$z" && open(X, "$f/$z")) {
578                                 while (<X>) {
579                                         chomp;
580                                         /^$/ && last;
581                                         if (/^Author:\s*(.*)/) {
582                                                 $full = $1;
583                                                 last;
584                                         }
585                                 }
586                                 close X;
587                         }
588                 }
589         } elsif (open(X, $f)) {
590                 while (<X>) {
591                         chomp;
592                         /^$/ && last;
593                         if (/^Name:\s*(.*)/) {
594                                 $full = $1;
595                                 last;
596                         }
597                 }
598                 close X;
599         }
600         $name_cache{$f} = $full;
601         return $full;
602 }
603
604 sub reload($) {
605         my $w = shift;
606         my $p = $w->{"dir"};
607         my @l = `cd $p && ls`;
608         my @fn = ();
609         my @full = ();
610         if ($p ne "./") { push @fn, "../"; push @full, "<parent>"; }
611         foreach my $x (@l) {
612                 chomp $x;
613                 if (-f "$p/$x") {
614                         push @fn, $x;
615                         push @full, (lookup_full_name("$p/$x") || $x);
616                 } elsif (-d "$p/$x") {
617                         push @fn, "$x/";
618                         push @full, (lookup_full_name("$p/$x/") || $x) . "/";
619                 }
620         }
621         $w->{"flist"} = \@fn;
622         $w->{"list"} = \@full;
623         $w->{"n"} = scalar @fn;
624         $w->{"i"} = 0;
625         $w->{"1st"} = 0;
626         $w->{"search"} = undef;
627         $w->{"search-results"} = 0;
628 }
629
630 sub redraw_line($$) {
631         my ($w,$i) = @_;
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); }
641         }
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);
646 }
647
648 sub redraw($) {
649         my $w = shift @_;
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; }
654         $win->idlok(1);
655         for (my $i=0; $i<$w->{"h"}; $i++) {
656                 $w->redraw_line($w->{"1st"} + $i);
657         }
658         $win->noutrefresh;
659 }
660
661 sub goto($$) {
662         my ($w,$i) = @_;
663         my $oldi = $w->{"i"};
664         if ($i < 0) { $i = 0; }
665         if ($i >= $w->{"n"}) { $i = $w->{"n"}-1; }
666         $w->{"i"} = $i;
667         if ($w->{"visible"}) {
668                 $w->redraw_line($oldi);
669                 if ($i < $w->{"1st"}) {
670                         my $j = $w->{"1st"} - $i;
671                         $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;
680                         $w->{"1st"} += $j;
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;
689         }
690         if ($auto_enter) { $w->select(0); }
691 }
692
693 sub go($$) {
694         my ($w,$delta) = @_;
695         $w->{"search"} = undef;
696         $w->goto($w->{"i"} + $delta);
697 }
698
699 sub search_next($$) {
700         my ($w,$i,$skip) = @_;
701         $i = ($i + $w->{"n"}) % $w->{"n"};
702         my $start = $i;
703         my $p = lc $w->{"search"};
704         do {
705                 my $s = lc substr($w->{"list"}->[$i], 0, length $p);
706                 if ($s eq $p) {
707                         $w->goto($i);
708                         return;
709                 }
710                 $i += $skip;
711                 if ($i < 0) { $i = $w->{"n"}-1; }
712                 elsif ($i >= $w->{"n"}) { $i=0; }
713         } while ($i != $start);
714         $w->goto($i);
715 }
716
717 sub goto_name($$) {
718         my ($w, $name) = @_;
719         for (my $i=0; $i<$w->{"n"}; $i++) {
720         if ($w->{"flist"}->[$i] eq $name) {
721                         $w->{"i"} = $i;
722                         $w->{"1st"} = $i - int($w->{"h"}/2);
723                         last;
724                 }
725         }
726 }
727
728 sub select($$) {
729         my ($w, $explicit) = @_;
730         if ($w->{"i"} < $w->{"n"}) {
731                 my $f = $w->{"flist"}->[$w->{"i"}];
732                 my $x = $w->{"list"}->[$w->{"i"}];
733                 if ($f =~ /\/$/) {
734                         if ($explicit) {
735                                 if ($f eq "../") {
736                                         $w->{"dir"} =~ s@([^/]*/)$@@;
737                                         my $back = $1;
738                                         $w->{"xdir"} =~ s@[^/]*/$@@;
739                                         $w->reload;
740                                         $w->goto_name($back);
741                                 } else {
742                                         $w->{"dir"} .= $f;
743                                         $w->{"xdir"} .= $x;
744                                         $w->reload;
745                                 }
746                                 $w->redraw;
747                         }
748                 } else {
749                         if ($w->{"search-results"} && $explicit) {
750                                 $f =~ m@^(.*/)([^/]+)$@ or die;
751                                 $w->{"dir"} = $1;
752                                 $f = $2;
753                                 $w->{"xdir"} = "???";           ### FIXME
754                                 $w->reload;
755                                 $w->goto_name($f);
756                                 $w->redraw;
757                         }
758                         $main_window->view($w->{"dir"} . $f, $w->{"xdir"} . $x);
759                 }
760         }
761 }
762
763 sub global_search() {
764         my ($w) = @_;
765         my @full = ( "<back>" );
766         my @fn = ( $w->{"dir"} );
767         $w->{"dir"} = "./";
768         $w->{"xdir"} = "./";
769
770         my $query = $w->{"search"};
771         $query =~ tr/"'\\\`//d;
772         my @resp = `../search/songsearch -C ../search/config -i ../search/index -n 20 $query`;
773         chomp @resp;
774         while (@resp) {
775                 my $r = shift @resp;
776                 $r =~ /^\d+\. \[\d+\] (.*)/ or next;
777                 my $file = $1;
778                 my $name = shift @resp;
779                 $name =~ s/^\t// or $name = $file;
780                 while (@resp && $resp[0] =~ /^\t/) {
781                         $r = shift @resp;
782                         $r =~ s/^\t//;
783                         $name .= " ($r)";
784                 }
785                 push @fn, $file;
786                 push @full, $name;
787         }
788
789         $w->{"flist"} = \@fn;
790         $w->{"list"} = \@full;
791         $w->{"n"} = scalar @fn;
792         $w->{"i"} = 0;
793         $w->{"1st"} = 0;
794         $w->{"search"} = undef;
795         $w->{"search-results"} = 1;
796         $w->redraw;
797 }
798
799 sub key($$) {
800         my ($w,$key) = @_;
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;
809                 $w->goto($w->{"i"});
810                 $w->select(1);
811         } elsif ($key eq KEY_LEFT) {
812                 if ($w->{"list"}->[0] =~ /^<(parent|back)>$/) {
813                         $w->{"i"} = 0;
814                         $w->select(1);
815                 }
816         } elsif ($key eq "\x12") {
817                 $w->reload;
818                 $w->redraw;
819         } elsif ($key eq "[" && $file_window_width < $term_w-1) {
820                 $file_window_width++;
821                 ::recalc_windows;
822         } elsif ($key eq "]" && $file_window_width > 1) {
823                 $file_window_width--;
824                 ::recalc_windows;
825         } elsif (defined $w->{"search"}) {
826                 if ($key eq "\033") {
827                         $w->go(0);
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 "?") {
833                         $w->global_search;
834                 } elsif ($key eq KEY_BACKSPACE) {
835                         if (length $w->{"search"}) {
836                                 $w->{"search"} =~ s/.$//;
837                                 $w->goto($w->{"i"});
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);
842                 } else { return 0; }
843         } else {
844                 if ($key eq "r") {
845                         $try_full_names = ($try_full_names+1) % 3;
846                         %name_cache = ();
847                         $w->reload;
848                         $w->redraw;
849                 } elsif ($key eq "/") {
850                         $w->{"search"} = "";
851                         $w->goto($w->{"i"});
852                 } else { return 0; }
853         }
854         return 1;
855 }
856
857 sub show_cursor($) {
858         my ($w) = @_;
859         if (defined $w->{"search"}) {
860                 $w->set_cursor($w->{"i"} - $w->{"1st"}, length $w->{"search"});
861         } else { $w->reset_cursor; }
862 }
863
864 package VS::Window::Status;
865 BEGIN { our @ISA = qw(VS::Window); }
866
867 sub new($) {
868         my $w = new VS::Window;
869         $w->{"msg"} = "";
870         return bless $w;
871 }
872
873 sub redraw($) {
874         my $w = shift @_;
875         my $win = $w->{"win"};
876         $win->bkgdset($attr_status);
877         $win->addstr(0, 0, $w->{"msg"});
878         $win->clrtoeol;
879         my $aux = "";
880         $aux = "T=" . $main_window->{"requested_xpose"} if ($main_window->{"requested_xpose"});
881         $win->addstr(0, $w->{"w"}-length $aux, $aux) if $aux ne "";
882         $win->refresh;
883 }
884
885 sub tell($$) {
886         my ($w,$m) = @_;
887         if ($w->{"msg"} ne $m) {
888                 $w->{"msg"} = $m;
889                 $w->redraw;
890         }
891 }