]> mj.ucw.cz Git - vs.git/blob - vs.pl
...
[vs.git] / vs.pl
1 #!/usr/bin/perl
2 # The Virtual Songbook 0.0
3 # (c) 2003 Martin Mares <mj@ucw.cz>
4
5 use Curses;
6 use strict;
7 use warnings;
8
9 ### Interface with Curses ###
10
11 my $W;
12 my $color_mode;
13
14 sub init_terminal() {
15         $W = new Curses;
16         start_color;
17         $color_mode = (has_colors && COLORS >= 8 && COLOR_PAIRS >= 8) unless defined $color_mode;
18         cbreak; noecho;
19         $W->intrflush(0);
20         $W->keypad(1);
21         $W->meta(1);
22 }
23
24 sub cleanup_terminal() {
25         endwin;
26 }
27
28 my ($attr_normal, $attr_status, $attr_hilite, $attr_chord);
29
30 sub setup_attrs() {
31         if ($color_mode) {
32                 init_pair(1, COLOR_YELLOW, COLOR_BLUE);
33                 $attr_status = COLOR_PAIR(1) | A_BOLD;
34                 init_pair(2, COLOR_YELLOW, COLOR_BLACK);
35                 $attr_chord = COLOR_PAIR(2);
36         } else {
37                 $attr_status = A_BOLD;
38                 $attr_chord = A_BOLD;
39         }
40         $attr_normal = A_NORMAL;
41         $attr_hilite = A_BOLD;
42 }
43
44 my $try_full_names = 1;
45 my $auto_enter = 1;
46 my $file_window_width = 20;
47
48 my ($term_w, $term_h);
49 my @window_list = ();
50 my $status_window = new VS::Window::Status;
51 my $main_window = new VS::Window::Main;
52 my $file_window = new VS::Window::File;
53 $file_window->reload;
54 my $focused_window_i = 2;
55 my $focused_window = $file_window;
56
57 sub focus_next() {
58         $focused_window->{"focused"} = 0;
59         do {
60                 $focused_window_i++;
61                 if ($focused_window_i > $#window_list) { $focused_window_i=0; }
62                 $focused_window = $window_list[$focused_window_i];
63         } while (!$focused_window->{"focusable"} || !$focused_window->{"visible"});
64         $focused_window->{"focused"} = 1;
65 }
66
67 sub recalc_windows() {
68         my $w = COLS;
69         my $h = LINES;
70         $term_w = $w;
71         $term_h = $h;
72         $status_window->place(0, 0, 1, $w);
73         if ($file_window->{"visible"}) {
74                 my $fww = $file_window_width;
75                 $main_window->place(1, 0, $h-1, $w-$fww-1);
76                 $W->attrset($focused_window == $file_window ? $attr_hilite : $attr_normal);
77                 $W->vline(2, $w-$fww-1, ACS_VLINE, $h-3);
78                 $W->hline(1, $w-$fww, ACS_HLINE, $fww);
79                 $W->hline($h-1, $w-$fww, ACS_HLINE, $fww);
80                 $W->addch(1, $w-$fww-1, ACS_ULCORNER);
81                 $W->addch($h-1, $w-$fww-1, ACS_LLCORNER);
82                 $W->attrset($attr_normal);
83                 $file_window->place(2, $w-$fww, $h-3, $fww);
84         } else {
85                 $main_window->place(1, 0, $h-1, $w);
86                 $file_window->place(0, 0, 0, 0);
87         }
88 }
89
90 sub toggle_window($) {
91         my $win = shift;
92         if ($win->{"visible"} = !$win->{"visible"}) {
93                 while ($focused_window != $win) { focus_next; }
94         } else {
95                 if ($focused_window == $win) { focus_next; }
96         }
97         recalc_windows;
98 }
99
100 init_terminal;
101 setup_attrs;
102 recalc_windows;
103
104 for(;;) {
105         $W->move($term_h-1, $term_w-1);
106         $W->refresh;
107         my $key = $W->getch;
108         if ($key eq "\033" || $key eq "q") {
109                 cleanup_terminal;
110                 exit 0;
111         } elsif ($key eq "f") {
112                 toggle_window($file_window);
113         } elsif ($key eq "\t") {
114                 focus_next;
115                 recalc_windows;
116         } elsif ($key eq "\014") {
117                 $curscr->clearok(1);
118         } elsif ($key eq "<" && $file_window_width < $term_w-1) {
119                 $file_window_width++;
120                 recalc_windows;
121         } elsif ($key eq ">" && $file_window_width > 1) {
122                 $file_window_width--;
123                 recalc_windows;
124         } elsif ($key eq "j") {
125                 $file_window->key(KEY_DOWN);
126         } elsif ($key eq "k") {
127                 $file_window->key(KEY_UP);
128         } elsif ($key eq "h") {
129                 $file_window->key(KEY_LEFT);
130         } elsif ($key eq "l") {
131                 $file_window->key(KEY_RIGHT);
132         } else {
133                 $focused_window->key($key);
134         }
135 }
136
137 ### Window Objects ###
138
139 package VS::Window;
140
141 sub new($) {
142         my $w = {
143                 "visible" => 1,
144                 "focusable" => 1,
145                 "focused" => 0,
146                 "x" => -1,
147                 "y" => -1,
148                 "w" => -1,
149                 "h" => -1
150         };
151         push @window_list, $w;
152         return bless $w;
153 }
154
155 sub place($$$$$) {
156         my ($w,$nx,$ny,$nh,$nw) = @_;
157         if ($w->{"visible"}) {
158                 if (!defined $w->{"win"} || $w->{"x"} != $nx || $w->{"y"} != $ny
159                  || $w->{"w"} != $nw || $w->{"h"} != $nh) {
160                         $w->{"win"} = $W->subwin($nh,$nw,$nx,$ny);
161                         $w->{"x"} = $nx;
162                         $w->{"y"} = $ny;
163                         $w->{"w"} = $nw;
164                         $w->{"h"} = $nh;
165                         $w->redraw;
166                 }
167         } else {
168                 delete $w->{"win"};
169         }
170 }
171
172 sub key($) { }
173 sub redraw($) { }
174
175 package VS::Window::Main;
176 use Curses;
177 BEGIN { our @ISA = qw(VS::Window); }
178
179 sub new($) {
180         my $w = new VS::Window;
181         $w->{"file"} = "";
182         $w->{"attrs"} = {};
183         $w->{"lines"} = ["", "", "   The Virtual Songbook 0.0\n", "   (c) 2003 Martin Mares <mj\@ucw.cz>"];
184         $w->{"linetype"} = [0,0,0,0];
185         $w->{"n"} = 4;
186         $w->{"top"} = 0;
187         return bless $w;
188 }
189
190 sub view($$$) {
191         my ($w,$f,$x) = @_;
192         if ($w->{"file"} ne $f) {
193                 $w->{"file"} = $f;
194                 $w->{"xfile"} = $x;
195                 $f =~ s@^./@@;
196                 $x =~ s@^./@@;
197                 if (open X, $f) {
198                         my %attrs = ();
199                         while (<X>) {
200                                 chomp;
201                                 /^$/ && last;
202                                 if (/^(\w+):\s*(.*)/ && !defined $attrs{$1}) {
203                                         $attrs{$1} = $2;
204                                 }
205                         }
206                         my @lines = ();
207                         my @types = ();
208                         while (<X>) {
209                                 chomp;
210                                 if (s/^!//) { push @types, 1; } else { push @types, 0; }
211                                 push @lines, $_;
212                         }
213                         close X;
214                         $w->{"attrs"} = \%attrs;
215                         $w->{"lines"} = \@lines;
216                         $w->{"linetypes"} = \@types;
217                         $w->{"top"} = 0;
218                         $w->{"n"} = scalar @lines;
219                         $w->redraw;
220                         if (defined $attrs{"Name"}) {
221                                 $x = $attrs{"Name"};
222                                 $x = $attrs{"Author"} . ": $x" if defined $attrs{"Author"};
223                         }
224                         $status_window->tell($x);
225                 } else {
226                         $status_window->tell("Cannot open $f");
227                 }
228         }
229 }
230
231 sub redraw_line($$) {
232         my ($w,$i) = @_;
233         my $win = $w->{"win"};
234         my $l = $w->{"lines"}->[$i];
235         if ($w->{"linetypes"}->[$i]) { $win->attrset($attr_chord); }
236         if (length $l < $w->{"w"}) {
237                 $win->addstr($i-$w->{"top"}, 0, $l);
238                 $win->clrtoeol;
239         } else {
240                 $win->addstr($i-$w->{"top"}, 0, substr($l, 0, $w->{"w"}));
241         }
242         $win->attrset($attr_normal);
243 }
244
245 sub redraw($) {
246         my $w = shift @_;
247         my $win = $w->{"win"};
248         my $top = $w->{"top"};
249         my $cnt = $w->{"n"} - $w->{"top"};
250         if ($cnt > $w->{"h"}) { $cnt = $w->{"h"}; }
251         for (my $i=$top; $i<$top+$cnt; $i++) { $w->redraw_line($i); }
252         if ($cnt < $w->{"h"}) {
253                 $win->move($cnt, 0);
254                 $win->clrtobot;
255         }
256         $win->noutrefresh;
257 }
258
259 sub go($$) {
260         my ($w,$delta) = @_;
261         my $win = $w->{"win"};
262         my $top = $w->{"top"} + $delta;
263         if ($top + $w->{"h"} > $w->{"n"}) { $top = $w->{"n"} - $w->{"h"}; }
264         if ($top < 0) { $top = 0; }
265         my $otop = $w->{"top"};
266         $w->{"top"} = $top;
267         if ($top < $otop - $w->{"h"}/2) {
268                 $w->redraw;
269         } elsif ($top < $otop) {
270                 my $j = $otop - $top;
271                 $win->scrollok(1);
272                 $win->scrl(-$j);
273                 $win->scrollok(0);
274                 for (my $i=0; $i<$j; $i++) { $w->redraw_line($top+$i); }
275         } elsif ($top == $otop) {
276                 # Nothing happens
277         } elsif ($top < $otop + $w->{"h"}/2) {
278                 my $j = $top - $otop;
279                 $win->scrollok(1);
280                 $win->scrl($j);
281                 $win->scrollok(0);
282                 for (my $i=$j; $i>0; $i--) { $w->redraw_line($top+$w->{"h"}-$i); }
283         } else {
284                 $w->redraw;
285         }
286         $win->noutrefresh;
287 }
288
289 sub key($$) {
290         my ($w,$key) = @_;
291         if ($key eq KEY_UP) { $w->go(-1); }
292         elsif ($key eq KEY_DOWN) { $w->go(1); }
293         elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
294         elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
295         elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
296         elsif ($key eq KEY_END) { $w->go(1000000000); }
297         else { $status_window->tell("Unknown key <$key>"); }
298 }
299
300 package VS::Window::File;
301 use Curses;
302 BEGIN { our @ISA = qw(VS::Window); }
303
304 sub new($) {
305         my $w = new VS::Window;
306         $w->{"dir"} = "./";
307         $w->{"xdir"} = "./";
308         return bless $w;
309 }
310
311 sub reload($) {
312         my $w = shift;
313         my $p = $w->{"dir"};
314         my @l = `cd $p && ls`;
315         my @fn = ();
316         my @full = ();
317         if ($p ne "./") { push @fn, "../"; push @full, "<parent>"; }
318         foreach my $x (@l) {
319                 chomp $x;
320                 if (-f "$p/$x") {
321                         push @fn, $x;
322                         my $fullname = $x;
323                         if ($try_full_names && open(X, "$p/$x")) {
324                                 while (<X>) {
325                                         chomp;
326                                         /^$/ && last;
327                                         if (/^Name:\s*(.*)/) {
328                                                 $fullname = $1;
329                                                 last;
330                                         }
331                                 }
332                                 close X;
333                         }
334                         push @full, $fullname;
335                 } elsif (-d "$p/$x") { push @fn, "$x/"; push @full, "$x/"; }
336         }
337         $w->{"flist"} = \@fn;
338         $w->{"list"} = \@full;
339         $w->{"n"} = scalar @fn;
340         $w->{"i"} = 0;
341         $w->{"1st"} = 0;
342 }
343
344 sub redraw_line($$) {
345         my ($w,$i) = @_;
346         my $line = $i - $w->{"1st"};
347         if ($line < 0 || $line >= $w->{"h"}) { return; }
348         my $win = $w->{"win"};
349         my $item = ($i < $w->{"n"}) ? substr($w->{"list"}->[$i], 0, $w->{"w"}) : "";
350         if ($i == $w->{"i"}) { $win->bkgdset($attr_status); }
351         $win->addstr($line, 0, $item);
352         $win->clrtoeol if length $item < $w->{"w"};
353         $win->bkgdset($attr_normal);
354 }
355
356 sub redraw($) {
357         my $w = shift @_;
358         my $win = $w->{"win"};
359         # Window size might have changed...
360         if ($w->{"1st"} + $w->{"h"} > $w->{"n"}) { $w->{"1st"} = $w->{"n"} - $w->{"h"}; }
361         if ($w->{"1st"} < 0) { $w->{"1st"} = 0; }
362         $win->idlok(1);
363         for (my $i=0; $i<$w->{"h"}; $i++) {
364                 $w->redraw_line($w->{"1st"} + $i);
365         }
366         $win->noutrefresh;
367 }
368
369 sub go($$) {
370         my ($w,$delta) = @_;
371         my $i = $w->{"i"};
372         my $oldi = $i;
373         $i += $delta;
374         if ($i < 0) { $i = 0; }
375         if ($i >= $w->{"n"}) { $i = $w->{"n"}-1; }
376         $w->{"i"} = $i;
377         if ($w->{"visible"}) {
378                 $w->redraw_line($oldi);
379                 if ($i < $w->{"1st"}) {
380                         my $j = $w->{"1st"} - $i;
381                         $w->{"1st"} = $i;
382                         if ($j >= $w->{"h"}/2) {
383                                 $w->{"win"}->scrollok(1);
384                                 $w->{"win"}->scrl(-$j);
385                                 $w->{"win"}->scrollok(0);
386                                 for (my $k=0; $k<$j; $k++) { $w->redraw_line($i+$k); }
387                         } else { $w->redraw; }
388                 } elsif ($i >= $w->{"1st"} + $w->{"h"}) {
389                         my $j = $i - $w->{"1st"} - $w->{"h"} + 1;
390                         $w->{"1st"} += $j;
391                         if ($j < $w->{"h"}/2) {
392                                 $w->{"win"}->scrollok(1);
393                                 $w->{"win"}->scrl($j);
394                                 $w->{"win"}->scrollok(0);
395                                 for (my $k=1; $k<=$j; $k++) { $w->redraw_line($i-$j+$k); }
396                         } else { $w->redraw; }
397                 } else { $w->redraw_line($i); }
398                 $w->{"win"}->noutrefresh;
399         }
400         if ($auto_enter && $i < $w->{"n"} && $w->{"flist"}->[$i] !~ /\/$/) { $w->select; }
401 }
402
403 sub select($) {
404         my ($w) = @_;
405         if ($w->{"i"} < $w->{"n"}) {
406                 my $f = $w->{"flist"}->[$w->{"i"}];
407                 my $x = $w->{"list"}->[$w->{"i"}];
408                 if ($f =~ /\/$/) {
409                         if ($f eq "../") {
410                                 $w->{"dir"} =~ s@([^/]*/)$@@;
411                                 my $back = $1;
412                                 $w->{"xdir"} =~ s@[^/]*/$@@;
413                                 $w->reload;
414                                 for (my $i=0; $i<$w->{"n"}; $i++) {
415                                         if ($w->{"flist"}->[$i] eq $back) {
416                                                 $w->{"i"} = $i;
417                                                 $w->{"1st"} = $i - int($w->{"h"}/2);
418                                                 last;
419                                         }
420                                 }
421                         } else {
422                                 $w->{"dir"} .= $f;
423                                 $w->{"xdir"} .= $x;
424                                 $w->reload;
425                         }
426                         $w->redraw;
427                 } else {
428                         $main_window->view($w->{"dir"} . $f, $w->{"xdir"} . $x);
429                 }
430         }
431 }
432
433 sub key($$) {
434         my ($w,$key) = @_;
435         if ($key eq KEY_UP) { $w->go(-1); }
436         elsif ($key eq KEY_DOWN) { $w->go(1); }
437         elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
438         elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
439         elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
440         elsif ($key eq KEY_END) { $w->go(1000000000); }
441         elsif ($key eq "\n" || $key eq "\r" || $key eq KEY_RIGHT) { $w->select; }
442         elsif ($key eq KEY_LEFT) {
443                 if ($w->{"list"}->[0] eq "<parent>") {
444                         $w->{"i"} = 0;
445                         $w->select;
446                 }
447         }
448 }
449
450 package VS::Window::Status;
451 BEGIN { our @ISA = qw(VS::Window); }
452
453 sub new($) {
454         my $w = new VS::Window;
455         $w->{"focusable"} = 0;
456         $w->{"msg"} = "";
457         return bless $w;
458 }
459
460 sub redraw($) {
461         my $w = shift @_;
462         my $win = $w->{"win"};
463         $win->bkgdset($attr_status);
464         $win->addstr(0, 0, $w->{"msg"});
465         $win->clrtoeol;
466         $win->refresh;
467 }
468
469 sub tell($$) {
470         my ($w,$m) = @_;
471         if ($w->{"msg"} ne $m) {
472                 $w->{"msg"} = $m;
473                 $w->redraw;
474         }
475 }