Mercurial > repo
diff interps/clc-intercal/CLC-INTERCAL-UI-Curses-1.-94.-2/INTERCAL/Interface/Curses.pm @ 996:859f9b4339e6
<Gregor> tar xf egobot.tar.xz
author | HackBot |
---|---|
date | Sun, 09 Dec 2012 19:30:08 +0000 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/interps/clc-intercal/CLC-INTERCAL-UI-Curses-1.-94.-2/INTERCAL/Interface/Curses.pm Sun Dec 09 19:30:08 2012 +0000 @@ -0,0 +1,911 @@ +package Language::INTERCAL::Interface::Curses; + +# Text (Curses) interface for sick and intercalc + +# This file is part of CLC-INTERCAL + +# Copyright (c) 2006-2008 Claudio Calvelli, all rights reserved. + +# CLC-INTERCAL is copyrighted software. However, permission to use, modify, +# and distribute it is granted provided that the conditions set out in the +# licence agreement are met. See files README and COPYING in the distribution. + +use strict; +use vars qw($VERSION $PERVERSION); +($VERSION) = ($PERVERSION = "CLC-INTERCAL/UI-Curses INTERCAL/Interface/Curses.pm 1.-94.-2") =~ /\s(\S+)$/; + +use Carp; +use Curses; +use Language::INTERCAL::Exporter '1.-94.-2'; +use Language::INTERCAL::Interface::common '1.-94.-2'; +use vars qw(@ISA); +@ISA = qw(Language::INTERCAL::Interface::common); + +my @savefields = qw(keypress keylist keyrows keycols lastkey + menu_byname menu_entries menu_keys menu_index + after_act in_menu in_dialog); + +my %keymap = ( + 'Left' => KEY_LEFT, + 'BackSpace' => KEY_BACKSPACE, + 'Enter' => KEY_ENTER, + 'Return' => "\cM", + 'Linefeed' => "\cJ", + (map { ("F$_" => KEY_F($_)) } (1..12)), + (map { ("M-" . chr($_) => chr($_ + 128)) } (1..127)), +); + +my %reserved = ( + &KEY_LEFT => \&_move_left, + &KEY_RIGHT => \&_move_right, + &KEY_UP => \&_move_up, + &KEY_DOWN => \&_move_down, + &KEY_ENTER => \&_activate, + "\cM" => \&_activate, + "\cJ" => \&_activate, +); + +sub new { + @_ == 2 + or croak "Usage: Language::INTERCAL::Interface::Curses->new(SERVER)"; + my ($class, $server) = @_; + $server or croak "Must provide SERVER"; + initscr(); + clearok(1); + noecho(); + cbreak(); + leaveok(0); + eval "END { eval { keypad(0) }; endwin(); print '\n' }"; + keypad(1); + meta(1); + my $curse = bless { + keypress => {}, + keylist => [], + keyrows => [], + keycols => [], + resize => 0, + redraw => 0, + windows => [], + pending => [], + menu_byname => {}, + menu_entries => {}, + menu_keys => [], + menu_index => {}, + in_menu => 0, + in_dialog => 0, + wid => 0, + server => $server, + }, $class; + $server->file_listen(fileno(STDIN), sub { + my $k = getch(); + while ($k ne ERR) { + push @{$curse->{pending}}, $k; + nodelay(1); + $k = getch(); + } + nodelay(0); + }); + $curse->_initialise; + $SIG{WINCH} = sub { $curse->{resize} = $curse->{redraw} = 1 }; + $curse; +} + +sub has_window { 1 } +sub is_interactive { 1 } +sub is_terminal { 1 } +sub can_paste { 0 } + +sub stdread { + croak "Curses interface should not use stdread directly"; +} + +sub getline { + @_ == 2 or croak "Usage: Curses->getline(PROMPT)"; + my ($curse, $prompt) = @_; + # XXX this is just a draft implementation so there is some way of + # XXX executing a WRITE IN - it's not meant to be the final form + my $v = ' ' x ($COLS - 10); + my @def = ( + 'vstack', border => 2, data => + ['text', value => $prompt, align => 'c'], + ['text', value => $v, align => 'l', name => '__getline'], + ); + my $window = $curse->window("Program input", undef, \@def); + $curse->set_text('__getline', ''); + my $line = ''; + $curse->{in_dialog} = \$line; + my $ok = 1; + $curse->{keypress}{"\c["} = { + hidden => 1, + action => sub { $curse->{running} = 0; $ok = 0 }, + enabled => 1, + }; + $curse->{keypress}{$_} = { + hidden => 1, + action => sub { + $line eq '' and return; + chop $line; + $curse->set_text('__getline', $line); + }, + enabled => 1, + } for (KEY_BACKSPACE, "\cH"); + my $or = $curse->{running}; + $curse->run; + $curse->close($window); + $curse->{running} = $or; + $ok ? "$line\n" : undef; +} + +sub file_dialog { + @_ == 5 or croak "Usage: Curses->file_dialog(TITLE, NEW?, OK, CANCEL)"; + my ($curse, $title, $new, $ok, $cancel) = @_; + # XXX this is just a draft implementation so there is some way of + # XXX getting a file name - it's not meand to be the final form + return $curse->getline($title); +} + +sub alter_data { + @_ == 3 or croak "Usage: Curses->alter_data(WINDOW, DATA)"; + croak "Augment not implemented for Curses"; # XXX +} + +sub window { + @_ == 4 || @_ == 5 || @_ == 6 + or croak "Usage: Curses->window(NAME, DESTROY, DEFINITION " + . "[, MENUS [, ACT]])"; + my ($curse, $name, $destroy, $def, $menus, $act) = @_; + $curse->{after_act} = $act; + my $window = _window($curse, $name, $def, $menus); + _place($window, 0, COLS, 0, LINES); + _finish_window($curse, $window); + &{$window->{show}}($curse, $window); + $window; +} + +sub _window { + my ($curse, $name, $def, $menus, $act) = @_; + $curse->{menu_byname} = {}; + $curse->{menu_entries} = {}; + $curse->{menu_keys} = []; + $curse->{menu_index} = {}; + my $wid = ++$curse->{wid}; + if (defined $menus) { + $curse->_parse_menus($wid, @$menus); + my @def = ( + 'vstack', border => 0, data => + ['hstack', border => 1, data => @{$curse->{menu_keys}}, ], + $def, + ); + $def = \@def; + } + $curse->{keypress} = {}; + $curse->{keylist} = []; + my $window = $curse->_parse_def($wid, @$def); + $window->{wid} = $wid; + $window; +} + +sub _finish_window { + my ($curse, $window) = @_; + $curse->{keyrows} = []; + $curse->{keycols} = []; + $curse->{lastkey} = [0, 0]; + if (@{$curse->{keylist}}) { + $curse->{keylist} = + [ sort { $a->{y} <=> $b->{y} || $a->{x} <=> $b->{x} } + @{$curse->{keylist}} ]; + for (my $kp = 0; $kp < @{$curse->{keylist}}; $kp++) { + my $k = $curse->{keylist}[$kp]; + push @{$curse->{keyrows}[$k->{y}]}, $kp; + push @{$curse->{keycols}[$k->{x}]}, $kp; + } + my $nmenu = @{$curse->{menu_keys} || []}; + $curse->{lastkey}[1] = $curse->{keylist}[$nmenu]; + $curse->{lastkey}[0] = $nmenu; + } + push @{$curse->{windows}}, [$window, @$curse{@savefields}]; + $curse->{in_menu} = 0; + $curse->{acter_act} = 0; + $window; +} + +sub show { + @_ == 2 or croak "Usage: Curses->show(WINDOW)"; + my ($curse, $window) = @_; + &{$window->{show}}($curse, $window); +} + +sub enable { + @_ == 2 or croak "Usage: Curses->enable(WINDOW)"; + my ($curse, $window) = @_; + $window->{enabled} = 1; + $curse->{redraw} = 1; +} + +sub disable { + @_ == 2 or croak "Usage: Curses->disable(WINDOW)"; + my ($curse, $window) = @_; + $window->{enabled} = 0; + $curse->{redraw} = 1; +} + +sub update { + @_ == 1 or croak "Usage: Curses->update"; + my ($curse) = @_; + refresh(); +} + +sub start { + @_ == 1 or croak "Usage: Curses->start"; + refresh(); +} + +sub run { + @_ == 1 or croak "Usage: Curses->run"; + my ($curse) = @_; + $curse->{running} = 1; + refresh(); + nodelay(0); + while ($curse->{running}) { + if ($curse->{resize}) { + $curse->{resize} = $curse->{redraw} = 0; + endwin(); + clearok(1); + $curse->_redraw(1); + } elsif ($curse->{redraw}) { + $curse->{redraw} = 0; + $curse->_redraw(0); + } + cbreak(); + meta(1); + while (! @{$curse->{pending}}) { + refresh(); + $curse->{server}->progress; + } + my $key = shift @{$curse->{pending}}; + if ($key eq "\c[") { + if (@{$curse->{pending}}) { + $key = shift @{$curse->{pending}}; + $key = chr(ord($key) | 0x80); + } + } + if (exists $reserved{$key}) { + &{$reserved{$key}}($curse); + next; + } + if (exists $curse->{keypress}{$key}) { + $key = $curse->{keypress}{$key}; + next unless $key->{enabled}; + if ($curse->{lastkey}[1] != $key && ! $key->{hidden}) { + my $ok = $curse->{lastkey}[1]; + $curse->{lastkey}[1] = $key; + for (my $kp = 0; $kp < @{$curse->{keylist}}; $kp++) { + next if $curse->{keylist}[$kp] != $key; + $curse->{lastkey}[0] = $kp; + } + $curse->show($ok); + } + $curse->show($key) unless $key->{hidden}; + $curse->{server}->progress(0) if ! @{$curse->{pending}}; + refresh() if ! @{$curse->{pending}}; + &{$key->{action}}; + $curse->{server}->progress(0) if ! @{$curse->{pending}}; + refresh() if ! @{$curse->{pending}}; + next; + } + if ($key =~ /^[[:print:]]$/ && $curse->{in_dialog}) { + ${$curse->{in_dialog}} .= $key; + $curse->set_text('__getline', ${$curse->{in_dialog}}); + $curse->update; + next; + } + } +} + +sub stop { + @_ == 1 or croak "Usage: Curses->stop"; + my ($curse) = @_; + $curse->{running} = 0; +} + +sub pending_events { + @_ == 1 or croak "Usage: Curses->pending_events"; + my ($curse) = @_; + if (! @{$curse->{pending}}) { + $curse->{server}->progress(0); + cbreak(); + } + return @{$curse->{pending}} != 0; +} + +sub _activate { + my ($curse) = @_; + if ($curse->{in_dialog}) { + $curse->{running} = 0; + return; + } + return unless $curse->{lastkey}[1]; + return unless $curse->{lastkey}[1]->{enabled}; + &{$curse->{lastkey}[1]->{action}}; +} + +sub _move_left { + my ($curse) = @_; + if ($curse->{in_menu}) { + # close this menu, then open the one on the left + $curse->close($curse->{in_menu}); + $curse->{in_menu} = 0; + return unless $curse->{lastkey}[1]; + _move_left($curse); + _activate($curse); + return; + } + return unless $curse->{lastkey}[1]; + my $i = $curse->{lastkey}[0]; + my $k = $curse->{lastkey}[1]; + my $r = $curse->{keyrows}[$k->{y}]; + my $ok = $curse->{lastkey}[1]; + if ($r->[0] == $i) { + $i = $#$r; + } else { + my $j = 1; + $j++ while $j < @$r && $r->[$j] != $i; + $j--; + $i = $j; + } + $curse->{lastkey}[0] = $r->[$i]; + $curse->{lastkey}[1] = $curse->{keylist}[$r->[$i]]; + $curse->show($ok); + $curse->show($curse->{lastkey}[1]); +} + +sub _move_right { + my ($curse) = @_; + if ($curse->{in_menu}) { + # close this menu, then open the one on the left + $curse->close($curse->{in_menu}); + $curse->{in_menu} = 0; + return unless $curse->{lastkey}[1]; + _move_right($curse); + _activate($curse); + return; + } + return unless $curse->{lastkey}[1]; + my $i = $curse->{lastkey}[0]; + my $k = $curse->{lastkey}[1]; + my $r = $curse->{keyrows}[$k->{y}]; + my $ok = $curse->{lastkey}[1]; + if ($r->[-1] == $i) { + $i = 0; + } else { + my $j = $#$r; + $j-- while $j >= 0 && $r->[$j] != $i; + $j++; + $i = $j; + } + $curse->{lastkey}[0] = $r->[$i]; + $curse->{lastkey}[1] = $curse->{keylist}[$r->[$i]]; + $curse->show($ok); + $curse->show($curse->{lastkey}[1]); +} + +sub _move_up { + my ($curse) = @_; + return unless $curse->{lastkey}[1]; + my $nmenu = @{$curse->{menu_keys} || []}; + my $i = $curse->{lastkey}[0]; + return if $i < $nmenu; + my $k = $curse->{lastkey}[1]; + my $r = $curse->{keycols}[$k->{x}]; + my $ok = $curse->{lastkey}[1]; + my $idx = 0; + $idx++ while $idx < @$r && $r->[$idx] < $nmenu; + if ($r->[$idx] == $i) { + $i = $#$r; + } else { + my $j = 1; + $j++ while $j < @$r && $r->[$j] != $i; + $j--; + $i = $j; + } + $curse->{lastkey}[0] = $r->[$i]; + $curse->{lastkey}[1] = $curse->{keylist}[$r->[$i]]; + $curse->show($ok); + $curse->show($curse->{lastkey}[1]); +} + +sub _down_until { + my ($curse, $until) = @_; + return unless $curse->{lastkey}[1]; + my $i = $curse->{lastkey}[0]; + do { + _move_down($curse); + } until $curse->{lastkey}[0] == $i + || $curse->{lastkey}[1]->{value} =~ $until; +} + +sub _move_down { + my ($curse) = @_; + return unless $curse->{lastkey}[1]; + my $i = $curse->{lastkey}[0]; + my $nmenu = @{$curse->{menu_keys} || []}; + if ($i < $nmenu) { + # open this menu + _activate($curse); + return; + } + my $k = $curse->{lastkey}[1]; + my $r = $curse->{keycols}[$k->{x}]; + my $ok = $curse->{lastkey}[1]; + my $idx = 0; + $idx++ while $idx < @$r && $r->[$idx] < $nmenu; + if ($r->[-1] == $i) { + $i = $idx; + } else { + my $j = $#$r; + $j-- while $j >= 0 && $r->[$j] != $i; + $j++; + $i = $j; + } + $curse->{lastkey}[0] = $r->[$i]; + $curse->{lastkey}[1] = $curse->{keylist}[$r->[$i]]; + $curse->show($ok); + $curse->show($curse->{lastkey}[1]); +} + +sub _redraw { + my ($curse, $place) = @_; + erase(); + $@ = ''; + for my $w (@{$curse->{windows}}) { + if ($place) { + eval { _place($w->[0], 0, $COLS, 0, $LINES) }; + last if $@; + } + &{$w->[0]{show}}($curse, $w->[0]); + } + if ($@) { + clearok(1); + erase(); + my $line = 0; + for my $s (split(/\n/, $@)) { + addstr($line++, 0, $s) if $line < $LINES; + } + } + refresh(); +} + +sub _offset { + my ($window, $x, $y) = @_; + $window->{x} += $x; + $window->{y} += $y; + return unless exists $window->{children}; + for my $child (@{$window->{children}}) { + _offset($child, $x, $y); + } +} + +sub _place { + my ($window, $x, $width, $y, $height) = @_; + my $diff = $width - $window->{width}; + $diff = 0 if $diff < 0; + $x += int($diff / 2); + $window->{x} ||= 0; + $diff = $height - $window->{height}; + $diff = 0 if $diff < 0; + $y += int($diff / 2); + $window->{y} ||= 0; + _offset($window, $x - $window->{x}, $y - $window->{y}); +} + +sub close { + @_ == 2 or croak "Usage: Curses->close(WINDOW)"; + my ($curse, $window) = @_; + $curse->_close($window->{wid}); + my @nw = grep { $_->[0] != $window } @{$curse->{windows}}; + $curse->{windows} = \@nw; + if (@nw) { + my $w; + ($w, @$curse{@savefields}) = @{$nw[-1]}; + } else { + @$curse{@savefields} = + ({}, {}, [], [], [0, 0], {}, {}, [], {}, 0, undef); + clearok(1); + $curse->_initialise; + } + $curse->_redraw(0); +} + +sub _extend_width { + my ($e, $cw) = @_; + return if $e->{width} >= $cw; + my $diff = $cw - $e->{width}; + $e->{width} = $cw; + return unless exists $e->{children}; + my $d0 = int($diff / scalar @{$e->{colwidth}}); + my $d1 = $diff % scalar @{$e->{colwidth}}; + my $d = 0; + my @d = (); + for (my $c = 0; $c < @{$e->{colwidth}}; $c++) { + $d[$c] = $d; + $d += $d0 + (($c < $d1) ? 1 : 0); + $e->{colwidth}[$c] += $d0 + (($c < $d1) ? 1 : 0); + } + for my $child (@{$e->{children}}) { + my ($c0, $c1, $r0, $r1) = @{$child->{table}}; + $d = -$e->{border}; + for (my $c = $c0; $c < $c1; $c++) { + $d += $e->{colwidth}[$c] + $e->{border}; + } + _extend_width($child, $d); + _offset($child, $d[$c0], 0); + } +} + +sub _extend_height { + my ($e, $rh) = @_; + return if $e->{height} >= $rh; + my $diff = $rh - $e->{height}; + $e->{height} = $rh; + return unless exists $e->{children}; + my $d0 = int($diff / scalar @{$e->{rowheight}}); + my $d1 = $diff % scalar @{$e->{rowheight}}; + my $d = 0; + my @d = (); + for (my $r = 0; $r < @{$e->{rowheight}}; $r++) { + $d[$r] = $d; + $d += $d0 + (($r < $d1) ? 1 : 0); + $e->{rowheight}[$r] += $d0 + (($r < $d1) ? 1 : 0); + } + for my $child (@{$e->{children}}) { + my ($c0, $c1, $r0, $r1) = @{$child->{table}}; + $d = -$e->{border}; + for (my $r = $r0; $r < $r1; $r++) { + $d += $e->{rowheight}[$r] + $e->{border}; + } + _extend_height($child, $d); + _offset($child, 0, $d[$r0]); + } +} + +sub _make_table { + my ($curse, $rows, $cols, $elements, $border, $augment) = @_; + my @width = (0) x $cols; + my @height = (0) x $rows; + $border = $border ? 1 : 0; + # try to determine row/column sizes using one cell elements + for my $te (@$elements) { + my ($e, $c0, $c1, $r0, $r1) = @$te; + $width[$c0] = $e->{width} + if $c0 + 1 == $c1 && $width[$c0] < $e->{width}; + $height[$r0] = $e->{height} + if $r0 + 1 == $r1 && $height[$r0] < $e->{height}; + } + # now adjust it for multirow/multicolumn + for my $te (@$elements) { + my ($e, $c0, $c1, $r0, $r1) = @$te; + if ($c1 - $c0 > 1) { + my $cw = ($c1 - $c0 - 1) * $border; + for (my $c = $c0; $c < $c1; $c++) { + $cw += $width[$c]; + } + if ($cw < $e->{width}) { + my $diff = $e->{width} - $cw; + my $d0 = int($diff / ($c1 - $c0)); + my $d1 = $diff % ($c1 - $c0); + for (my $c = $c0; $c < $c1; $c++) { + $width[$c] += $d0; + $width[$c] ++ if $c < $d1; + } + } + } + if ($r1 - $r0 > 1) { + my $rh = ($r1 - $r0 - 1) * $border; + for (my $r = $r0; $r < $r1; $r++) { + $rh += $height[$r]; + } + if ($rh < $e->{height}) { + my $diff = $e->{height} - $rh; + my $d0 = int($diff / ($r1 - $r0)); + my $d1 = $diff % ($r1 - $r0); + for (my $r = $r0; $r < $r1; $r++) { + $height[$r] += $d0; + $height[$r] ++ if $r < $d1; + } + } + } + } + # determine total window size and cell starting points + my $width = $border; + my @x = (); + for (my $c = 0; $c < $cols; $c++) { + $x[$c] = $width; + $width += $width[$c] + $border; + } + my $height = $border; + my @y = (); + for (my $r = 0; $r < $rows; $r++) { + $y[$r] = $height; + $height += $height[$r] + $border; + } + # place all elements and extend them to fill cell if required + my @children = (); + for my $te (@$elements) { + my ($e, $c0, $c1, $r0, $r1) = @$te; + _offset($e, $x[$c0], $y[$r0]); + my $cw = ($c1 - $c0 - 1) * $border; + for (my $c = $c0; $c < $c1; $c++) { + $cw += $width[$c]; + } + _extend_width($e, $cw); + my $rh = ($r1 - $r0 - 1) * $border; + for (my $r = $r0; $r < $r1; $r++) { + $rh += $height[$r]; + } + _extend_height($e, $rh); + $e->{table} = [$c0, $c1, $r0, $r1]; + push @children, $e; + } + # ready to go... + return { + type => 'table', + width => $width, + height => $height, + colwidth => \@width, + rowheight => \@height, + show => \&_show_table, + children => \@children, + border => $border, + }; +} + +sub _show_table { + my ($curse, $table) = @_; + $table->{type} eq 'table' or die "Internal error"; + # draw border, if required + # XXX multirow fields may show '+' where '|' should be + if ($table->{border}) { + my $y = $table->{y}; + my $row = 0; + for my $rh (@{$table->{rowheight}}, 0) { + move($y, $table->{x}); + my $col = 0; + for my $cw (@{$table->{colwidth}}, 0) { + my $plus = '-'; + for my $e (@{$table->{children}}) { + next if $e->{table}[0] != $col && $e->{table}[1] != $col; + next if $e->{table}[2] != $row && $e->{table}[3] != $row; + $plus = '+'; + last; + } + addstr($plus . ('-' x $cw)); + $col++; + } + $y++; + for (my $x = 0; $x < $rh; $x++) { + move($y, $table->{x}); + for my $cw (@{$table->{colwidth}}, 0) { + addstr('|' . (' ' x $cw)); + } + $y++; + } + $row++; + } + } + # draw elements + for my $e (@{$table->{children}}) { + &{$e->{show}}($curse, $e); + } +} + +sub _make_text { + my ($curse, $value, $align, $size) = @_; + $size ||= length $value; + return { + type => 'text', + width => $size, + height => 1, + value => $value, + enabled => 1, + align => $align, + show => \&_show_text_key, + }; +} + +sub _show_text_key { + my ($curse, $text) = @_; + $text->{type} eq 'text' || $text->{type} eq 'key' + or die "Internal error"; + move($text->{y}, $text->{x}); + my $diff0 = $text->{width} - length($text->{value}); + my $diff1 = int($diff0 / 2); + my $diff2 = $diff0 - $diff1; + eval { attrset(A_NORMAL) }; + eval { attron(A_BOLD) } if $text->{enabled}; + eval { attron(A_REVERSE) } if $text == $curse->{lastkey}[1]; + addstr(' ' x $diff0) if $diff0 > 0 && $text->{align} =~ /^r/i; + addstr(' ' x $diff1) if $diff1 > 0 && $text->{align} =~ /^c/i; + addstr($text->{value}); + addstr(' ' x $diff0) if $diff0 > 0 && $text->{align} =~ /^l/i; + addstr(' ' x $diff2) if $diff2 > 0 && $text->{align} =~ /^c/i; + eval { attrset(A_NORMAL) }; +} + +sub _set_text { + my ($curse, $text, $value) = @_; + $text->{type} eq 'text' or die "Internal error"; + defined $value or $value = ''; + $value = substr($value, 0, $text->{width}); + $text->{value} = $value; + _show_text_key($curse, $text); +} + +sub _get_text { + my ($curse, $text) = @_; + $text->{type} eq 'text' or die "Internal error"; + $text->{value}; +} + +sub _make_key { + my ($curse, $label, $action, $keys) = @_; + if ($curse->{after_act}) { + my $act = $curse->{after_act}; + my $cb = $action; + $action = sub { + $@ = ''; + my $res = eval { $cb->(@_); }; + if ($act) { + $act->($curse, $@ || $res, @_); + } elsif ($@) { + die $@; + } + }; + } + my $key = { + type => 'key', + width => length $label, + height => 1, + action => $action, + align => ($curse->{keyalign} || 'c'), + enabled => 1, + value => $label, + show => \&_show_text_key, + }; + push @{$curse->{keylist}}, $key; + for my $k (@$keys) { + $k = $keymap{$k} if exists $keymap{$k}; + next if exists $reserved{$k}; + $curse->{keypress}{$k} = $key; + }; + return $key; +} + +sub _make_menu { + my ($curse, $name) = @_; + $curse->{menu_byname}{$name} = {}; + $curse->{menu_entries}{$name} = []; + my $key1 = 'M-' . lc(substr($name, 0, 1)); + my $key2 = 'M-' . uc(substr($name, 0, 1)); + $curse->{menu_index}{$name} = scalar @{$curse->{menu_keys}}; + push @{$curse->{menu_keys}}, [ + 'key', + name => $name, + action => sub { _show_menu($curse, @_) }, + key => [$key1, $key2], + ]; + 1; +} + +sub _show_menu { + my ($curse, $name) = @_; + # find this menu + exists $curse->{menu_index}{$name} or return; + my $entry = $curse->{menu_index}{$name}; + # check if menu has ticks + my $c = $curse->{menu_byname}{$name}; + my $ticks = grep { exists $_->{ticked} } values %$c; + # get list of entries; + my $e = $curse->{menu_entries}{$name}; + my @entries = grep { $c->{$_->[0]}{enabled} } @$e; + return unless @entries; + if ($ticks) { + @entries = + map { [($c->{$_->[0]}{ticked} ? '*' : ' ') . $_->[0], + $_->[0], + $_->[1]] + } @entries; + } else { + @entries = map { [$_->[0], $_->[0], $_->[1]] } @entries; + } + # determine menu size and draw window + my $rows = scalar @entries; + my $cols = 0; + for my $e (@entries) { + $cols = length($e->[0]) if $cols < length($e->[0]); + } + # now open a window under the menu label with the entries as a stack of buttons + my $mw; + my $act = $curse->{after_act}; + my @keys = map { + my ($label, $keyname, $action) = @$_; + [ 'key', + action => sub { + $curse->close($mw); + $@ = ''; + my $res = eval { $action->($curse, $name, @_); }; + if ($act) { + $act->($curse, $@ || $res, $name, @_); + } elsif ($@) { + die $@; + } + }, + name => $keyname, + label => $label, + key => [], + ], + } @entries; + my @wd = ( + 'vstack', + border => 1, + data => [ + 'vstack', + border => 0, + data => @keys, + ], + ); + my $k = $curse->{keylist}[$entry]; + $curse->{keyalign} = 'l'; + $mw = $curse->_window($name, \@wd); + delete $curse->{keyalign}; + $curse->{keypress}{"\c["} = { + hidden => 1, + action => sub { $curse->close($mw) }, + enabled => 1, + }; + for my $ent (@entries) { + my $initial = lc(substr($ent->[1], 0, 1)); + next if exists $curse->{keypress}{$initial}; + $curse->{keypress}{$initial} = { + hidden => 1, + enabled => 1, + action => sub { _down_until($curse, qr/^[\s\*]*$initial/i) }, + } + } + _offset($mw, $k->{x} - 1, $k->{y} + 1); + _finish_window($curse, $mw); + $curse->{in_menu} = $mw; + &{$mw->{show}}($curse, $mw); +} + +sub _make_menu_entry { + my ($curse, $action, $menu, $name, $entry, $ticks) = @_; + $curse->{menu_byname}{$name}{$entry} = { + action => $action, + enabled => 1, + }; + push @{$curse->{menu_entries}{$name}}, [$entry, $action]; + 1; +} + +sub _enable_menu { + my ($curse, $item, $state, $name, $entry) = @_; + $curse->{menu_byname}{$name}{$entry}{enabled} = $state; + 1; +} + +sub _tick_menu { + my ($curse, $item, $state, $name, $entry) = @_; + $curse->{menu_byname}{$name}{$entry}{ticked} = $state; + 1; +} + +sub _menu_action { + my ($curse, $item, $name, $entry) = @_; + exists $curse->{menu_byname}{$name}{$entry} or return 0; + $curse->{menu_byname}{$name}{$entry}{enabled} or return 0; + my $action = $curse->{menu_byname}{$name}{$entry}{action}; + $action or return 0; + $action->($curse, $name, $entry); +} + +1;