#!/usr/bin/perl

# porky.pl is a ncurses based log viewer written in perl. It's not finished
# yet, and I don't believe that i will finish it :-)
#
# Copyright 2009 urug <urug@urug.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.

use warnings;
use strict;
use POSIX;
use Curses;
use Data::Dumper;
use constant MIN_LINES => 23;
use constant MIN_COLUMNS => 80;
use constant NAME => "PORKY";
use constant VERSION => "0.0001";
use constant AUTHOR => "urug <urug\@urug.net>";

my $conf_dir = "$ENV{HOME}/.porkyviewer";
my $conf_filters = "$conf_dir/filters.cfg";

my $copyright = <<EOF;
urug <urug\@urug.net>
porky.pl, curses based log viewer

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
EOF

my $help = <<EOF;
 [up / down]      move cursor up/down
 [pgup / pgdown]  prev/next page
 [home / end]     first/last page
 [left / right]   scroll text left/right

 [/]    find text                   [F1..FN]    switch to N'th buffer
 [n]    find next                   [k]         kill current buffer
 [l]    go to line                  [s]         save current buffer to file
                                    [i]         reopen initial buffer
                                    [r]         reverse buffer

 [f]    filters
 [g]    groups (May not work if log format is not recognized)

 [c]    copyright
 [q]    quit program
EOF

my $banner = <<EOF;
Porky can read any log file, but grouping works only on syslog format.

$0 <file.log>
EOF

