#!/usr/bin/perl
# The Virtual Songbook
# (c) 2003--2004 Martin Mares <mj@ucw.cz>

use Curses;
use strict;
use warnings;

### Help ###

my @help_message = (
	"",
	"   The Virtual Songbook 0.9\n",
	"   (c) 2003--2004 Martin Mares <mj\@ucw.cz>",
	"",
	"Control keys:",
	"",
	"   ?            display this help text",
	"   q            quit the program",
	"   <arrows>     scroll text in the current window",
	"   [ ]          enlarge/shrink the current window",
	"   f            display the file list window, focus it if already displayed",
	"   s            display the split view window, focus it if already displayed",
	"   m            focus the main window",
	"   F S          hide the file list / split view window",
	"   Ctrl-L       redraw the screen",
	"",
	"Main window keys:",
	"",
	"   + - 0        transpose chords up/down/reset",
	"   =            a synonym for +",
	"   c            toggle display of chords",
	"",
	"File list keys:",
	"",
	"   <up> <down>  select next/previous item",
	"   <left>       go one directory up",
	"   <enter>      go to the selected directory",
	"   <right>      like <enter>",
	"   j k h l      synonyms of the arrow keys (like in vi) [works in any window]",
	"   Ctrl-R       reload the list",
	"   r            toggle resolving of song/directory names",
	"   /            incremental search (/=next, \\=previous)",
	"",
);

my $help_status = "The Virtual Songbook 0.9";

### Interface with Curses ###

my $W;
my $color_mode;

sub init_terminal() {
	$W = new Curses;
	start_color;
	$color_mode = (has_colors && COLORS >= 8 && COLOR_PAIRS >= 8) unless defined $color_mode;
	cbreak; noecho;
	$W->intrflush(0);
	$W->keypad(1);
	$W->meta(1);
}

sub cleanup_terminal() {
	endwin;
}

my ($attr_normal, $attr_status, $attr_hilite, $attr_chord, $attr_search);

sub setup_attrs() {
	if ($color_mode) {
		init_pair(1, COLOR_YELLOW, COLOR_BLUE);
		$attr_status = COLOR_PAIR(1) | A_BOLD;
		init_pair(2, COLOR_YELLOW, COLOR_BLACK);
		$attr_chord = COLOR_PAIR(2);
		init_pair(3, COLOR_YELLOW, COLOR_GREEN);
		$attr_search = COLOR_PAIR(3) | A_BOLD;
	} else {
		$attr_status = A_BOLD;
		$attr_chord = A_BOLD;
		$attr_search = A_BOLD;
	}
	$attr_normal = A_NORMAL;
	$attr_hilite = A_BOLD;
}

my $try_full_names = 1;
my $auto_enter = 1;
my $file_window_width = 20;
my $split_window_height = 10;

my ($term_w, $term_h);
my @window_list = ();
my $file_window = new VS::Window::File;
my $main_window = new VS::Window::Main;
my $split_window = new VS::Window::Main;
my $status_window = new VS::Window::Status;
$split_window->{"visible"} = 0;
$file_window->reload;
my $focused_window;

sub recalc_windows() {
	my $w = COLS;
	my $h = LINES;
	$term_w = $w;
	$term_h = $h;
	$status_window->place(0, 0, 1, $w);
	if ($file_window->{"visible"}) {
		my $fww = $file_window_width;
		$w -= $fww;
		$W->attrset($file_window->{"focused"} ? $attr_hilite : $attr_normal);
		$W->vline(2, $w-1, ACS_VLINE, $h-3);
		$W->hline(1, $w, ACS_HLINE, $fww);
		$W->hline($h-1, $w, ACS_HLINE, $fww);
		$W->addch(1, $w-1, ACS_ULCORNER);
		$W->addch($h-1, $w-1, ACS_LLCORNER);
		$W->attrset($attr_normal);
		$file_window->place(2, $w, $h-3, $fww);
		$w--;
	} else { $file_window->place(0, 0, 0, 0); }
	if ($split_window->{"visible"}) {
		my $swh = $split_window_height;
		$h -= $swh;
		$W->attrset($split_window->{"focused"} ? $attr_hilite : $attr_normal);
		$W->hline($h-1, 0, ($split_window->{"focused"} ? "=" : ACS_HLINE), $w);
		$W->attrset($attr_normal);
		$split_window->place($h, 0, $swh, $w);
		$h--;
	} else { $split_window->place(0, 0, 0, 0); }
	$main_window->place(1, 0, $h-1, $w);
}

