]> mj.ucw.cz Git - vs.git/blob - vs.pl
006339119e08b1789942bd4f2117be69a1a1b09c
[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         } else {
125                 $focused_window->key($key);
126         }
127 }
128
129 ### Window Objects ###
130
131 package VS::Window;
132
133 sub new($) {
134         my $w = {
135                 "visible" => 1,
136                 "focusable" => 1,
137                 "focused" => 0,
138                 "x" => -1,
139                 "y" => -1,
140                 "w" => -1,
141                 "h" => -1
142         };
143         push @window_list, $w;
144         return bless $w;
145 }
146
147 sub place($$$$$) {
148         my ($w,$nx,$ny,$nh,$nw) = @_;
149         if ($w->{"visible"}) {
150                 if (!defined $w->{"win"} || $w->{"x"} != $nx || $w->{"y"} != $ny
151                  || $w->{"w"} != $nw || $w->{"h"} != $nh) {
152                         $w->{"win"} = $W->subwin($nh,$nw,$nx,$ny);
153                         $w->{"x"} = $nx;
154                         $w->{"y"} = $ny;
155                         $w->{"w"} = $nw;
156                         $w->{"h"} = $nh;
157                         $w->redraw;
158                 }
159         } else {
160                 delete $w->{"win"};
161         }
162 }
163
164 sub key($) { }
165 sub redraw($) { }
166
167 package VS::Window::Main;
168 use Curses;
169 BEGIN { our @ISA = qw(VS::Window); }
170
171 sub new($) {
172         my $w = new VS::Window;
173         $w->{"file"} = "";
174         $w->{"attrs"} = {};
175         $w->{"lines"} = ["", "", "   The Virtual Songbook 0.0\n", "   (c) 2003 Martin Mares <mj\@ucw.cz>"];
176         $w->{"linetype"} = [0,0,0,0];
177         $w->{"n"} = 4;
178         $w->{"top"} = 0;
179         return bless $w;
180 }
181
182 sub view($$$) {
183         my ($w,$f,$x) = @_;
184         if ($w->{"file"} ne $f) {
185                 $w->{"file"} = $f;
186                 $w->{"xfile"} = $x;
187                 $f =~ s@^./@@;
188                 $x =~ s@^./@@;
189                 if (open X, $f) {
190                         my %attrs = ();
191                         while (<X>) {
192                                 chomp;
193                                 /^$/ && last;
194                                 if (/^(\w+):\s*(.*)/ && !defined $attrs{$1}) {
195                                         $attrs{$1} = $2;
196                                 }
197                         }
198                         my @lines = ();
199                         my @types = ();
200                         while (<X>) {
201                                 chomp;
202                                 if (s/^!//) { push @types, 1; } else { push @types, 0; }
203                                 push @lines, $_;
204                         }
205                         close X;
206                         $w->{"attrs"} = \%attrs;
207                         $w->{"lines"} = \@lines;
208                         $w->{"linetypes"} = \@types;
209                         $w->{"top"} = 0;
210                         $w->{"n"} = scalar @lines;
211                         $w->redraw;
212                         if (defined $attrs{"Name"}) {
213                                 $x = $attrs{"Name"};
214                                 $x = $attrs{"Author"} . ": $x" if defined $attrs{"Author"};
215                         }
216                         $status_window->tell($x);
217                 } else {
218                         $status_window->tell("Cannot open $f");
219                 }
220         }
221 }
222
223 sub redraw_line($$) {
224         my ($w,$i) = @_;
225         my $win = $w->{"win"};
226         my $l = $w->{"lines"}->[$i];
227         if ($w->{"linetypes"}->[$i]) { $win->attrset($attr_chord); }
228         if (length $l < $w->{"w"}) {
229                 $win->addstr($i-$w->{"top"}, 0, $l);
230                 $win->clrtoeol;
231         } else {
232                 $win->addstr($i-$w->{"top"}, 0, substr($l, 0, $w->{"w"}));
233         }
234         $win->attrset($attr_normal);
235 }
236
237 sub redraw($) {
238         my $w = shift @_;
239         my $win = $w->{"win"};
240         my $top = $w->{"top"};
241         my $cnt = $w->{"n"} - $w->{"top"};
242         if ($cnt > $w->{"h"}) { $cnt = $w->{"h"}; }
243         for (my $i=$top; $i<$top+$cnt; $i++) { $w->redraw_line($i); }
244         if ($cnt < $w->{"h"}) {
245                 $win->move($cnt, 0);
246                 $win->clrtobot;
247         }
248         $win->noutrefresh;
249 }
250
251 sub go($$) {
252         my ($w,$delta) = @_;
253         my $win = $w->{"win"};
254         my $top = $w->{"top"} + $delta;
255         if ($top + $w->{"h"} > $w->{"n"}) { $top = $w->{"n"} - $w->{"h"}; }
256         if ($top < 0) { $top = 0; }
257         my $otop = $w->{"top"};
258         $w->{"top"} = $top;
259         if ($top < $otop - $w->{"h"}/2) {
260                 $w->redraw;
261         } elsif ($top < $otop) {
262                 my $j = $otop - $top;
263                 $win->scrollok(1);
264                 $win->scrl(-$j);
265                 $win->scrollok(0);
266                 for (my $i=0; $i<$j; $i++) { $w->redraw_line($top+$i); }
267         } elsif ($top == $otop) {
268                 # Nothing happens
269         } elsif ($top < $otop + $w->{"h"}/2) {
270                 my $j = $top - $otop;
271                 $win->scrollok(1);
272                 $win->scrl($j);
273                 $win->scrollok(0);
274                 for (my $i=$j; $i>0; $i--) { $w->redraw_line($top+$w->{"h"}-$i); }
275         } else {
276                 $w->redraw;
277         }
278         $win->noutrefresh;
279 }
280
281 sub key($$) {
282         my ($w,$key) = @_;
283         if ($key eq KEY_UP) { $w->go(-1); }
284         elsif ($key eq KEY_DOWN) { $w->go(1); }
285         elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
286         elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
287         elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
288         elsif ($key eq KEY_END) { $w->go(1000000000); }
289         else { $status_window->tell("Unknown key <$key>"); }
290 }
291
292 package VS::Window::File;
293 use Curses;
294 BEGIN { our @ISA = qw(VS::Window); }
295
296 sub new($) {
297         my $w = new VS::Window;
298         $w->{"dir"} = "./";
299         $w->{"xdir"} = "./";
300         return bless $w;
301 }
302
303 sub reload($) {
304         my $w = shift;
305         my $p = $w->{"dir"};
306         my @l = `cd $p && ls`;
307         my @fn = ();
308         my @full = ();
309         if ($p ne "./") { push @fn, "../"; push @full, "<parent>"; }
310         foreach my $x (@l) {
311                 chomp $x;
312                 if (-f "$p/$x") {
313                         push @fn, $x;
314                         my $fullname = $x;
315                         if ($try_full_names && open(X, "$p/$x")) {
316                                 while (<X>) {
317                                         chomp;
318                                         /^$/ && last;
319                                         if (/^Name:\s*(.*)/) {
320                                                 $fullname = $1;
321                                                 last;
322                                         }
323                                 }
324                                 close X;
325                         }
326                         push @full, $fullname;
327                 } elsif (-d "$p/$x") { push @fn, "$x/"; push @full, "$x/"; }
328         }
329         $w->{"flist"} = \@fn;
330         $w->{"list"} = \@full;
331         $w->{"n"} = scalar @fn;
332         $w->{"i"} = 0;
333         $w->{"1st"} = 0;
334 }
335
336 sub redraw_line($$) {
337         my ($w,$i) = @_;
338         my $line = $i - $w->{"1st"};
339         if ($line < 0 || $line >= $w->{"h"}) { return; }
340         my $win = $w->{"win"};
341         my $item = ($i < $w->{"n"}) ? substr($w->{"list"}->[$i], 0, $w->{"w"}) : "";
342         if ($i == $w->{"i"}) { $win->bkgdset($attr_status); }
343         $win->addstr($line, 0, $item);
344         $win->clrtoeol if length $item < $w->{"w"};
345         $win->bkgdset($attr_normal);
346 }
347
348 sub redraw($) {
349         my $w = shift @_;
350         $w->{"win"}->idlok(1);
351         for (my $i=0; $i<$w->{"h"}; $i++) {
352                 $w->redraw_line($w->{"1st"} + $i);
353         }
354         $w->{"win"}->noutrefresh;
355 }
356
357 sub go($$) {
358         my ($w,$delta) = @_;
359         my $i = $w->{"i"};
360         my $oldi = $i;
361         $i += $delta;
362         if ($i < 0) { $i = 0; }
363         if ($i >= $w->{"n"}) { $i = $w->{"n"}-1; }
364         $w->{"i"} = $i;
365         if ($w->{"visible"}) {
366                 $w->redraw_line($oldi);
367                 if ($i < $w->{"1st"}) {
368                         my $j = $w->{"1st"} - $i;
369                         $w->{"1st"} = $i;
370                         if ($j >= $w->{"h"}/2) {
371                                 $w->{"win"}->scrollok(1);
372                                 $w->{"win"}->scrl(-$j);
373                                 $w->{"win"}->scrollok(0);
374                                 for (my $k=0; $k<$j; $k++) { $w->redraw_line($i+$k); }
375                         } else { $w->redraw; }
376                 } elsif ($i >= $w->{"1st"} + $w->{"h"}) {
377                         my $j = $i - $w->{"1st"} - $w->{"h"} + 1;
378                         $w->{"1st"} += $j;
379                         if ($j < $w->{"h"}/2) {
380                                 $w->{"win"}->scrollok(1);
381                                 $w->{"win"}->scrl($j);
382                                 $w->{"win"}->scrollok(0);
383                                 for (my $k=1; $k<=$j; $k++) { $w->redraw_line($i-$j+$k); }
384                         } else { $w->redraw; }
385                 } else { $w->redraw_line($i); }
386                 $w->{"win"}->noutrefresh;
387         }
388         if ($auto_enter && $i < $w->{"n"} && $w->{"flist"}->[$i] !~ /\/$/) { $w->select; }
389 }
390
391 sub select($) {
392         my ($w) = @_;
393         if ($w->{"i"} < $w->{"n"}) {
394                 my $f = $w->{"flist"}->[$w->{"i"}];
395                 my $x = $w->{"list"}->[$w->{"i"}];
396                 if ($f =~ /\/$/) {
397                         if ($f eq "../") {
398                                 $w->{"dir"} =~ s@([^/]*/)$@@;
399                                 my $back = $1;
400                                 $w->{"xdir"} =~ s@[^/]*/$@@;
401                                 $w->reload;
402                                 for (my $i=0; $i<$w->{"n"}; $i++) {
403                                         if ($w->{"flist"}->[$i] eq $back) {
404                                                 $w->{"i"} = $i;
405                                                 if ($i > $w->{"h"}/2) { $w->{"1st"} = $i - int($w->{"h"}/2); }
406                                                 if ($w->{"1st"} + $w->{"h"} > $w->{"n"}) { $w->{"1st"} = $w->{"n"} - $w->{"h"}; }
407                                                 if ($w->{"1st"} < 0) { $w->{"1st"} = 0; }
408                                                 last;
409                                         }
410                                 }
411                         } else {
412                                 $w->{"dir"} .= $f;
413                                 $w->{"xdir"} .= $x;
414                                 $w->reload;
415                         }
416                         $w->redraw;
417                 } else {
418                         $main_window->view($w->{"dir"} . $f, $w->{"xdir"} . $x);
419                 }
420         }
421 }
422
423 sub key($$) {
424         my ($w,$key) = @_;
425         if ($key eq KEY_UP) { $w->go(-1); }
426         elsif ($key eq KEY_DOWN) { $w->go(1); }
427         elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
428         elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
429         elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
430         elsif ($key eq KEY_END) { $w->go(1000000000); }
431         elsif ($key eq "\n" || $key eq "\r" || $key eq KEY_RIGHT) { $w->select; }
432         elsif ($key eq KEY_LEFT) {
433                 if ($w->{"list"}->[0] eq "<parent>") {
434                         $w->{"i"} = 0;
435                         $w->select;
436                 }
437         }
438 }
439
440 package VS::Window::Status;
441 BEGIN { our @ISA = qw(VS::Window); }
442
443 sub new($) {
444         my $w = new VS::Window;
445         $w->{"focusable"} = 0;
446         $w->{"msg"} = "";
447         return bless $w;
448 }
449
450 sub redraw($) {
451         my $w = shift @_;
452         my $win = $w->{"win"};
453         $win->bkgdset($attr_status);
454         $win->addstr(0, 0, $w->{"msg"});
455         $win->clrtoeol;
456         $win->refresh;
457 }
458
459 sub tell($$) {
460         my ($w,$m) = @_;
461         if ($w->{"msg"} ne $m) {
462                 $w->{"msg"} = $m;
463                 $w->redraw;
464         }
465 }