die NAME." ".VERSION.", ".AUTHOR."\n$banner" if ( $#ARGV != 0 );

# Odczytaj filtry i logi przed inicjalizacja ncurses.
my %filters = conf_load();
my $syslog = logs_load($ARGV[0]);

initscr();
noecho();
cbreak();
keypad(1);

# Mniej wiecej minimalny rozmiar terminala
if ( $COLS < MIN_COLUMNS || $LINES < MIN_LINES ) {
    endwin();
    print STDERR "Your term is too small. Minimal term size is: ", MIN_COLUMNS, "x", MIN_LINES, "\n";
    exit 1;
}

show_logs($syslog, \%filters);
endwin();

# Zrzuc filtry na dysk
conf_save(\%filters);


#* Glowne okno, w ktorym wyswielane sa wszystkie bufory z logami
sub show_logs {
    my $syslog_whole = shift;
    my $filters = shift;

    # Domyslny bufor w ktorym wyswietlane jest wszystko co odczytano
    my %initial_buffer = ( filter => '',
                           group => '',
                           pos => 0,
                           page_start => 0,
                           scroll => 0,
                           logs => $syslog_whole );

    # Tablica przechowujaca bufory
    my @buffers = ();

    # Pierwszy bufor
    my $buf_pos;
    buffer_add(\@buffers, \$buf_pos, { %initial_buffer });

    # Poszukiwany ciag znakow (moze byc regex)
    my $search_for = '';
    my $search_for_old = '';

    # Utworz okienko logow i okienko statystyk :)
    my ($stats_panel, $stats_text) = panel_create_stats();
    my ($main_panel, $main_text) = panel_create_logs();

    while ( 1 ) {
        my $cur_buf = $buffers[$buf_pos];

        if ( $search_for ) {
            logs_search_for($cur_buf->{logs}, \$cur_buf->{pos}, $search_for);
            $search_for_old = $search_for;
            $search_for = '';
        }

        panel_insert_stats($stats_text, \@buffers, $buf_pos);
        panel_insert_tab($main_text, $cur_buf->{scroll}, $cur_buf->{logs},
                        \$cur_buf->{page_start}, $cur_buf->{pos}, \&elem_logs);

        panel_refresh($stats_panel);
        panel_refresh($main_panel);

        my $key = getch();

        if ( keys_updown($key, scalar @{$cur_buf->{logs}}, \$cur_buf->{pos},
                        \$cur_buf->{page_start}, getmaxy($main_text)) ) {
            next;
        } elsif ( keys_leftright($key, \$cur_buf->{scroll}) ) {
            next;
        } elsif ( keys_close($key) ) {
            last;
        } elsif ( keys_function($key, \@buffers, \$buf_pos) ) {
            next;
        } elsif ( $key eq 'h' ) {
            show_warning("HELP", $help, "BIG");
        } elsif ( $key eq 'c' ) {
            show_warning("COPYRIGHT", $copyright, "BIG");
        } elsif ( $key eq '/' ) {
            $search_for = show_dialog("Search for", "Press [ENTER] to apply");
        } elsif ( $key eq 'n' ) {
            $search_for = $search_for_old;
        } elsif ( $key eq 'l' ) {
            show_gotoline(\$cur_buf->{pos}, scalar @{$cur_buf->{logs}});
        } elsif ( $key eq 'f' ) {
            buffer_add(\@buffers, \$buf_pos, show_filters($cur_buf, $filters));
        } elsif ( $key eq 'g' ) {
            buffer_add(\@buffers, \$buf_pos, show_groups($cur_buf));
        } elsif ( $key eq 'k' ) {
            buffer_delete(\@buffers, \$buf_pos);
        } elsif ( $key eq 's' ) {
            buffer_save($cur_buf);
        } elsif ( $key eq 'i' ) {
            buffer_add(\@buffers, \$buf_pos, { %initial_buffer });
        } elsif ( $key eq 'r' ) {
            $cur_buf->{logs} = [ reverse @{$cur_buf->{logs}} ];
        }
    }

    panel_delete($main_panel, $main_text);
    panel_delete($stats_panel, $stats_text);
}

#* Wyswietla okienko "idz do linii"
sub show_gotoline {
    my $tab_pos = shift;
    my $tab_size = shift;

    my $pos = show_dialog("Go to line", "Press [ENTER] to apply");

    $$tab_pos = $pos-1 if ( $pos && $pos >= 1 && $pos <= $tab_size );
}

#* Wyswietla listbox
sub show_listbox {
    my $tab = shift;
    my $handle_other_keys = shift;
    my $title = shift;
    my $hints = shift;

    my $width = defined $hints ? length($hints) + 2 : 22;
    my $height = @$tab + 7;
    my ($panel, $text) = panel_create_listbox($title, $hints, $height, $width);

    my $selected = 0;
    my $start_point = 0;
    my $result = 0;
    my $scroll = 0;
    my $key = '';

    OUTER: while ( 1 ) {
        panel_insert_tab($text, $scroll, $tab, \$start_point, $selected, \&elem_scalar);
        panel_refresh($panel);

        $key = getch();

        if ( keys_updown($key, scalar @$tab, \$selected, \$start_point, getmaxy($text)) ) {
            next;
        } elsif ( keys_leftright($key, \$scroll) ) {
            next;
        } elsif ( $key eq "\n" && scalar @$tab > 0 ) {
            last;
        } else {
            for ( @$handle_other_keys ) {
                last OUTER if ( $key eq $_ );
            }
        }
    }

    panel_delete($panel, $text);
    return ($key, $selected);
}

#* Wyswietla grupy
sub show_groups {
    my $buffer = shift;
    my @groups = ('service', 'date', 'date, hour');
    my $result = {};

    while ( 1 ) {
        my ($key, $index) = show_listbox(\@groups, ['q'], "GROUP BY", "Press [q] to quit");

        if ( keys_close($key) ) {
            last;
        } elsif ( $key eq "\n" ) {
            $result = show_group($buffer, [split /, /, $groups[$index] ]);
            last if ( scalar keys %$result > 0 );
        }
    }

    return $result;
}

#* Wyswietl konkretna grupe (gdzie grupa, to klucz w tablicy asocjacyjnej
#* logs)
sub show_group {
    my $buffer = shift;
    my $keys = shift;

    my %all_groups = ();
    for my $log (@{$buffer->{logs}}) {
        my $group = {};

        $group->{$_} = $log->{$_} for ( @$keys );

        $all_groups{join(", ", sort { $b cmp $a } values %$group)} = $group;
    }

    my @keys = sort { $b cmp $a } keys %all_groups;
    my %new_buf = ();

    while ( 1 ) {
        my ($key, $index) = show_listbox(\@keys, ['q'], "GROUP BY", "Press [q] to quit");

        if ( keys_close($key) ) {
            last;
        } elsif ( $key eq "\n" ) {
            $new_buf{filter} = $buffer->{filter};
            $new_buf{group} = join(", ", values %{$all_groups{$keys[$index]}});
            $new_buf{logs} = logs_group($buffer->{logs}, $all_groups{$keys[$index]});
            $new_buf{pos} = 0;
            $new_buf{page_start} = 0;
            $new_buf{scroll} = 0;
            last;
        }
    }

    return \%new_buf;
}

#* Okienko z pytaniem tak/nie
sub show_yesno {
    my @tab = qw(yes no);

    my ($key, $index) = show_listbox(\@tab, [], "Are you sure?", undef);

    return $tab[$index];
}

#* Okienko filtrow
sub show_filters {
    my $buffer = shift;
    my $filters = shift;

    my %new_buf = ();

    while ( 1 ) {
        my @tab = keys %$filters;
        my ($key, $index) = show_listbox(\@tab, ['q', 'n', 'r'], "FILTERS", "[n]ew [r]emove [q]uit");

        if ( keys_close($key) ) {
            last;
        } elsif ( $key eq "\n" ) {
            $new_buf{filter} = $tab[$index];
            $new_buf{group} = $buffer->{group};
            $new_buf{logs} = logs_filter($buffer->{logs}, $tab[$index], $filters->{$tab[$index]});
            $new_buf{pos} = 0;
            $new_buf{page_start} = 0;
            $new_buf{scroll} = 0;
            last;
        } elsif ( $key eq 'n' ) {
            filter_new($filters);
        } elsif ( $key eq 'r' && scalar @tab > 0 ) {
            filter_delete($filters, $tab[$index]);
        }
    }

    return \%new_buf
}

#* Okienko w ktorym mozna wpisac jakas informacje
sub show_dialog {
    my $title = shift;
    my $hints = shift;

    my ($panel, $text) = panel_create_dialog($title, $hints);
    panel_refresh($panel);

    echo();
    my $str = "";
    getstr($text, $str);
    noecho();

    panel_delete($panel, $text);
    return $str;
}

# Pokazuje okienko informacyjne, ktore obsluguje tylko klawisz 'q'
sub show_warning {
    my $title = shift;
    my $warning = shift;
    my $size = shift;

    # Wybierz rozmiar, maly albo duzy :P
    my ($width, $height) = $size && $size eq "BIG" ? (80, 23) : (50, 10);

    my ($panel, $text) = panel_create_textbox($width, $height, $title, $warning);
    panel_refresh($panel);

    keypad(0);
    while ( 1 ) {
        last if ( keys_close(getch()) );
    }

    keypad(1);
    panel_delete($panel, $text);
}

#* Wyswietla statystyki w wybranym panelu
sub panel_insert_stats {
    my $win = shift;
    my $buffers = shift;
    my $cur_buf_no = shift;

    my $buffers_amount = @$buffers;
    my $tab_pos = $buffers->[$cur_buf_no]->{pos};
    my $tab_size = @{$buffers->[$cur_buf_no]->{logs}};
    my $filter_name = $buffers->[$cur_buf_no]->{filter};
    my $group_name = $buffers->[$cur_buf_no]->{group};
    $cur_buf_no++;

    # Numeruj linie od 0, ale tylko wtedy gdy jakiekolwiek sa wyswietlone
    $tab_pos++ if $tab_size > 0;

    my $text = sprintf("Buffer: %-7s Line: %-13s Filter: %-7s Group: %s",
                       "[$cur_buf_no/$buffers_amount]",
                       "[$tab_pos/$tab_size]", "[$filter_name]",
                       "[$group_name]");

    clear($win);
    attrset($win, A_BOLD);
    addstr($win, $text);
    attroff($win, A_BOLD);
}

#* Wyswietla zawartosc jakies tablicy w wybranym oknie
sub panel_insert_tab {
    my $win = shift;
    my $scroll_text = shift;
    my $tab = shift;
    my $start_point = shift;
    my $tab_pos = shift;
    my $func = shift;

    clear($win);

    my $max_lines = getmaxy($win);
    my $max_chars = getmaxx($win);

    # Jesli gdzies jest bug i ustawia tab_pos < 0, popraw to
    $tab_pos = 0 if ( $tab_pos < 0 );

    # Skalibruj okno, jesli zaznaczona pozycja wybiega poza
    # wyswietlany obszar.
    if ( $tab_pos - $$start_point > $max_lines - 1 ) {
        $$start_point = $tab_pos;
    } elsif ( $tab_pos < $$start_point ) {
        $$start_point = $tab_pos;
    }

    my $i = $$start_point;
    my $posY = 0;

    while ( $posY < $max_lines && $i < @$tab ) {
        # Jezeli user juz wyscrollowal poza dlugosc string'a,
        # to nie ma sensu go wyswietlac :-P
        next if ( $scroll_text >= length $func->($tab->[$i]) );

        my $msg = substr($func->($tab->[$i]), $scroll_text, $max_chars);

        attrset($win, A_REVERSE) if ( $i == $tab_pos );
        addstr($win, $posY, 0, $msg);
        attroff($win, A_REVERSE) if ( $i == $tab_pos );
    }
    continue {
        $i++;
        $posY++;
    }
}

#* Procedurka do usuwania paneli
sub panel_delete {
    my $panel = shift;
    my $text = shift;

    my $win = panel_window($panel);
    del_panel($panel);
    delwin($text);
    delwin($win);
}

#* Uniwersalna procedurka do tworzenia ladnych paneli
sub panel_create {
    my $height = shift;
    my $width = shift;
    my $x = shift;
    my $y = shift;
    my $title = shift;
    my $hints = shift;

    # Okno zewnetrzne, stanowiace ramke
    my $win = newwin($height, $width, $x, $y);
    box($win, ACS_VLINE, ACS_HLINE);

    # Polozenie okna wewnetrznego, sluzacego do pisania tekstu
    my $textY = 1;
    my $textHeight = $height - 2;

    # Tytul (Na gorze)
    if ( defined $title ) {
        attrset($win, A_BOLD);
        addstr($win, 1, 1, "-- $title --");
        attroff($win, A_BOLD);

        hline($win, 2, 1, ACS_HLINE, $width-2);

        $textY += 2;
        $textHeight -= 2;
    }

    # Pomoc, albo opis przyciskow (na dole)
    if ( defined $hints ) {
        attrset($win, A_BOLD);
        addstr($win, $height-2, 1, "$hints");
        attroff($win, A_BOLD);

        hline($win, $height-3, 1, ACS_HLINE, $width-2);

        $textHeight -= 2;
    }

    my $text = derwin($win, $textHeight, getmaxx($win)-2, $textY, 1);
    return (new_panel($win), $text);
}

#* Odswiez jeden panel
sub panel_refresh {
    my $panel = shift;

    # To jest wymagane, by odswiezyc okna znajdujace sie wewnatrz panelu
    touchwin(panel_window($panel));

    update_panels();
    doupdate();
}

#* Tworzy okienko, ktorego przeznaczeniem jest interakcja z uzytkownikiem
sub panel_create_dialog {
    my $title = shift;
    my $hints = shift;

    my ($panel, $text) = panel_create(10, 60, $LINES/2-4, $COLS/2-25, $title, $hints);
    return ($panel, $text);
}

#* Tworzy panel, ktorego przeznaczeniem jest wyswietlanie list
sub panel_create_listbox {
    my $title = shift;
    my $hints = shift;
    my $height = shift;
    my $width = shift;

    my ($panel, $text) = panel_create($height, $width, $LINES/2-$height/2, $COLS/2-$width/2, $title, $hints);
    return ($panel, $text);
}

#* Tworzy prosty panel informacyjny, o podanej wielkosci
sub panel_create_textbox {
    my $width = shift;
    my $height = shift;
    my $title = shift;
    my $information = shift;

    my $hints = "Press [q] to close window";

    my ($panel, $text) = panel_create($height, $width, $LINES/2-$height/2, $COLS/2-$width/2, $title, $hints);
    addstr($text, $information);

    return ($panel, $text);
}

#* Tworzy i zwraca panel bez tytulu i pomocy, z sama obwodka. Sluzy
#* do wyswietlania statystyk
sub panel_create_stats {
    return panel_create(3, $COLS, $LINES-3, 0);
}

#* Tworzy glowny panel z logami
sub panel_create_logs {
    my $title = NAME . " " . VERSION . " [". localtime() . "]";

    my ($panel, $text) = panel_create($LINES-3, $COLS, 0, 0, $title);
    addstr(panel_window($panel), $LINES-4, $COLS-23, "[ Press 'h' for help ]\r");

    return ($panel, $text);
}

#* Odczytaj logi z podanych plikow. Zwraca tablice tablic asocjacyjnych.
sub logs_load {
    my $fname = shift;

    my @logs = ();
    my $syslog_regex = qr/^([[:alpha:]]{3})\s*      # day
                           ([[:digit:]]{1,3})\s*    # day of month
                           ([[:digit:]:]{2}):       # hour
                           ([[:digit:]:]{2}):       # min
                           ([[:digit:]:]{2})\       # sec
                           ([[:word:]-]*)\          # hostname
                           ([[:word:]\/]*)          # service
                          /x;

    open(FILE, $fname) or die "$fname: $!.\n";

    while(<FILE>) {
        my %log_line = ();

        $log_line{whole} = $_;
        if ( ! /$syslog_regex/ ) {
            $log_line{date} = "UNKNOWN";
            $log_line{hour} = "UNKNOWN",
            $log_line{service} = "UNKNOWN";
        } else {
            $log_line{date} = "$1 $2";
            $log_line{hour} = $3 || "UNKNOWN",
            $log_line{service} = $7 || "UNKNOWN",
        }

        push @logs, \%log_line;
    }

    close FILE;
    return \@logs;
}

#* Utworz nowa tablice logow na podstawie filtru (wyrazenie regularne)
sub logs_filter {
    my $logs = shift;
    my $fname = shift;
    my $fexpr = shift;

    my $regex = eval { qr|$fexpr| };
    if ( ! defined $regex ) {
        show_warning("ERROR", "$@\n");
        return;
    }

    my @new_tab = ();

    # Jesli pierwsza litera nazwy, to '!', odwroc warunek
    if ( substr($fname, 0, 1) eq "!" ) {
        @new_tab = grep { elem_logs($_) !~ $regex } @$logs;
    } else {
        @new_tab = grep { elem_logs($_) =~ $regex } @$logs;
    }

    return \@new_tab;
}

#* Wyszukaj logi, o okreslonych wartosciach dla wybranych kluczy
sub logs_group {
    my $logs = shift;
    my $group_by = shift;

    my @keys = keys %$group_by;
    my @new_tab = grep {
        my $log = $_;
        scalar @keys == grep { $log->{$_} eq $group_by->{$_} } @keys;
    } @$logs;

    return \@new_tab;
}

#* Wyszukuje podanego ciagu w tablicy logow
sub logs_search_for {
    my $logs = shift;
    my $logs_cur_pos = shift;
    my $search_for = shift;

    my $regex = eval { qr|$search_for| };
    if ( ! defined $regex ) {
        show_warning("ERROR", "$@\n");
        return;
    }

    for ( my $i = $$logs_cur_pos + 1; $i < @$logs; $i++ ) {
        if ( elem_logs($logs->[$i]) =~ $regex ) {
            $$logs_cur_pos = $i;
            return;
        }
    }

    # Jesli nie znaleziono, to wyswietl ostrzezenie
    show_warning("WARNING", "\"$search_for\" not found");
}

#* Zwroc podany element z tablicy logow (Uzywany jako ref na funkcje)
sub elem_logs {
    my $log = shift;

    return "$log->{whole}";
}

#* Zwroc zawartosc skalara (Uzywany jako ref na funkcje)
sub elem_scalar {
    my $val = shift;

    return $val;
}

#* Usun filtr o okreslonym kluczu z tablicy filtrow
sub filter_delete {
    my $filters = shift;
    my $key = shift;

    my $yesno = show_yesno();

    delete $filters->{$key} if ( $yesno eq 'yes' && exists $filters->{$key} );
}

#* Dodaj nowy filtr do tablicy filtrow
sub filter_new {
    my $filters = shift;

    my $fname = show_dialog("Filter name. Start with ! to invert match",
                            "Press [ENTER] to apply");

    return if ! $fname || $fname eq "!";

    # Nie pozwol na duplikat nazwy
    if ( exists $filters->{$fname} ) {
        show_warning("ERROR", "Duplicated filter name");
        return;
    }

    my $fexpr = show_dialog("Filter expression (e.g. regex)",
                            "Press [ENTER] to apply");

    return if ! $fexpr;

    my $regex = eval { qr|$fexpr| };
    if ( ! defined $regex ) {
        show_warning("ERROR", "$@\n");
        return;
    }

    $filters->{$fname} = $regex;
}

#* Dodaj nowy bufor i od razu do niego przejdz
sub buffer_add {
    my $buffers = shift;
    my $cur_buf_no = shift;
    my $new_buf = shift;

    # Jesli nowy wpis do pamieci jest pusty, wyjdz
    return if scalar keys %$new_buf == 0;

    # Skasuj pierwsze bufor, jezeli brakuje miejsca
    push @$buffers, $new_buf;
    shift @$buffers if scalar @$buffers > 12;

    $$cur_buf_no = $#{$buffers};
}

#* Przelacz widok na wybrany bufor
sub buffer_switch_to {
    my $buffers = shift;
    my $cur_buf_no = shift;
    my $new_buf_no = shift;

    return if ($new_buf_no < 0 || $new_buf_no > $#{$buffers} );

    $$cur_buf_no = $new_buf_no;
}

#* Skasuj aktualny bufor z pamieci
sub buffer_delete {
    my $buffers = shift;
    my $cur_buf_no = shift;

    # Nie pozwalaj na skasowanie ostatniego/nieistniejacego buforu
    return if $#{$buffers} == 0 || $$cur_buf_no > $#{$buffers};

    splice @$buffers, $$cur_buf_no, 1;
    $$cur_buf_no = 0;
}

#* Zapisz bufor do pliku
sub buffer_save {
    my $buffer = shift;

    my $fname = show_dialog("Save buffer to file",
                            "Please enter filename and press [ENTER]");

    return if ! $fname;

    open FILE, ">>$fname" or show_warning("ERROR", "$fname: $!\n"), return;

    print FILE elem_logs($_) for ( @{$buffer->{logs}} );
    close FILE;
}

#* Zrzuc filtry do pliku
sub conf_save {
    my $filters = shift;

    if ( ! stat "$conf_dir" ) {
        mkdir "$conf_dir" or die "$conf_dir: $!\n";
    }

    open CF, ">$conf_filters" or die "$conf_filters: $!\n";
    print CF Dumper($filters);
    close CF;
}

#* Odczytaj filtry ze strefy zrzutu
sub conf_load {
    open CF, "$conf_filters" or warn "$conf_filters: $!\n", return;

    my $VAR1;
    my @content = <CF>;

    eval "@content";
    die "Damaged $conf_filters!\n" if ( $@ );

    return %$VAR1;
}

#* Obsluga przyciskow F1...F12 do zmiany buforu
sub keys_function {
    my $key = shift;
    my $buffers = shift;
    my $cur_buf_no = shift;

    for ( my $i = 1; $i <= 12; $i++ ) {
        if ( $key eq KEY_F($i) ) {
            buffer_switch_to($buffers, $cur_buf_no, $i-1);
            return 1;
        }
    }

    return 0;
}

#* Przewijanie tekstu w prawo/lewo
sub keys_leftright {
    my $key = shift;
    my $scroll = shift;

    my $served = 1;

    if ( $key eq KEY_LEFT ) {
        $$scroll -= 20 if $$scroll > 0;
    } elsif ( $key eq KEY_RIGHT  ) {
        $$scroll += 20;
    } else {
        $served = 0;
    }

    return $served;
}

#* Procedura obslugi strzalek na tablicach
sub keys_updown {
    my $key = shift;
    my $tab_size = shift;
    my $tab_pos = shift;
    my $start_point = shift;
    my $max_lines_per_page = shift;

    my $served = 1;

    if ( $key eq KEY_NPAGE ) {  # PageDOWN
        if ( $$start_point + $max_lines_per_page < $tab_size ) {
            $$tab_pos = $$start_point + $max_lines_per_page;
        }
        else {
            $$tab_pos = ($tab_size == 0 ? 0 : $tab_size - 1);
        }
    } elsif ( $key eq KEY_PPAGE ) { # PageUP
        if (  $$start_point - $max_lines_per_page > 0 ) {
            $$tab_pos = $$start_point - $max_lines_per_page;
        } else {
            $$tab_pos = 0;
        }
    } elsif ( $key eq KEY_DOWN ) {
        if ( $$tab_pos < $tab_size - 1 ) {
            $$tab_pos++;
            if ( $$tab_pos - $$start_point > $max_lines_per_page - 1) {
                $$start_point++;
            }
        }
    } elsif ( $key eq KEY_UP ) {
        if ( $$tab_pos > 0 ) {
            $$tab_pos--;
            if ( $$start_point > $$tab_pos ) {
                $$start_point--;
            }
        }
    } elsif ( $key eq KEY_HOME ) {
        $$tab_pos = 0;
    } elsif ( $key eq KEY_END ) {
        $$tab_pos = ($tab_size == 0 ? 0 : $tab_size - 1);
    } else {
        $served = 0;
    }

    return $served;
}

#* Obsluga przycisku zamkniecia okna/programu
sub keys_close {
    my $key = shift;

    return $key eq 'q';
}