sub focus_next() {
	my @wins = ( $main_window, $split_window, $file_window );	# [0] is always focusable
	my $i = 0;
	while ($i <= $#wins && $wins[$i] != $focused_window) { $i++; }
	$i++;
	while ($i <= $#wins && !$wins[$i]->{"visible"}) { $i++; }
	if ($i > $#wins) { $i=0; }
	$wins[$i]->focus;
}

init_terminal;
setup_attrs;
$file_window->focus;	# calls recalc_windows
$main_window->view_help;

for(;;) {
	$focused_window->show_cursor;
	$W->refresh;
	my $key = $W->getch;
	if ($focused_window->key($key)) {
		# key handled by the window
	} elsif ($key eq "\033" || $key eq "q") {
		cleanup_terminal;
		exit 0;
	} elsif ($key eq "f") {
		$file_window->toggle(1);
	} elsif ($key eq "F") {
		$file_window->toggle(0);
	} elsif ($key eq "s") {
		$split_window->toggle(1);
	} elsif ($key eq "S") {
		$split_window->toggle(0);
	} elsif ($key eq "m") {
		$main_window->toggle(1);
	} elsif ($key eq "?") {
		$main_window->view_help;
	} elsif ($key eq "\014") {
		$curscr->clearok(1);
	} elsif ($key eq "j") {
		$file_window->key(KEY_DOWN);
	} elsif ($key eq "k") {
		$file_window->key(KEY_UP);
	} elsif ($key eq "h") {
		$file_window->key(KEY_LEFT);
	} elsif ($key eq "l") {
		$file_window->key(KEY_RIGHT);
	} elsif ($key eq "\t") {
		focus_next;
	}
}

### Chords ###

package VS::Chord;

# Internal representation of chords: <base-tone>:<type> (so C="0:", C#="1:", Dmi="2:mi" etc.)
# but usually they are accompanied by position info after a second colon

our (%t2n, @n2t);

BEGIN {
%t2n = (
	"C" => 0,
	"C#" => 1,
		"Db" => 1,
	"D" => 2,
	"D#" => 3,
		"Eb" => 3,
	"E" => 4,
		"Fb" => 4,
		"E#" => 5,
	"F" => 5,
	"F#" => 6,
		"Gb" => 6,
	"G" => 7,
	"G#" => 8,
		"Ab" => 8,
	"A" => 9,
	"A#" => 10,
		"Hb" => 10,
		"B" => 10,
		"Bb" => 10,	# common error
	"H" => 11,
		"B#" => 11,
		"Cb" => 11,
		"H#" => 0
);
@n2t = ( "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "B", "H" );
}

sub parse_line($) {
	my $r = shift @_;
	my @l = ();
	my $pos = 0;
	while (my ($spaces,$chord,$rest) = $r =~ /(\s*)(\S+)(.*)/) {
		$pos += length $spaces;
		if (my ($tone,$sh,$mod) = ($chord =~ /^([CDEFGABH](#|b|))(.*)$/)) {
			my $k = $t2n{$tone};
			push @l, "$k:$mod:$pos"
		} else {
			push @l, "0:?$chord:$pos";
		}
		$pos += length $chord;
		$r = $rest;
	}
	return \@l;
}

sub synthesize_line($$) {
	my ($l,$xpos) = @_;
	my $pp = 0;
	my $result = "";
	for (my $i=0; $i<@$l; $i++) {
		my ($tone,$mod,$pos) = split(/:/, $l->[$i]);
		$tone = ($tone + $xpos) % 12;
		my $chord = $n2t[$tone] . "$mod ";
		if ($pp < $pos) {
			$result .= " " x ($pos - $pp);
			$pp = $pos;
		}
		$result .= $chord;
		$pp += length $chord;
	}
	return $result;
}

### Window Objects ###

package VS::Window;

sub new($) {
	my $w = {
		"visible" => 1,
		"focused" => 0,
		"x" => -1,
		"y" => -1,
		"w" => -1,
		"h" => -1
	};
	push @window_list, $w;
	return bless $w;
}

sub place($$$$$) {
	my ($w,$nx,$ny,$nh,$nw) = @_;
	if ($w->{"visible"}) {
		if (!defined $w->{"win"} || $w->{"x"} != $nx || $w->{"y"} != $ny
		 || $w->{"w"} != $nw || $w->{"h"} != $nh) {
			$w->{"win"} = $W->subwin($nh,$nw,$nx,$ny);
			$w->{"x"} = $nx;
			$w->{"y"} = $ny;
			$w->{"w"} = $nw;
			$w->{"h"} = $nh;
			$w->redraw;
		}
	} else {
		delete $w->{"win"};
	}
}

sub focus($) {
	my $w = shift;
	$focused_window->{"focused"} = 0 if defined $focused_window;
	if (!$w->{"visible"}) { $w=$main_window; }	# Main is always visible
	$focused_window = $w;
	$w->{"focused"} = 1;
	::recalc_windows;
}

sub toggle($;$) {
	my $w = shift;
	my $vis = shift;
	if ($vis) {
		if (!$w->{"visible"} || !$w->{"focused"}) {
			$w->{"visible"} = 1;
			$w->focus;
		}
	} else {
		if ($w->{"visible"}) {
			$w->{"visible"} = 0;
			$main_window->focus;
		}
	}
}

sub key($) { return 0; }
sub redraw($) { }

sub reset_cursor($) {
	$W->move($term_h-1, $term_w-1);
}

sub set_cursor($$$) {
	my ($w,$x,$y) = @_;
	if ($x < 0) {
		$x = 0;
	} elsif ($x >= $w->{"h"}) {
		$x = $w->{"h"} - 1;
	}
	if ($y < 0) {
		$y = 0;
	} elsif ($y >= $w->{"w"}) {
		$y = $w->{"w"} - 1;
	}
	$W->move($w->{"x"} + $x, $w->{"y"} + $y);
}

sub show_cursor($) {
	my ($w) = @_;
	$w->reset_cursor;
}

package VS::Window::Main;
use Curses;
BEGIN { our @ISA = qw(VS::Window); }

sub new($) {
	my $w = new VS::Window;
	bless $w;
	$w->empty;
	return $w;
}

sub empty($) {
	my $w = shift;
	$w->{"file"} = "";
	$w->{"attrs"} = {};
	$w->{"lines"} = [];
	$w->{"text_lines"} = [];
	$w->{"chords"} = undef;
	$w->{"visible_lines"} = [];
	$w->{"n"} = 0;
	$w->{"top"} = 0;
	$w->{"current_xpose"} = 0;
	$w->{"requested_xpose"} = 0;
	$w->{"chords_visible"} = 1;
}

sub update_text($) {
	my $w = shift;
	my $l = $w->{($w->{"chords_visible"} ? "lines" : "text_lines")};
	$w->{"visible_lines"} = $l;
	$w->{"n"} = @$l;
}

sub view_help($) {
	my $w = shift;
	$w->empty;
	$w->{"text_lines"} = $w->{"lines"} = \@help_message;
	$w->update_text;
	$status_window->tell($help_status);
	$w->update_other;
	$w->sync;
}

sub view($$$) {
	my ($w,$f,$x) = @_;
	if ($w->{"file"} ne $f) {
		$w->empty;
		$w->{"file"} = $f;
		$w->{"xfile"} = $x;
		$f =~ s@^./@@;
		$x =~ s@^./@@;
		if (open X, $f) {
			my %attrs = ();
			while (<X>) {
				chomp;
				/^$/ && last;
				if (/^(\w+):\s*(.*)/ && !defined $attrs{$1}) {
					$attrs{$1} = $2;
				}
			}
			my @lines = ();
			my @text_lines = ();
			while (<X>) {
				chomp;
				push @lines, $_;
				push @text_lines, $_ unless /^!/;
			}
			close X;
			$w->{"attrs"} = \%attrs;
			$w->{"lines"} = \@lines;
			$w->{"text_lines"} = \@text_lines;
			$w->{"top"} = 0;
			$w->update_text;
			$w->update_other;
			$w->sync;
			if (defined $attrs{"Name"}) {
				$x = $attrs{"Name"};
				$x = $attrs{"Author"} . ": $x" if defined $attrs{"Author"};
			}
			$status_window->tell($x);
		} else {
			$status_window->tell("Cannot open $f");
		}
	}
}

sub transpose($) {
	my $w = shift @_;
	if (!defined $w->{"chords"}) {
		$w->{"chords"} = [];
		for (my $i=0; $i<@{$w->{"lines"}}; $i++) {
			push @{$w->{"chords"}}, ($w->{"lines"}->[$i] =~/^!(.*)/ ? VS::Chord::parse_line($1) : undef);
		}
	}
	for (my $i=0; $i<@{$w->{"lines"}}; $i++) {
		if (defined $w->{"chords"}->[$i]) {
			$w->{"lines"}->[$i] = "!" . VS::Chord::synthesize_line($w->{"chords"}->[$i], $w->{"requested_xpose"});
		}
	}
	$w->{"current_xpose"} = $w->{"requested_xpose"};
}

sub redraw_line($$) {
	my ($w,$i) = @_;
	my $win = $w->{"win"};
	my $l = $w->{"visible_lines"}->[$i] || "";
	if ($l =~ s/^!//) { $win->attrset($attr_chord); }
	if (length $l < $w->{"w"}) {
		$win->addstr($i-$w->{"top"}, 0, $l);
		$win->clrtoeol;
	} else {
		$win->addstr($i-$w->{"top"}, 0, substr($l, 0, $w->{"w"}));
	}
	$win->attrset($attr_normal);
}

sub redraw($) {
	my $w = shift @_;
	my $win = $w->{"win"};
	my $top = $w->{"top"};
	my $cnt = $w->{"n"} - $w->{"top"};
	if ($cnt > $w->{"h"}) { $cnt = $w->{"h"}; }
	for (my $i=$top; $i<$top+$cnt; $i++) { $w->redraw_line($i); }
	if ($cnt < $w->{"h"}) {
		$win->move($cnt, 0);
		$win->clrtobot;
	}
	$win->noutrefresh;
}

sub other_split($) {
	my $w = shift @_;
	return ($w == $main_window) ? $split_window : $main_window;
}

sub update_other($) {
	my $w = shift @_;
	my $other = $w->other_split;
	foreach $a ("attrs", "lines", "text_lines", "chords", "n", "top", "current_xpose", "requested_xpose") {
		$other->{$a} = $w->{$a} if defined $w->{$a};
	}
	$other->update_text;
}

sub sync($) {
	my $w = shift @_;
	my $other = $w->other_split;
	if ($w->{"current_xpose"} != $w->{"requested_xpose"}) {
		$w->transpose;
		$w->update_other;
	}
	$w->redraw;
	$other->redraw if $other->{"visible"};
}

sub go($$) {
	my ($w,$delta) = @_;
	my $win = $w->{"win"};
	my $top = $w->{"top"} + $delta;
	if ($top + $w->{"h"} > $w->{"n"}) { $top = $w->{"n"} - $w->{"h"}; }
	if ($top < 0) { $top = 0; }
	my $otop = $w->{"top"};
	$w->{"top"} = $top;
	if ($top < $otop - $w->{"h"}/2) {
		$w->redraw;
	} elsif ($top < $otop) {
		my $j = $otop - $top;
		$win->scrollok(1);
		$win->scrl(-$j);
		$win->scrollok(0);
		for (my $i=0; $i<$j; $i++) { $w->redraw_line($top+$i); }
	} elsif ($top == $otop) {
		# Nothing happens
	} elsif ($top < $otop + $w->{"h"}/2) {
		my $j = $top - $otop;
		$win->scrollok(1);
		$win->scrl($j);
		$win->scrollok(0);
		for (my $i=$j; $i>0; $i--) { $w->redraw_line($top+$w->{"h"}-$i); }
	} else {
		$w->redraw;
	}
	$win->noutrefresh;
}

sub key($$) {
	my ($w,$key) = @_;
	if ($key eq KEY_UP) { $w->go(-1); }
	elsif ($key eq KEY_DOWN) { $w->go(1); }
	elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
	elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
	elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
	elsif ($key eq KEY_END) { $w->go(1000000000); }
	elsif ($key eq "+" || $key eq "=") { $w->{"requested_xpose"} = ($w->{"requested_xpose"}+1)%12; $w->sync; $status_window->redraw; }
	elsif ($key eq "-") { $w->{"requested_xpose"} = ($w->{"requested_xpose"}+11)%12; $w->sync; $status_window->redraw; }
	elsif ($key eq "0") { $w->{"requested_xpose"} = 0; $w->sync; $status_window->redraw; }
	elsif ($key eq "c") { $w->{"chords_visible"} = !$w->{"chords_visible"}; $w->update_text; $w->redraw; }
	elsif ($w == $split_window) {
		if ($key eq "]" && $split_window_height > 1) {
			$split_window_height--;
			::recalc_windows;
		} elsif ($key eq "[" && $split_window_height < $term_h-2) {
			$split_window_height++;
			::recalc_windows;
		} else { return 0; }
	} else { return 0; }
	return 1;
}

package VS::Window::File;
use Curses;
BEGIN { our @ISA = qw(VS::Window); }

our %name_cache = ();

sub new($) {
	my $w = new VS::Window;
	$w->{"dir"} = "./";
	$w->{"xdir"} = "./";
	return bless $w;
}

sub lookup_full_name($) {
	my $f = shift;
	return $name_cache{$f} if exists $name_cache{$f};
	return undef if (!$try_full_names || ($f =~ /\/$/ && $try_full_names < 2));
	my $full;
	if ($f =~ /\/$/) {
		my @sub = `cd $f && ls`;
		while (!defined($full) && @sub) {
			my $z = shift @sub;
			chomp $z;
			if (-f "$f/$z" && open(X, "$f/$z")) {
				while (<X>) {
					chomp;
					/^$/ && last;
					if (/^Author:\s*(.*)/) {
						$full = $1;
						last;
					}
				}
				close X;
			}
		}
	} elsif (open(X, $f)) {
		while (<X>) {
			chomp;
			/^$/ && last;
			if (/^Name:\s*(.*)/) {
				$full = $1;
				last;
			}
		}
		close X;
	}
	$name_cache{$f} = $full;
	return $full;
}

sub reload($) {
	my $w = shift;
	my $p = $w->{"dir"};
	my @l = `cd $p && ls`;
	my @fn = ();
	my @full = ();
	if ($p ne "./") { push @fn, "../"; push @full, "<parent>"; }
	foreach my $x (@l) {
		chomp $x;
		if (-f "$p/$x") {
			push @fn, $x;
			push @full, (lookup_full_name("$p/$x") || $x);
		} elsif (-d "$p/$x") {
			push @fn, "$x/";
			push @full, (lookup_full_name("$p/$x/") || $x) . "/";
		}
	}
	$w->{"flist"} = \@fn;
	$w->{"list"} = \@full;
	$w->{"n"} = scalar @fn;
	$w->{"i"} = 0;
	$w->{"1st"} = 0;
	$w->{"search"} = undef;
}

sub redraw_line($$) {
	my ($w,$i) = @_;
	my $line = $i - $w->{"1st"};
	if ($line < 0 || $line >= $w->{"h"}) { return; }
	my $win = $w->{"win"};
	my $item = ($i < $w->{"n"}) ? $w->{"list"}->[$i] : "";
	if ($i == $w->{"i"}) {
		if (defined $w->{"search"}) {
			$win->bkgdset($attr_search);
			substr($item, 0, length $w->{"search"}) = $w->{"search"};
		} else { $win->bkgdset($attr_status); }
	}
	$item = substr($item, 0, $w->{"w"});
	$win->addstr($line, 0, $item);
	$win->clrtoeol if length $item < $w->{"w"};
	$win->bkgdset($attr_normal);
}

sub redraw($) {
	my $w = shift @_;
	my $win = $w->{"win"};
	# Window size might have changed...
	if ($w->{"1st"} + $w->{"h"} > $w->{"n"}) { $w->{"1st"} = $w->{"n"} - $w->{"h"}; }
	if ($w->{"1st"} < 0) { $w->{"1st"} = 0; }
	$win->idlok(1);
	for (my $i=0; $i<$w->{"h"}; $i++) {
		$w->redraw_line($w->{"1st"} + $i);
	}
	$win->noutrefresh;
}

sub goto($$) {
	my ($w,$i) = @_;
	my $oldi = $w->{"i"};
	if ($i < 0) { $i = 0; }
	if ($i >= $w->{"n"}) { $i = $w->{"n"}-1; }
	$w->{"i"} = $i;
	if ($w->{"visible"}) {
		$w->redraw_line($oldi);
		if ($i < $w->{"1st"}) {
			my $j = $w->{"1st"} - $i;
			$w->{"1st"} = $i;
			if ($j >= $w->{"h"}/2) {
				$w->{"win"}->scrollok(1);
				$w->{"win"}->scrl(-$j);
				$w->{"win"}->scrollok(0);
				for (my $k=0; $k<$j; $k++) { $w->redraw_line($i+$k); }
			} else { $w->redraw; }
		} elsif ($i >= $w->{"1st"} + $w->{"h"}) {
			my $j = $i - $w->{"1st"} - $w->{"h"} + 1;
			$w->{"1st"} += $j;
			if ($j < $w->{"h"}/2) {
				$w->{"win"}->scrollok(1);
				$w->{"win"}->scrl($j);
				$w->{"win"}->scrollok(0);
				for (my $k=1; $k<=$j; $k++) { $w->redraw_line($i-$j+$k); }
			} else { $w->redraw; }
		} else { $w->redraw_line($i); }
		$w->{"win"}->noutrefresh;
	}
	if ($auto_enter && $i < $w->{"n"} && $w->{"flist"}->[$i] !~ /\/$/) { $w->select; }
}

sub go($$) {
	my ($w,$delta) = @_;
	$w->{"search"} = undef;
	$w->goto($w->{"i"} + $delta);
}

sub search_next($$) {
	my ($w,$i,$skip) = @_;
	$i = ($i + $w->{"n"}) % $w->{"n"};
	my $start = $i;
	my $p = lc $w->{"search"};
	do {
		my $s = lc substr($w->{"list"}->[$i], 0, length $p);
		if ($s eq $p) {
			$w->goto($i);
			return;
		}
		$i += $skip;
		if ($i < 0) { $i = $w->{"n"}-1; }
		elsif ($i >= $w->{"n"}) { $i=0; }
	} while ($i != $start);
	$w->goto($i);
}

sub select($) {
	my ($w) = @_;
	if ($w->{"i"} < $w->{"n"}) {
		my $f = $w->{"flist"}->[$w->{"i"}];
		my $x = $w->{"list"}->[$w->{"i"}];
		if ($f =~ /\/$/) {
			if ($f eq "../") {
				$w->{"dir"} =~ s@([^/]*/)$@@;
				my $back = $1;
				$w->{"xdir"} =~ s@[^/]*/$@@;
				$w->reload;
				for (my $i=0; $i<$w->{"n"}; $i++) {
					if ($w->{"flist"}->[$i] eq $back) {
						$w->{"i"} = $i;
						$w->{"1st"} = $i - int($w->{"h"}/2);
						last;
					}
				}
			} else {
				$w->{"dir"} .= $f;
				$w->{"xdir"} .= $x;
				$w->reload;
			}
			$w->redraw;
		} else {
			$main_window->view($w->{"dir"} . $f, $w->{"xdir"} . $x);
		}
	}
}

sub key($$) {
	my ($w,$key) = @_;
	if ($key eq KEY_UP) { $w->go(-1); }
	elsif ($key eq KEY_DOWN) { $w->go(1); }
	elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
	elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
	elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
	elsif ($key eq KEY_END) { $w->go(1000000000); }
	elsif ($key eq KEY_RIGHT || $key eq "\r" || $key eq "\n") { $w->{"search"}=undef; $w->goto($w->{"i"}); $w->select; }
	elsif ($key eq KEY_LEFT) {
		if ($w->{"list"}->[0] eq "<parent>") {
			$w->{"i"} = 0;
			$w->select;
		}
	} elsif ($key eq "\x12") {
		$w->reload;
		$w->redraw;
	} elsif ($key eq "[" && $file_window_width < $term_w-1) {
		$file_window_width++;
		::recalc_windows;
	} elsif ($key eq "]" && $file_window_width > 1) {
		$file_window_width--;
		::recalc_windows;
	} elsif (defined $w->{"search"}) {
		if ($key eq "\033") {
			$w->go(0);
		} elsif ($key eq "/") {
			$w->search_next($w->{"i"}+1, 1);
		} elsif ($key eq "\\") {
			$w->search_next($w->{"i"}-1, -1);
		} elsif ($key eq "?") {
			return 0;
		} elsif ($key eq KEY_BACKSPACE) {
			if (length $w->{"search"}) {
				$w->{"search"} =~ s/.$//;
				$w->goto($w->{"i"});
			} else { $w->go(0); }
		} elsif (($key ge ' ' && $key lt "\x7f" || $key ge "\xa0" && $key le "\xff") && length $key == 1) {
			$w->{"search"} .= $key;
			$w->search_next($w->{"i"}, 1);
		} else { return 0; }
	} else {
		if ($key eq "r") {
			$try_full_names = ($try_full_names+1) % 3;
			%name_cache = ();
			$w->reload;
			$w->redraw;
		} elsif ($key eq "/") {
			$w->{"search"} = "";
			$w->goto($w->{"i"});
		} elsif ($key eq "\n" || $key eq "\r") {
			$w->select;
		} else { return 0; }
	}
	return 1;
}

sub show_cursor($) {
	my ($w) = @_;
	if (defined $w->{"search"}) {
		$w->set_cursor($w->{"i"} - $w->{"1st"}, length $w->{"search"});
	} else { $w->reset_cursor; }
}

package VS::Window::Status;
BEGIN { our @ISA = qw(VS::Window); }

sub new($) {
	my $w = new VS::Window;
	$w->{"msg"} = "";
	return bless $w;
}

sub redraw($) {
	my $w = shift @_;
	my $win = $w->{"win"};
	$win->bkgdset($attr_status);
	$win->addstr(0, 0, $w->{"msg"});
	$win->clrtoeol;
	my $aux = "";
	$aux = "T=" . $main_window->{"requested_xpose"} if ($main_window->{"requested_xpose"});
	$win->addstr(0, $w->{"w"}-length $aux, $aux) if $aux ne "";
	$win->refresh;
}

sub tell($$) {
	my ($w,$m) = @_;
	if ($w->{"msg"} ne $m) {
		$w->{"msg"} = $m;
		$w->redraw;
	}
}
