package HexedUI; use strict; use warnings; use include; use Hexed::Util; use Curses; use Hexed::Fixed; use Hexed::Term; use Hexed::Map; use File::Slurp; # Clean up the terminal when we're finished. sub cleanup { system("clear"); endwin; exit; } END { cleanup } $SIG{INT} = \&cleanup; # Make an interface to one or more windows. sub create { my ($class, $layout) = @_; # Initialize ncurses. initscr; start_color; noecho; keypad(1); curs_set(0); # The prompt can turn it back on # For whatever reason, we have to push a key onto the buffer and then read it # off so that getch() doesn't screw up the display. ungetch("."); getch; # Yo mama uses a monochrome terminal. my @colors = ( qw(BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE) ); # Magically initialize all possible combos of foregrounds and backgrounds. my $cnt = 0; no strict "refs"; # Naughty! foreach my $bg (@colors) { foreach my $fg (@colors) { init_pair(++$cnt, &{ "COLOR_$fg" }, &{ "COLOR_$bg" }); } } use strict "refs"; # Size *does* matter! getmaxyx(my $rows, my $cols); my $self = bless {}, $class; # HexedUI provides support for temporary popups of various sorts. We need a # window the size of our whole screen to do that. Thus, a window named # "Screen" is reserved. die "The window called Screen is ours! Rawr!\n" if $layout->{Screen}; $layout->{Screen} = { Type => "Fixed", At => [0, 0], Size => ["100%", "100%"], Border => 1 }; # We don't have to worry about hiding this window or anything since its # interior is blank and its border matches up with individual borders of # other windows. (Presumably.) TODO: guarentee this. # Likewise, a fullscreen pad is needed. die "The window called FullScreen is ours! Rawr!\n" if $layout->{FullScreen}; $layout->{FullScreen} = { Type => "Term", At => [0, 0], Size => ["100%", "100%"], Border => 1, Prompt => 0 }; # Create each window in the layout. while (my ($name, $dat) = each %$layout) { # Any time a size or position says a percent, change that to whatever # percent of either the actual rows or columns of our terminal is. $dat->{Size}[0] =~ s#(\d+)%#$1 * $rows / 100#; $dat->{Size}[1] =~ s#(\d+)%#$1 * $cols / 100#; $dat->{At}[0] =~ s#(\d+)%#$1 * $rows / 100#; $dat->{At}[1] =~ s#(\d+)%#$1 * $cols / 100#; # And evaluate that value. my $sizeY = int eval($dat->{Size}[0]) - 1; my $sizeX = int eval($dat->{Size}[1]) - 1; my $posY = int eval($dat->{At}[0]) - 1; my $posX = int eval($dat->{At}[1]) - 1; # Arrays have an offset of 0, but ncurses' windows start at 1. my $win = newwin($sizeY + 1, $sizeX + 1, $posY + 1, $posX + 1); $win->box(0, 0) if $dat->{Border}; $win->refresh unless $name eq "Screen" or $name eq "FullScreen"; # If there's a border, then there's automatically an X and Y offset of 1 # each side. So we lose 2 rows and 2 columns. Reflect that visually. # Additionally, the window itself may have more padding. my $off = 0; $off = 2 if $dat->{Border}; $off += 2 if $dat->{Pad}; my $handler = "Hexed::$dat->{Type}"; my %dat = ( # These are the coordinates of what we're actually allowed to draw in. Y1 => $posY + ($off / 2) + 1, X1 => $posX + ($off / 2) + 1, Height => $sizeY - $off, Width => $sizeX - $off ); $dat{Y2} = $dat{Y1} + $dat{Height}; $dat{X2} = $dat{X1} + $dat{Width}; $self->{$name} = $handler->init({ Win => $win, Border => $dat->{Border}, Off => $off, %dat, Opts => $dat }); } return $self; } # After using the fullscreen, redraw all the windows. sub restore { my $self = shift; $self->{Screen}->cls; foreach my $win (keys %$self) { next if $win eq "Screen" or $win eq "FullScreen"; $self->{$win}{Win}->box(0, 0) if $self->{$win}{Border}; $self->{$win}->draw(1); } return 1; } # Display a pop-up message in the center of the screen. sub btw { my ($ui, $msg) = @_; my $self = $ui->{Screen}; # Make sure there's room! my $width = int($self->{Width} / 2); my @lines = wrap($width, $msg); die "The popup message is too big!\n" if $#lines > $self->{Height}; # Visually pad the message box with an extra space. # First find the longest line. my $long = 0; foreach (@lines) { $long = length($_) if length($_) > $long; } $long += 2; # to account for the pad. # Now pad each line. @lines = ("", @lines, ""); foreach (0 .. $#lines) { $lines[$_] = " $lines[$_]"; $lines[$_] .= " " x ($long - length($lines[$_])); } # Make the Screen visible... $self->cls; # Draw the message in the center of the screen. $self->place("center", "center", join("\n", @lines), CONFIG->box_color); $self->draw; # Now leave the message up there till the user presses a key. my $in = $ui->input; $ui->restore; return $in; } # Allows complete editing of text through an external editor. sub edit { my ($self, $text) = @_; # Put the text somewhere. my $tmp = CONFIG->tmp_file; my $editor = CONFIG->editor; open my $fh, ">$tmp" or die "Can't write to $tmp: $!\n"; print $fh "$text\n"; close $fh; # Very lazily just delegate our task... But we do have to kill curses for a # bit. def_prog_mode; endwin; system($editor, $tmp); reset_prog_mode; refresh; # And suck the text back in! Restore the screen, too. my @edited = read_file($tmp); @edited = map { s/\n//; $_ } @edited; unlink $tmp; $self->restore; if (wantarray) { return @edited; } else { return join " ", @edited; } } # Invisible one-character input. sub input { return getch; } # Display fullscreen stuff. sub fs { my ($self, @msgs) = @_; my $win = $self->{FullScreen}; $win->{Win}->box(0, 0) if $win->{Border}; # Display all the messages. $win->msg($_) foreach @msgs; # Draw it. $win->draw(1); # Pause... $self->input; # Clear it and make a new pad for next time. $win->{OffY} = $win->{Total} = 0; $win->{Msgs} = []; $win->{Pad}->clear; $self->restore; return 1; } 42;