]> mj.ucw.cz Git - vs.git/blob - vs.pl
Split view, searching, hiding of chords, help screen, lots of minor
[vs.git] / vs.pl
1 #!/usr/bin/perl
2 # The Virtual Songbook
3 # (c) 2003--2004 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\n",
14         "   (c) 2003--2004 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)",
44         "",
45 );
46
47 my $help_status = "The Virtual Songbook 0.9";
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 }
628
629 sub redraw_line($$) {
630         my ($w,$i) = @_;
631         my $line = $i - $w->{"1st"};
632         if ($line < 0 || $line >= $w->{"h"}) { return; }
633         my $win = $w->{"win"};
634         my $item = ($i < $w->{"n"}) ? $w->{"list"}->[$i] : "";
635         if ($i == $w->{"i"}) {
636                 if (defined $w->{"search"}) {
637                         $win->bkgdset($attr_search);
638                         substr($item, 0, length $w->{"search"}) = $w->{"search"};
639                 } else { $win->bkgdset($attr_status); }
640         }
641         $item = substr($item, 0, $w->{"w"});
642         $win->addstr($line, 0, $item);
643         $win->clrtoeol if length $item < $w->{"w"};
644         $win->bkgdset($attr_normal);
645 }
646
647 sub redraw($) {
648         my $w = shift @_;
649         my $win = $w->{"win"};
650         # Window size might have changed...
651         if ($w->{"1st"} + $w->{"h"} > $w->{"n"}) { $w->{"1st"} = $w->{"n"} - $w->{"h"}; }
652         if ($w->{"1st"} < 0) { $w->{"1st"} = 0; }
653         $win->idlok(1);
654         for (my $i=0; $i<$w->{"h"}; $i++) {
655                 $w->redraw_line($w->{"1st"} + $i);
656         }
657         $win->noutrefresh;
658 }
659
660 sub goto($$) {
661         my ($w,$i) = @_;
662         my $oldi = $w->{"i"};
663         if ($i < 0) { $i = 0; }
664         if ($i >= $w->{"n"}) { $i = $w->{"n"}-1; }
665         $w->{"i"} = $i;
666         if ($w->{"visible"}) {
667                 $w->redraw_line($oldi);
668                 if ($i < $w->{"1st"}) {
669                         my $j = $w->{"1st"} - $i;
670                         $w->{"1st"} = $i;
671                         if ($j >= $w->{"h"}/2) {
672                                 $w->{"win"}->scrollok(1);
673                                 $w->{"win"}->scrl(-$j);
674                                 $w->{"win"}->scrollok(0);
675                                 for (my $k=0; $k<$j; $k++) { $w->redraw_line($i+$k); }
676                         } else { $w->redraw; }
677                 } elsif ($i >= $w->{"1st"} + $w->{"h"}) {
678                         my $j = $i - $w->{"1st"} - $w->{"h"} + 1;
679                         $w->{"1st"} += $j;
680                         if ($j < $w->{"h"}/2) {
681                                 $w->{"win"}->scrollok(1);
682                                 $w->{"win"}->scrl($j);
683                                 $w->{"win"}->scrollok(0);
684                                 for (my $k=1; $k<=$j; $k++) { $w->redraw_line($i-$j+$k); }
685                         } else { $w->redraw; }
686                 } else { $w->redraw_line($i); }
687                 $w->{"win"}->noutrefresh;
688         }
689         if ($auto_enter && $i < $w->{"n"} && $w->{"flist"}->[$i] !~ /\/$/) { $w->select; }
690 }
691
692 sub go($$) {
693         my ($w,$delta) = @_;
694         $w->{"search"} = undef;
695         $w->goto($w->{"i"} + $delta);
696 }
697
698 sub search_next($$) {
699         my ($w,$i,$skip) = @_;
700         $i = ($i + $w->{"n"}) % $w->{"n"};
701         my $start = $i;
702         my $p = lc $w->{"search"};
703         do {
704                 my $s = lc substr($w->{"list"}->[$i], 0, length $p);
705                 if ($s eq $p) {
706                         $w->goto($i);
707                         return;
708                 }
709                 $i += $skip;
710                 if ($i < 0) { $i = $w->{"n"}-1; }
711                 elsif ($i >= $w->{"n"}) { $i=0; }
712         } while ($i != $start);
713         $w->goto($i);
714 }
715
716 sub select($) {
717         my ($w) = @_;
718         if ($w->{"i"} < $w->{"n"}) {
719                 my $f = $w->{"flist"}->[$w->{"i"}];
720                 my $x = $w->{"list"}->[$w->{"i"}];
721                 if ($f =~ /\/$/) {
722                         if ($f eq "../") {
723                                 $w->{"dir"} =~ s@([^/]*/)$@@;
724                                 my $back = $1;
725                                 $w->{"xdir"} =~ s@[^/]*/$@@;
726                                 $w->reload;
727                                 for (my $i=0; $i<$w->{"n"}; $i++) {
728                                         if ($w->{"flist"}->[$i] eq $back) {
729                                                 $w->{"i"} = $i;
730                                                 $w->{"1st"} = $i - int($w->{"h"}/2);
731                                                 last;
732                                         }
733                                 }
734                         } else {
735                                 $w->{"dir"} .= $f;
736                                 $w->{"xdir"} .= $x;
737                                 $w->reload;
738                         }
739                         $w->redraw;
740                 } else {
741                         $main_window->view($w->{"dir"} . $f, $w->{"xdir"} . $x);
742                 }
743         }
744 }
745
746 sub key($$) {
747         my ($w,$key) = @_;
748         if ($key eq KEY_UP) { $w->go(-1); }
749         elsif ($key eq KEY_DOWN) { $w->go(1); }
750         elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
751         elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
752         elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
753         elsif ($key eq KEY_END) { $w->go(1000000000); }
754         elsif ($key eq KEY_RIGHT || $key eq "\r" || $key eq "\n") { $w->{"search"}=undef; $w->goto($w->{"i"}); $w->select; }
755         elsif ($key eq KEY_LEFT) {
756                 if ($w->{"list"}->[0] eq "<parent>") {
757                         $w->{"i"} = 0;
758                         $w->select;
759                 }
760         } elsif ($key eq "\x12") {
761                 $w->reload;
762                 $w->redraw;
763         } elsif ($key eq "[" && $file_window_width < $term_w-1) {
764                 $file_window_width++;
765                 ::recalc_windows;
766         } elsif ($key eq "]" && $file_window_width > 1) {
767                 $file_window_width--;
768                 ::recalc_windows;
769         } elsif (defined $w->{"search"}) {
770                 if ($key eq "\033") {
771                         $w->go(0);
772                 } elsif ($key eq "/") {
773                         $w->search_next($w->{"i"}+1, 1);
774                 } elsif ($key eq "\\") {
775                         $w->search_next($w->{"i"}-1, -1);
776                 } elsif ($key eq "?") {
777                         return 0;
778                 } elsif ($key eq KEY_BACKSPACE) {
779                         if (length $w->{"search"}) {
780                                 $w->{"search"} =~ s/.$//;
781                                 $w->goto($w->{"i"});
782                         } else { $w->go(0); }
783                 } elsif (($key ge ' ' && $key lt "\x7f" || $key ge "\xa0" && $key le "\xff") && length $key == 1) {
784                         $w->{"search"} .= $key;
785                         $w->search_next($w->{"i"}, 1);
786                 } else { return 0; }
787         } else {
788                 if ($key eq "r") {
789                         $try_full_names = ($try_full_names+1) % 3;
790                         %name_cache = ();
791                         $w->reload;
792                         $w->redraw;
793                 } elsif ($key eq "/") {
794                         $w->{"search"} = "";
795                         $w->goto($w->{"i"});
796                 } elsif ($key eq "\n" || $key eq "\r") {
797                         $w->select;
798                 } else { return 0; }
799         }
800         return 1;
801 }
802
803 sub show_cursor($) {
804         my ($w) = @_;
805         if (defined $w->{"search"}) {
806                 $w->set_cursor($w->{"i"} - $w->{"1st"}, length $w->{"search"});
807         } else { $w->reset_cursor; }
808 }
809
810 package VS::Window::Status;
811 BEGIN { our @ISA = qw(VS::Window); }
812
813 sub new($) {
814         my $w = new VS::Window;
815         $w->{"msg"} = "";
816         return bless $w;
817 }
818
819 sub redraw($) {
820         my $w = shift @_;
821         my $win = $w->{"win"};
822         $win->bkgdset($attr_status);
823         $win->addstr(0, 0, $w->{"msg"});
824         $win->clrtoeol;
825         my $aux = "";
826         $aux = "T=" . $main_window->{"requested_xpose"} if ($main_window->{"requested_xpose"});
827         $win->addstr(0, $w->{"w"}-length $aux, $aux) if $aux ne "";
828         $win->refresh;
829 }
830
831 sub tell($$) {
832         my ($w,$m) = @_;
833         if ($w->{"msg"} ne $m) {
834                 $w->{"msg"} = $m;
835                 $w->redraw;
836         }
837 }