package App::ClusterSSH;

use 5.008.004;
use warnings;
use strict;
use version; our $VERSION = version->new('4.00_06');

use Carp;

use base qw/ App::ClusterSSH::Base /;
use App::ClusterSSH::Host;

use POSIX ":sys_wait_h";
use Pod::Usage;
use Getopt::Long qw(:config no_ignore_case bundling no_auto_abbrev);
use POSIX qw/:sys_wait_h strftime mkfifo/;
use File::Temp qw/:POSIX/;
use Fcntl;
use Tk 800.022;
use Tk::Xlib;
use Tk::ROText;
require Tk::Dialog;
require Tk::LabEntry;
use X11::Protocol;
use X11::Protocol::Constants qw/ Shift Mod5 ShiftMask /;
use vars qw/ %keysymtocode %keycodetosym /;
use X11::Keysyms '%keysymtocode', 'MISCELLANY', 'XKB_KEYS', '3270', 'LATIN1',
    'LATIN2', 'LATIN3', 'LATIN4', 'KATAKANA', 'ARABIC', 'CYRILLIC', 'GREEK',
    'TECHNICAL', 'SPECIAL', 'PUBLISHING', 'APL', 'HEBREW', 'THAI', 'KOREAN';
use File::Basename;
use Net::hostent;
use Carp;
use Sys::Hostname;
use English;
use Socket;

# Notes on general order of processing
#
# parse cmd line options for extra config files
# load system configuration files
# load cfg files from options
# overlay rest of cmd line args onto options
# record all clusters
# parse givwen tags/hostnames and resolve to connections
# open terminals
# optionally open console if required

sub new {
    my ( $class, %args ) = @_;

    my $self = $class->SUPER::new(%args);

    # catch and reap any zombies
    $SIG{CHLD} = \&REAPER;

    return $self;
}

sub REAPER {
    my $kid;
    do {
        $kid = waitpid( -1, WNOHANG );
        logmsg( 2, "REAPER currently returns: $kid" );
    } until ( $kid == -1 || $kid == 0 );
}

# Command line options list
my @options_spec = (
    'debug:+',
    'd',    # backwards compatibility - DEPRECATED
    'D',    # backwards compatibility - DEPRECATED
    'version|v',
    'help|h|?',
    'man|H',
    'action|a=s',
    'cluster-file|c=s',
    'config-file|C=s',
    'evaluate|e=s',
    'tile|g',
    'no-tile|G',
    'username|l=s',
    'options|o=s',
    'port|p=i',
    'autoquit|q',
    'no-autoquit|Q',
    'history|s',
    'term-args|t=s',
    'title|T=s',
    'output-config|u',
    'font|f=s',
    'list|L',
    'use_all_a_records|A',
);
my %options;
my %config;
my %clusters;    # hash for resolving cluster names
my %windows;     # hash for all window definitions
my %menus;       # hash for all menu definitions
my @servers;     # array of servers provided on cmdline
my %servers;     # hash of server cx info
my $helper_script = "";
my $xdisplay;
my %keyboardmap;
my $sysconfigdir = "/etc";
my %ssh_hostnames;

$keysymtocode{unknown_sym} = 0xFFFFFF;    # put in a default "unknown" entry
$keysymtocode{EuroSign}
    = 0x20AC;    # Euro sign - missing from X11::Protocol::Keysyms

# and also map it the other way
%keycodetosym = reverse %keysymtocode;

# Set up UTF-8 on STDOUT
binmode STDOUT, ":utf8";

#use bytes;

### all sub-routines ###

# Pick a color based on a string.
sub pick_color {
    my ($string)   = @_;
    my @components = qw(AA BB CC EE);
    my $color      = 0;
    for ( my $i = 0; $i < length($string); $i++ ) {
        $color += ord( substr( $string, $i, 1 ) );
    }

    srand($color);
    my $ans = '\\#';
    $ans .= $components[ int( 4 * rand() ) ];
    $ans .= $components[ int( 4 * rand() ) ];
    $ans .= $components[ int( 4 * rand() ) ];
    return $ans;
}

# close a specific host session
sub terminate_host($) {
    my $svr = shift;
    logmsg( 2, "Killing session for $svr" );
    if ( !$servers{$svr} ) {
        logmsg( 2, "Session for $svr not found" );
        return;
    }

    logmsg( 2, "Killing process $servers{$svr}{pid}" );
    kill( 9, $servers{$svr}{pid} ) if kill( 0, $servers{$svr}{pid} );
    delete( $servers{$svr} );
}

# catch_all exit routine that should always be used
sub exit_prog() {
    logmsg( 3, "Exiting via normal routine" );

    # for each of the client windows, send a kill

    # to make sure we catch all children, even when they havnt
    # finished starting or received teh kill signal, do it like this
    while (%servers) {
        foreach my $svr ( keys(%servers) ) {
            terminate_host($svr);
        }
    }
    exit 0;
}

# output function according to debug level
# $1 = log level (0 to 3)
# $2 .. $n = list to pass to print
sub logmsg($@) {
    my $level = shift;

    if ( $level > 6 ) {
        croak('requested debug level should not be above 6');
    }

    if ( $level <= $options{debug} ) {
        print( strftime( "%H:%M:%S: ", localtime ) )
            if ( $options{debug} > 1 );
        print @_, $/;
    }
}

# set some application defaults
sub load_config_defaults() {
    $config{terminal}           = "xterm";
    $config{terminal_args}      = "";
    $config{terminal_title_opt} = "-T";
    $config{terminal_colorize}  = 1;
    $config{terminal_bg_style}  = 'dark';
    $config{terminal_allow_send_events}
        = "-xrm '*.VT100.allowSendEvents:true'";
    $config{terminal_font}           = "6x13";
    $config{terminal_size}           = "80x24";
    $config{use_hotkeys}             = "yes";
    $config{key_quit}                = "Control-q";
    $config{key_addhost}             = "Control-Shift-plus";
    $config{key_clientname}          = "Alt-n";
    $config{key_history}             = "Alt-h";
    $config{key_retilehosts}         = "Alt-r";
    $config{key_paste}               = "Control-v";
    $config{mouse_paste}             = "Button-2";
    $config{auto_quit}               = "yes";
    $config{window_tiling}           = "yes";
    $config{window_tiling_direction} = "right";
    $config{console_position}        = "";

    $config{screen_reserve_top}    = 0;
    $config{screen_reserve_bottom} = 60;
    $config{screen_reserve_left}   = 0;
    $config{screen_reserve_right}  = 0;

    $config{terminal_reserve_top}    = 5;
    $config{terminal_reserve_bottom} = 0;
    $config{terminal_reserve_left}   = 5;
    $config{terminal_reserve_right}  = 0;

    $config{terminal_decoration_height} = 10;
    $config{terminal_decoration_width}  = 8;

    ( $config{comms} = basename($0) ) =~ s/^.//;
    $config{comms} =~ s/.pl$//;    # for when testing directly out of cvs
    $config{method} = $config{comms};

    $config{title} = "C" . uc( $config{comms} );

    $config{comms} = "telnet" if ( $config{comms} eq "tel" );

    $config{ $config{comms} } = $config{comms};

    $config{ssh_args} = " -x -o ConnectTimeout=10"
        if ( $config{ $config{comms} } =~ /ssh$/ );
    $config{rsh_args} = "";

    $config{telnet_args} = "";

    $config{extra_cluster_file} = "";

    $config{unmap_on_redraw} = "no";    # Debian #329440

    $config{show_history}   = 0;
    $config{history_width}  = 40;
    $config{history_height} = 10;

    $config{command}             = q{};
    $config{max_host_menu_items} = 30;

    $config{max_addhost_menu_cluster_items} = 6;
    $config{menu_send_autotearoff}          = 0;
    $config{menu_host_autotearoff}          = 0;

    $config{send_menu_xml_file} = $ENV{HOME} . '/.csshrc_send_menu';

    $config{use_all_a_records} = 0;
}

# load in config file settings
sub parse_config_file($) {
    my $config_file = shift;
    logmsg( 2, "Reading in from config file $config_file" );
    return if ( !-e $config_file || !-r $config_file );

    open( CFG, $config_file ) or die("Couldnt open $config_file: $!");
    my $l;
    while ( defined( $l = <CFG> ) ) {
        next
            if ( $l =~ /^\s*$/ || $l =~ /^#/ )
            ;    # ignore blank lines & commented lines
        $l =~ s/#.*//;     # remove comments from remaining lines
        $l =~ s/\s*$//;    # remove trailing whitespace
        chomp $l;
        if ( $l =~ s/\\\s*$// ) {
            $l .= <CFG>;
            redo unless eof(CFG);
        }

        next unless $l =~ m/\s*(\S+)\s*=\s*(.*)\s*/;
        my ( $key, $value ) = ( $1, $2 );
        $config{$key} = $value;
        logmsg( 3, "$key=$value" );
    }
    close(CFG);

    # tidy up entries, just in case
    $config{terminal_font} =~ s/['"]//g;
}

sub find_binary($) {
    my $binary = shift;

    logmsg( 2, "Looking for $binary" );
    my $path;
    if ( !-x $binary || substr( $binary, 0, 1 ) ne '/' ) {

       # search the users $PATH and then a few other places to find the binary
       # just in case $PATH isnt set up right
        foreach (
            split( /:/, $ENV{PATH} ), qw!
            /bin
            /sbin
            /usr/sbin
            /usr/bin
            /usr/local/bin
            /usr/local/sbin
            /opt/local/bin
            /opt/local/sbin
            !
            )
        {
            logmsg( 3, "Looking in $_" );

            if ( -f $_ . '/' . $binary && -x $_ . '/' . $binary ) {
                $path = $_ . '/' . $binary;
                logmsg( 2, "Found at $path" );
                last;
            }
        }
    }
    else {
        logmsg( 2, "Already configured OK" );
        $path = $binary;
    }
    if ( !$path || !-f $path || !-x $path ) {
        warn(
            "Terminal binary not found ($binary) - please amend \$PATH or the cssh config file\n"
        );
        die unless ( $options{'output-config'} );
    }

    chomp($path);
    return $path;
}

# make sure our config is sane (i.e. binaries found) and get some extra bits
sub check_config() {

    # check we have xterm on our path
    logmsg( 2, "Checking path to xterm" );
    $config{terminal} = find_binary( $config{terminal} );

    # check we have comms method on our path
    logmsg( 2, "Checking path to $config{comms}" );
    $config{ $config{comms} } = find_binary( $config{ $config{comms} } );

    # make sure comms in an accepted value
    die
        "FATAL: Only ssh, rsh and telnet protocols are currently supported (comms=$config{comms})\n"
        if ( $config{comms} !~ /^(:?[rs]sh|telnet)$/ );

    # Set any extra config options given on command line
    $config{title} = $options{title} if ( $options{title} );

    $config{auto_quit} = "yes" if $options{autoquit};
    $config{auto_quit} = "no"  if $options{'no-autoquit'};

    # backwards compatibility & tidyup
    if ( $config{always_tile} ) {
        if ( !$config{window_tiling} ) {
            if ( $config{always_tile} eq "never" ) {
                $config{window_tiling} = "no";
            }
            else {
                $config{window_tiling} = "yes";
            }
        }
        delete( $config{always_tile} );
    }
    $config{window_tiling} = "yes" if $options{tile};
    $config{window_tiling} = "no"  if $options{'no-tile'};

    $config{user} = $options{username} if ( $options{username} );
    $config{terminal_args} = $options{'term-args'}
        if ( $options{'term-args'} );

    if ( $config{terminal_args} =~ /-class (\w+)/ ) {
        $config{terminal_allow_send_events}
            = "-xrm '$1.VT100.allowSendEvents:true'";
    }

    $config{internal_previous_state} = "";    # set to default

    # option font overrides config file font setting
    $config{terminal_font} = $options{font} if ( $options{font} );
    get_font_size();

    $config{extra_cluster_file} =~ s/\s+//g;

    $config{ssh_args} = $options{options} if ( $options{options} );

    $config{show_history} = 1 if $options{'show-history'};

    $config{command} = $options{action} if ( $options{action} );

    if ( $options{use_all_a_records} ) {
        $config{use_all_a_records} = !$config{use_all_a_records} || 0;
    }
}

sub load_configfile() {
    parse_config_file( $sysconfigdir . '/csshrc' );
    parse_config_file( $ENV{HOME} . '/.csshrc' );
    if ( $options{'config-file'} ) {
        parse_config_file( $options{'config-file'} );
    }
    check_config();
}

# dump out the config to STDOUT
sub dump_config {
    my $noexit = shift;

    logmsg( 3, "Dumping config to STDOUT" );

    print("# Configuration dump produced by 'cssh -u'\n");

    foreach ( sort( keys(%config) ) ) {
        next
            if ( $_ =~ /^internal/ && $options{debug} == 0 )
            ;    # do not output internal vars
        print "$_=$config{$_}\n";
    }
    exit_prog if ( !$noexit );
}

sub list_tags {
    print( 'Available cluster tags:', $/ );
    print "\t", $_, $/ foreach ( sort( keys(%clusters) ) );
    exit_prog;
}

sub check_ssh_hostnames {
    return unless ( $config{method} eq "ssh" );

    my $ssh_config = "$ENV{HOME}/.ssh/config";

    if ( -r $ssh_config && open( SSHCFG, "<", $ssh_config ) ) {
        while (<SSHCFG>) {
            next unless (m/^\s*host\s+([\w\.-]+)/i);

            # account for multiple declarations of hosts
            $ssh_hostnames{$_} = 1 foreach ( split( /\s+/, $1 ) );
        }
        close(SSHCFG);
    }

    if ( $options{debug} > 1 ) {
        if (%ssh_hostnames) {
            logmsg( 2, "Parsed these ssh config hosts:" );
            logmsg( 2, "- $_" ) foreach ( sort( keys(%ssh_hostnames) ) );
        }
        else {
            logmsg( 2, "No hostnames parsed from user ssh config file" );
        }
    }
}

sub evaluate_commands {
    my ( $return, $user, $port, $host );

    # break apart the given host string to check for user or port configs
    print "{evaluate}=$options{evaluate}\n";
    $user = $1 if ( $options{evaluate} =~ s/^(.*)@// );
    $port = $1 if ( $options{evaluate} =~ s/:(\w+)$// );
    $host = $options{evaluate};

    $user = $user ? "-l $user" : "";
    if ( $config{comms} eq "telnet" ) {
        $port = $port ? " $port" : "";
    }
    else {
        $port = $port ? "-p $port" : "";
    }

    print STDERR "Testing terminal - running command:\n";

    my $terminal_command
        = "$config{terminal} $config{terminal_allow_send_events} -e \"$^X\" \"-e\" 'print \"Working\\n\" ; sleep 5'";

    print STDERR $terminal_command, $/;

    system($terminal_command);
    print STDERR "\nTesting comms - running command:\n";

    my $comms_command = $config{ $config{comms} } . " "
        . $config{ $config{comms} . "_args" };

    if ( $config{comms} eq "telnet" ) {
        $comms_command .= " $host $port";
    }
    else {
        $comms_command .= " $user $port $host echo Working";
    }

    print STDERR $comms_command, $/;

    system($comms_command);

    exit_prog;
}

sub load_keyboard_map() {

    # load up the keyboard map to convert keysyms to keyboardmap
    my $min      = $xdisplay->{min_keycode};
    my $count    = $xdisplay->{max_keycode} - $min;
    my @keyboard = $xdisplay->GetKeyboardMapping( $min, $count );

    # @keyboard arry
    #  0 = plain key
    #  1 = with shift
    #  2 = with Alt-GR
    #  3 = with shift + AltGr
    #  4 = same as 2 - control/alt?
    #  5 = same as 3 - shift-control-alt?

    logmsg( 1, "Loading keymaps and keycodes" );

    foreach ( 0 .. $#keyboard ) {
        if ( defined $keyboard[$_][3] ) {
            if ( defined( $keycodetosym{ $keyboard[$_][3] } ) ) {
                $keyboardmap{ $keycodetosym{ $keyboard[$_][3] } }
                    = 'sa' . ( $_ + $min );
            }
            else {
                logmsg( 2, "Unknown keycode ", $keyboard[$_][3] )
                    if ( $keyboard[$_][3] != 0 );
            }
        }
        if ( defined $keyboard[$_][2] ) {
            if ( defined( $keycodetosym{ $keyboard[$_][2] } ) ) {
                $keyboardmap{ $keycodetosym{ $keyboard[$_][2] } }
                    = 'a' . ( $_ + $min );
            }
            else {
                logmsg( 2, "Unknown keycode ", $keyboard[$_][2] )
                    if ( $keyboard[$_][2] != 0 );
            }
        }
        if ( defined $keyboard[$_][1] ) {
            if ( defined( $keycodetosym{ $keyboard[$_][1] } ) ) {
                $keyboardmap{ $keycodetosym{ $keyboard[$_][1] } }
                    = 's' . ( $_ + $min );
            }
            else {
                logmsg( 2, "Unknown keycode ", $keyboard[$_][1] )
                    if ( $keyboard[$_][1] != 0 );
            }
        }
        if ( defined $keyboard[$_][0] ) {
            if ( defined( $keycodetosym{ $keyboard[$_][0] } ) ) {
                $keyboardmap{ $keycodetosym{ $keyboard[$_][0] } }
                    = 'n' . ( $_ + $min );
            }
            else {
                logmsg( 2, "Unknown keycode ", $keyboard[$_][0] )
                    if ( $keyboard[$_][0] != 0 );
            }
        }

        # dont know these two key combs yet...
        #$keyboardmap{ $keycodetosym { $keyboard[$_][4] } } = $_ + $min;
        #$keyboardmap{ $keycodetosym { $keyboard[$_][5] } } = $_ + $min;
    }

    #print "$_ => $keyboardmap{$_}\n" foreach(sort(keys(%keyboardmap)));
    #print "keysymtocode: $keysymtocode{o}\n";
    #die;
}

sub get_keycode_state($) {
    my $keysym = shift;
    $keyboardmap{$keysym} =~ m/^(\D+)(\d+)$/;
    my ( $state, $code ) = ( $1, $2 );

    logmsg( 2, "keyboardmap=:", $keyboardmap{$keysym}, ":" );
    logmsg( 2, "state=$state, code=$code" );

SWITCH: for ($state) {
        /^n$/ && do {
            $state = 0;
            last SWITCH;
        };
        /^s$/ && do {
            $state = Shift();
            last SWITCH;
        };
        /^a$/ && do {
            $state = Mod5();
            last SWITCH;
        };
        /^sa$/ && do {
            $state = Shift() + Mod5();
            last SWITCH;
        };

        die("Should never reach here");
    }

    logmsg( 2, "returning state=:$state: code=:$code:" );

    return ( $state, $code );
}

# read in all cluster definitions
sub get_clusters() {

    # first, read in global file
    my $cluster_file = '/etc/clusters';

    logmsg( 3, "Logging for $cluster_file" );

    if ( -f $cluster_file ) {
        logmsg( 2, "Loading clusters in from $cluster_file" );
        open( CLUSTERS, $cluster_file ) || die("Couldnt read $cluster_file");
        my $l;
        while ( defined( $l = <CLUSTERS> ) ) {
            next
                if ( $l =~ /^\s*$/ || $l =~ /^#/ )
                ;    # ignore blank lines & commented lines
            chomp $l;
            if ( $l =~ s/\\\s*$// ) {
                $l .= <CLUSTER>;
                redo unless eof(CLUSTERS);
            }
            my @line = split( /\s/, $l );

        #s/^([\w-]+)\s*//;               # remote first word and stick into $1

            logmsg(
                3,
                "cluster $line[0] = ",
                join( " ", @line[ 1 .. $#line ] )
            );
            $clusters{ $line[0] } = join( " ", @line[ 1 .. $#line ] )
                ;    # Now bung in rest of line
        }
        close(CLUSTERS);
    }

    # Now get any definitions out of %config
    logmsg( 2, "Looking for csshrc" );
    if ( $config{clusters} ) {
        logmsg( 2, "Loading clusters in from csshrc" );

        foreach ( split( /\s+/, $config{clusters} ) ) {
            if ( !$config{$_} ) {
                warn(
                    "WARNING: missing cluster definition in .csshrc file ($_)"
                );
            }
            else {
                logmsg( 3, "cluster $_ = $config{$_}" );
                $clusters{$_} = $config{$_};
            }
        }
    }

    # and any clusters defined within the config file or on the command line
    if ( $config{extra_cluster_file} || $options{'cluster-file'} ) {

        # check for multiple entries and push it through glob to catch ~'s
        foreach my $item ( split( /,/, $config{extra_cluster_file} ),
            $options{'cluster-file'} )
        {
            next unless ($item);

            # cater for people using '$HOME'
            $item =~ s/\$HOME/$ENV{HOME}/;
            foreach my $file ( glob($item) ) {
                if ( !-r $file ) {
                    warn("Unable to read cluster file '$file': $!\n");
                    next;
                }
                logmsg( 2, "Loading clusters in from '$file'" );

                open( CLUSTERS, $file ) || die("Couldnt read '$file': $!\n");
                my $l;
                while ( defined( $l = <CLUSTERS> ) ) {
                    next if ( $l =~ /^\s*$/ || $l =~ /^#/ );
                    chomp $l;
                    if ( $l =~ s/\\\s*$// ) {
                        $l .= <CLUSTER>;
                        redo unless eof(CLUSTERS);
                    }

                    my @line = split( /\s/, $l );
                    logmsg(
                        3,
                        "cluster $line[0] = ",
                        join( " ", @line[ 1 .. $#line ] )
                    );
                    $clusters{ $line[0] } = join( " ", @line[ 1 .. $#line ] )
                        ;    # Now bung in rest of line
                }
            }

        }
    }

    logmsg( 2, "Finished loading clusters" );
}

sub resolve_names(@) {
    logmsg( 2, 'Resolving cluster names: started' );
    my @servers = @_;

    foreach (@servers) {
        my $dirty    = $_;
        my $username = q{};
        logmsg( 3, 'Checking tag ', $_ );

        if ( $dirty =~ s/^(.*)@// ) {
            $username = $1;
        }
        if (   $config{use_all_a_records}
            && $dirty !~ m/^(\d{1,3}\.?){4}$/
            && !defined( $clusters{$dirty} ) )
        {
            my $hostobj = gethostbyname($dirty);
            if ( defined($hostobj) ) {
                my @alladdrs = map { inet_ntoa($_) } @{ $hostobj->addr_list };
                if ( $#alladdrs > 0 ) {
                    $clusters{$dirty} = join ' ', @alladdrs;
                    logmsg( 3, 'Expanded to ', $clusters{$dirty} );
                }
                else {
                    logmsg( 3, 'Only one A record' );
                }
            }
        }
        if ( $clusters{$dirty} ) {
            logmsg( 3, '... it is a cluster' );
            foreach my $node ( split( / /, $clusters{$dirty} ) ) {
                if ($username) {
                    $node =~ s/^(.*)@//;
                    $node = $username . '@' . $node;
                }
                push( @servers, $node );
            }
            $_ = q{};
        }
    }

    # now clean the array up
    @servers = grep { $_ !~ m/^$/ } @servers;

    logmsg( 3, 'leaving with ', $_ ) foreach (@servers);
    logmsg( 2, 'Resolving cluster names: completed' );
    return (@servers);
}

sub change_main_window_title() {
    my $number = keys(%servers);
    $windows{main_window}->title( $config{title} . " [$number]" );
}

sub show_history() {
    if ( $config{show_history} ) {
        $windows{history}->packForget();
        $config{show_history} = 0;
    }
    else {
        $windows{history}->pack(
            -fill   => "x",
            -expand => 1,
        );
        $config{show_history} = 1;
    }
}

sub update_display_text($) {
    my $char = shift;

    return if ( !$config{show_history} );

    logmsg( 2, "Dropping :$char: into display" );

SWITCH: {
        foreach ($char) {
            /^Return$/ && do {
                $windows{history}->insert( 'end', "\n" );
                last SWITCH;
            };

            /^BackSpace$/ && do {
                $windows{history}->delete('end - 2 chars');
                last SWITCH;
            };

            /^(:?Shift|Control|Alt)_(:?R|L)$/ && do {
                last SWITCH;
            };

            length($char) > 1 && do {
                $windows{history}
                    ->insert( 'end', chr( $keysymtocode{$char} ) )
                    if ( $keysymtocode{$char} );
                last SWITCH;
            };

            do {
                $windows{history}->insert( 'end', $char );
                last SWITCH;
            };
        }
    }
}

sub send_text($@) {
    my $svr = shift;
    my $text = join( "", @_ );

    logmsg( 2, "servers{$svr}{wid}=$servers{$svr}{wid}" );
    logmsg( 3, "Sending to '$svr' text:$text:" );

    # command macro substitution

    # $svr contains a trailing space here, so ensure its stripped off
    {
        my $servername = $svr;
        $servername =~ s/\s+//;
        $text       =~ s/%s/$servername/xsm;
    }
    $text =~ s/%h/hostname()/xsme;

    # use connection username, else default to current username
    {
        my $username = $servers{$svr}{username};
        $username ||= getpwuid($UID);
        $text =~ s/%u/$username/xsm;
    }
    $text =~ s/%n/\n/xsm;

    foreach my $char ( split( //, $text ) ) {
        next if ( !defined($char) );
        my $ord = ord($char);
        $ord = 65293 if ( $ord == 10 );    # convert 'Return' to sym

        if ( !defined( $keycodetosym{$ord} ) ) {
            warn("Unknown character in xmodmap keytable: $char ($ord)\n");
            next;
        }
        my $keysym  = $keycodetosym{$ord};
        my $keycode = $keysymtocode{$keysym};

        logmsg( 2, "Looking for char :$char: with ord :$ord:" );
        logmsg( 2, "Looking for keycode :$keycode:" );
        logmsg( 2, "Looking for keysym  :$keysym:" );
        logmsg( 2, "Looking for keyboardmap :", $keyboardmap{$keysym}, ":" );
        my ( $state, $code ) = get_keycode_state($keysym);
        logmsg( 2, "Got state :$state: code :$code:" );

        for my $event (qw/KeyPress KeyRelease/) {
            logmsg( 2, "sending event=$event code=:$code: state=:$state:" );
            $xdisplay->SendEvent(
                $servers{$svr}{wid},
                0,
                $xdisplay->pack_event_mask($event),
                $xdisplay->pack_event(
                    'name'        => $event,
                    'detail'      => $code,
                    'state'       => $state,
                    'time'        => time(),
                    'event'       => $servers{$svr}{wid},
                    'root'        => $xdisplay->root(),
                    'same_screen' => 1,
                ),
            );
        }
    }
    $xdisplay->flush();
}

sub send_text_to_all_servers {
    my $text = join( '', @_ );

    foreach my $svr ( keys(%servers) ) {
        send_text( $svr, $text )
            if ( $servers{$svr}{active} == 1 );
    }
}

sub send_resizemove($$$$$) {
    my ( $win, $x_pos, $y_pos, $x_siz, $y_siz ) = @_;

    logmsg( 3,
        "Moving window $win to x:$x_pos y:$y_pos (size x:$x_siz y:$y_siz)" );

    #logmsg( 2, "resize move normal: ", $xdisplay->atom('WM_NORMAL_HINTS') );
    #logmsg( 2, "resize move size:   ", $xdisplay->atom('WM_SIZE_HINTS') );

    # set the window to have "user" set size & position, rather than "program"
    $xdisplay->req(
        'ChangeProperty',
        $win,
        $xdisplay->atom('WM_NORMAL_HINTS'),
        $xdisplay->atom('WM_SIZE_HINTS'),
        32,
        'Replace',

        # create data struct on fly to set bitwise flags
        pack( 'LLLLL' . 'x[L]' x 12, 1 | 2, $x_pos, $y_pos, $x_siz, $y_siz ),
    );

    $xdisplay->req(
        'ConfigureWindow',
        $win,
        'x'      => $x_pos,
        'y'      => $y_pos,
        'width'  => $x_siz,
        'height' => $y_siz,
    );

    #$xdisplay->flush(); # dont flush here, but after all tiling worked out
}

sub setup_helper_script() {
    logmsg( 2, "Setting up helper script" );
    $helper_script = <<"	HERE";
		my \$pipe=shift;
		my \$svr=shift;
		my \$user=shift;
		my \$port=shift;
		my \$command="$config{$config{comms}} $config{$config{comms}."_args"} ";
		open(PIPE, ">", \$pipe) or die("Failed to open pipe: \$!\\n");
		print PIPE "\$\$:\$ENV{WINDOWID}" 
			or die("Failed to write to pipe: $!\\n");
		close(PIPE) or die("Failed to close pipe: $!\\n");
		if(\$svr =~ m/==\$/)
		{
			\$svr =~ s/==\$//;
			warn("\\nWARNING: failed to resolve IP address for \$svr.\\n\\n"
			);
			sleep 5;
		}
		if(\$user) {
			unless("$config{comms}" eq "telnet") {
				\$user = \$user ? "-l \$user " : "";
				\$command .= \$user;
			}
		}
		if("$config{comms}" eq "telnet") {
			\$command .= "\$svr \$port";
		} else {
			if (\$port) {
				\$command .= "-p \$port \$svr";
			} else {
			  \$command .= "\$svr";
			}
		}
		\$command .= " $config{command} || sleep 5";
#		warn("Running:\$command\\n"); # for debug purposes
		exec(\$command);
	HERE

    #	eval $helper_script || die ($@); # for debug purposes
    logmsg( 2, $helper_script );
    logmsg( 2, "Helper script done" );
}

sub split_hostname {
    my ($connect_string) = @_;

    my ( $server, $username, $port );

    logmsg( 3, 'split_hostname: connect_string=' . $connect_string );

    $username = $config{user} if ( $config{user} );

    if ( $connect_string =~ s/^(.*)@// ) {
        $username = $1;
    }

    # cope with IPv6 addresses

    # check for correct syntax of using [<IPv6 address>]
    # See http://tools.ietf.org/html/rfc2732 for more details
    if ( $connect_string =~ m/^\[([\w:%]+)\](?::(\d+))?$/xsm ) {
        logmsg( 3, 'connect_string contains IPv6 address' );
        $server = $1;
        $port   = $2;
    }
    else {

        my $colon_count = $connect_string =~ tr/://;

        # See if there are exactly 7 colons - if so, assume pure IPv6
        if ( $colon_count == 7 ) {
            $server = $connect_string;
        }
        else {

            # if more than 1 but less than 8 colons and last octect is
            # numbers only, warn about ambiguity
            if (   $colon_count > 1
                && $colon_count < 8
                && $connect_string =~ m/:(\d+)$/ )
            {
                our $seen_error;
                warn 'Potentially ambiguous IPv6 address/port definition: ',
                    $connect_string, $/;
                warn 'Assuming it is an IPv6 address only.', $/;
                $server = $connect_string;
                if ( !$seen_error ) {
                    warn '*** See documenation for more information.', $/;
                    $seen_error = 1;
                }
            }
            else {

               # split out port from end of connect string
               # could have an invalid IPv6 address here, but the connect
               # method will warn if it cannot connect anyhow
               # However, this also catchs IPv4 addresses, possibly with ports
                ( $server, $port )
                    = $connect_string =~ m/^([\w%.-]+)(?::(\d+))?$/xsm;
            }
        }
    }

    $port ||= defined $options{port} ? $options{port} : q{};
    $username ||= q{};

    logmsg( 3, "username=$username, server=$server, port=$port" );

    return ( $username, $server, $port );
}

sub open_client_windows(@) {
    foreach (@_) {
        next unless ($_);

        my $server_object = App::ClusterSSH::Host->parse_host_string($_);

        my $username = $server_object->get_username();
        my $port     = $server_object->get_port();
        my $server   = $server_object->get_hostname();

        #my ( $username, $server, $port ) = split_hostname($_);
        my $given_server_name = $server_object->get_givenname();

        # see if we can find the hostname - if not, drop it
        my $realname = $server_object->get_realname();
        if ( !$realname ) {
            my $text = "WARNING: '$_' unknown";

            if (%ssh_hostnames) {
                $text
                    .= " (unable to resolve and not in user ssh config file)";
            }

            warn( $text, $/ );

       #next;  # Debian bug 499935 - ignore warnings about hostname resolution
        }

        my $color = '';
        if ( $config{terminal_colorize} ) {
            my $c = pick_color($server);
            if ( $config{terminal_bg_style} eq 'dark' ) {
                $color = "-bg \\#000000 -fg $c";
            }
            else {
                $color = "-fg \\#000000 -bg $c";
            }
        }

        my $count = q{};
        while ( defined( $servers{ $server . q{ } . $count } ) ) {
            $count++;
        }
        $server .= q{ } . $count;

        $servers{$server}{connect_string} = $_;
        $servers{$server}{givenname}      = $given_server_name;
        $servers{$server}{realname}       = $realname;
        $servers{$server}{username}       = $username;
        $servers{$server}{port}           = $port || '';

        logmsg( 2, "Working on server $server for $_" );

        $servers{$server}{pipenm} = tmpnam();

        logmsg( 2, "Set temp name to: $servers{$server}{pipenm}" );
        mkfifo( $servers{$server}{pipenm}, 0600 )
            or die("Cannot create pipe: $!");

       # NOTE: the pid is re-fetched from the xterm window (via helper_script)
       # later as it changes and we need an accurate PID as it is widely used
        $servers{$server}{pid} = fork();
        if ( !defined( $servers{$server}{pid} ) ) {
            die("Could not fork: $!");
        }

        if ( $servers{$server}{pid} == 0 ) {

          # this is the child
          # Since this is the child, we can mark any server unresolved without
          # affecting the main program
            $servers{$server}{realname} .= "==" if ( !$realname );
            my $exec
                = "$config{terminal} $color $config{terminal_args} $config{terminal_allow_send_events} $config{terminal_title_opt} '$config{title}: $servers{$server}{connect_string}' -font $config{terminal_font} -e \"$^X\" \"-e\" '$helper_script' '$servers{$server}{pipenm}' '$servers{$server}{givenname}' '$servers{$server}{username}' '$servers{$server}{port}'";
            logmsg( 2, "Terminal exec line:\n$exec\n" );
            exec($exec) == 0 or warn("Failed: $!");
        }
    }

    # Now all the windows are open, get all their window id's
    foreach my $server ( keys(%servers) ) {
        next if ( defined( $servers{$server}{active} ) );

        # sleep for a moment to give system time to come up
        select( undef, undef, undef, 0.1 );

        # block on open so we get the text when it comes in
        unless (
            sysopen(
                $servers{$server}{pipehl}, $servers{$server}{pipenm},
                O_RDONLY
            )
            )
        {
            warn(
                "Cannot open pipe for reading when talking to $server: $!\n");
        }
        else {

            # NOTE: read both the xterm pid and the window ID here
            # get PID here as it changes from the fork above, and we need the
            # correct PID
            logmsg( 2, "Performing sysread" );
            my $piperead;
            sysread( $servers{$server}{pipehl}, $piperead, 100 );
            ( $servers{$server}{pid}, $servers{$server}{wid} )
                = split( /:/, $piperead, 2 );
            warn("Cannot determ pid of '$server' window\n")
                unless $servers{$server}{pid};
            warn("Cannot determ window ID of '$server' window\n")
                unless $servers{$server}{wid};
            logmsg( 2, "Done and closing pipe" );

            close( $servers{$server}{pipehl} );
        }
        delete( $servers{$server}{pipehl} );

        unlink( $servers{$server}{pipenm} );
        delete( $servers{$server}{pipenm} );

        $servers{$server}{active} = 1;    # mark as active
        $config{internal_activate_autoquit}
            = 1;                          # activate auto_quit if in use
    }
    logmsg( 2, "All client windows opened" );
    $config{internal_total} = int( keys(%servers) );
}

sub get_font_size() {
    logmsg( 2, "Fetching font size" );

    # get atom name<->number relations
    my $quad_width = $xdisplay->atom("QUAD_WIDTH");
    my $pixel_size = $xdisplay->atom("PIXEL_SIZE");

    my $font = $xdisplay->new_rsrc;
    $xdisplay->OpenFont( $font, $config{terminal_font} );

    my %font_info;

    eval { (%font_info) = $xdisplay->QueryFont($font); }
        || die( "Fatal: Unrecognised font used ($config{terminal_font}).\n"
            . "Please amend \$HOME/.csshrc with a valid font (see man page).\n"
        );

    $config{internal_font_width}  = $font_info{properties}{$quad_width};
    $config{internal_font_height} = $font_info{properties}{$pixel_size};

    if ( !$config{internal_font_width} || !$config{internal_font_height} ) {
        die(      "Fatal: Unrecognised font used ($config{terminal_font}).\n"
                . "Please amend \$HOME/.csshrc with a valid font (see man page).\n"
        );
    }

    logmsg( 2, "Done with font size" );
}

sub show_console() {
    logmsg( 2, "Sending console to front" );

    $config{internal_previous_state} = "mid-change";

    # fudge the counter to drop a redraw event;
    $config{internal_map_count} -= 4;

    $xdisplay->flush();
    $windows{main_window}->update();

    select( undef, undef, undef, 0.2 );    #sleep for a mo
    $windows{main_window}->withdraw;

    # Sleep for a moment to give WM time to bring console back
    select( undef, undef, undef, 0.5 );

    if ( $config{menu_send_autotearoff} ) {
        $menus{send}->menu->tearOffMenu()->raise;
    }

    if ( $config{menu_host_autotearoff} ) {
        $menus{hosts}->menu->tearOffMenu()->raise;
    }

    $windows{main_window}->deiconify;
    $windows{main_window}->raise;
    $windows{main_window}->focus( -force );
    $windows{text_entry}->focus( -force );

    $config{internal_previous_state} = "normal";

    # fvwm seems to need this (Debian #329440)
    $windows{main_window}->MapWindow;
}

# leave function def open here so we can be flexible in how it called
sub retile_hosts {
    my $force = shift || "";
    logmsg( 2, "Retiling windows" );

    if ( $config{window_tiling} ne "yes" && !$force ) {
        logmsg( 3,
            "Not meant to be tiling; just reshow windows as they were" );

        foreach my $server ( reverse( keys(%servers) ) ) {
            $xdisplay->req( 'MapWindow', $servers{$server}{wid} );
        }
        $xdisplay->flush();
        show_console();
        return;
    }

    # ALL SIZES SHOULD BE IN PIXELS for consistency

    logmsg( 2, "Count is currently $config{internal_total}" );

    if ( $config{internal_total} == 0 ) {

        # If nothing to tile, done bother doing anything, just show console
        show_console();
        return;
    }

    # work out terminal pixel size from terminal size & font size
    # does not include any title bars or scroll bars - purely text area
    $config{internal_terminal_cols}
        = ( $config{terminal_size} =~ /(\d+)x.*/ )[0];
    $config{internal_terminal_width}
        = ( $config{internal_terminal_cols} * $config{internal_font_width} )
        + $config{terminal_decoration_width};

    $config{internal_terminal_rows}
        = ( $config{terminal_size} =~ /.*x(\d+)/ )[0];
    $config{internal_terminal_height}
        = ( $config{internal_terminal_rows} * $config{internal_font_height} )
        + $config{terminal_decoration_height};

    # fetch screen size
    $config{internal_screen_height} = $xdisplay->{height_in_pixels};
    $config{internal_screen_width}  = $xdisplay->{width_in_pixels};

    # Now, work out how many columns of terminals we can fit on screen
    $config{internal_columns} = int(
        (         $config{internal_screen_width} 
                - $config{screen_reserve_left}
                - $config{screen_reserve_right}
        ) / (
            $config{internal_terminal_width} 
                + $config{terminal_reserve_left}
                + $config{terminal_reserve_right}
        )
    );

    # Work out the number of rows we need to use to fit everything on screen
    $config{internal_rows} = int(
        ( $config{internal_total} / $config{internal_columns} ) + 0.999 );

    logmsg( 2, "Screen Columns: ", $config{internal_columns} );
    logmsg( 2, "Screen Rows: ",    $config{internal_rows} );

    # Now adjust the height of the terminal to either the max given,
    # or to get everything on screen
    {
        my $height = int(
            (   (         $config{internal_screen_height}
                        - $config{screen_reserve_top}
                        - $config{screen_reserve_bottom}
                ) - (
                    $config{internal_rows} * (
                              $config{terminal_reserve_top}
                            + $config{terminal_reserve_bottom}
                    )
                )
            ) / $config{internal_rows}
        );

        logmsg( 2, "Terminal height=$height" );

        $config{internal_terminal_height} = (
              $height > $config{internal_terminal_height}
            ? $config{internal_terminal_height}
            : $height
        );
    }

    dump_config("noexit") if ( $options{debug} > 1 );

    # now we have the info, plot first window position
    my @hosts;
    my ( $current_x, $current_y, $current_row, $current_col ) = 0;
    if ( $config{window_tiling_direction} =~ /right/i ) {
        logmsg( 2, "Tiling top left going bot right" );
        @hosts = sort( keys(%servers) );
        $current_x
            = $config{screen_reserve_left} + $config{terminal_reserve_left};
        $current_y
            = $config{screen_reserve_top} + $config{terminal_reserve_top};
        $current_row = 0;
        $current_col = 0;
    }
    else {
        logmsg( 2, "Tiling bot right going top left" );
        @hosts = reverse( sort( keys(%servers) ) );
        $current_x
            = $config{screen_reserve_right} 
            - $config{internal_screen_width}
            - $config{terminal_reserve_right}
            - $config{internal_terminal_width};
        $current_y
            = $config{screen_reserve_bottom} 
            - $config{internal_screen_height}
            - $config{terminal_reserve_bottom}
            - $config{internal_terminal_height};

        $current_row = $config{internal_rows} - 1;
        $current_col = $config{internal_columns} - 1;
    }

    # Unmap windows (hide them)
    # Move windows to new locatation
    # Remap all windows in correct order
    foreach my $server (@hosts) {
        logmsg( 3,
            "x:$current_x y:$current_y, r:$current_row c:$current_col" );

        # sf tracker 3061999
        # $xdisplay->req( 'UnmapWindow', $servers{$server}{wid} );

        if ( $config{unmap_on_redraw} =~ /yes/i ) {
            $xdisplay->req( 'UnmapWindow', $servers{$server}{wid} );
        }

        logmsg( 2, "Moving $server window" );
        send_resizemove(
            $servers{$server}{wid},
            $current_x, $current_y,
            $config{internal_terminal_width},
            $config{internal_terminal_height}
        );

        $xdisplay->flush();
        select( undef, undef, undef, 0.1 );    # sleep for a moment for the WM

        if ( $config{window_tiling_direction} =~ /right/i ) {

            # starting top left, and move right and down
            $current_x
                += $config{terminal_reserve_left}
                + $config{terminal_reserve_right}
                + $config{internal_terminal_width};

            $current_col += 1;
            if ( $current_col == $config{internal_columns} ) {
                $current_y
                    += $config{terminal_reserve_top}
                    + $config{terminal_reserve_bottom}
                    + $config{internal_terminal_height};
                $current_x = $config{screen_reserve_left}
                    + $config{terminal_reserve_left};
                $current_row++;
                $current_col = 0;
            }
        }
        else {

            # starting bottom right, and move left and up

            $current_col -= 1;
            if ( $current_col < 0 ) {
                $current_row--;
                $current_col = $config{internal_columns};
            }
        }
    }

    # Now remap in right order to get overlaps correct
    if ( $config{window_tiling_direction} =~ /right/i ) {
        foreach my $server ( reverse(@hosts) ) {
            logmsg( 2, "Setting focus on $server" );
            $xdisplay->req( 'MapWindow', $servers{$server}{wid} );

            # flush every time and wait a moment (The WMs are so slow...)
            $xdisplay->flush();
            select( undef, undef, undef, 0.1 );    # sleep for a mo
        }
    }
    else {
        foreach my $server (@hosts) {
            logmsg( 2, "Setting focus on $server" );
            $xdisplay->req( 'MapWindow', $servers{$server}{wid} );

            # flush every time and wait a moment (The WMs are so slow...)
            $xdisplay->flush();
            select( undef, undef, undef, 0.1 );    # sleep for a mo
        }
    }

    # and as a last item, set focus back onto the console
    show_console();
}

sub capture_terminal() {
    logmsg( 0, "Stub for capturing a terminal window" );

    return if ( $options{debug} < 6 );

    # should never see this - all experimental anyhow

    foreach my $server ( keys(%servers) ) {
        foreach my $data ( keys( %{ $servers{$server} } ) ) {
            print "server $server key $data is $servers{$server}{$data}\n";
        }
    }

    #return;

    my %atoms;

    for my $atom ( $xdisplay->req( 'ListProperties', $servers{loki}{wid} ) ) {
        $atoms{ $xdisplay->atom_name($atom) }
            = $xdisplay->req( 'GetProperty', $servers{loki}{wid},
            $atom, "AnyPropertyType", 0, 200, 0 );

        print $xdisplay->atom_name($atom), " ($atom) => ";
        print "join here\n";
        print join(
            "\n",
            $xdisplay->req(
                'GetProperty', $servers{loki}{wid},
                $atom, "AnyPropertyType", 0, 200, 0
            )
            ),
            "\n";
    }

    print "list by number\n";
    for my $atom ( 1 .. 90 ) {
        print "$atom: ", $xdisplay->req( 'GetAtomName', $atom ), "\n";
        print join(
            "\n",
            $xdisplay->req(
                'GetProperty', $servers{loki}{wid},
                $atom, "AnyPropertyType", 0, 200, 0
            )
            ),
            "\n";
    }
    print "\n";

    print "size hints\n";
    print join(
        "\n",
        $xdisplay->req(
            'GetProperty', $servers{loki}{wid},
            42, "AnyPropertyType", 0, 200, 0
        )
        ),
        "\n";

    print "atom list by name\n";
    foreach ( keys(%atoms) ) {
        print "atom :$_: = $atoms{$_}\n";
    }

    print "geom\n";
    print join " ", $xdisplay->req( 'GetGeometry', $servers{loki}{wid} ), $/;
    print "attrib\n";
    print join " ",
        $xdisplay->req( 'GetWindowAttributes', $servers{loki}{wid} ),
        $/;
}

sub toggle_active_state() {
    logmsg( 2, "Toggling active state of all hosts" );

    foreach my $svr ( sort( keys(%servers) ) ) {
        $servers{$svr}{active} = not $servers{$svr}{active};
    }
}

sub close_inactive_sessions() {
    logmsg( 2, "Closing all inactive sessions" );

    foreach my $svr ( sort( keys(%servers) ) ) {
        terminate_host($svr) if ( !$servers{$svr}{active} );
    }
    build_hosts_menu();
}

sub add_host_by_name() {
    logmsg( 2, "Adding host to menu here" );

    $windows{host_entry}->focus();
    my $answer = $windows{addhost}->Show();

    if ( $answer ne "Add" ) {
        $menus{host_entry} = "";
        return;
    }

    if ( $menus{host_entry} ) {
        logmsg( 2, "host=", $menus{host_entry} );
        open_client_windows(
            resolve_names( split( /\s+/, $menus{host_entry} ) ) );
    }

    if ( $menus{listbox}->curselection() ) {
        my @hosts = $menus{listbox}->get( $menus{listbox}->curselection() );
        logmsg( 2, "host=", join( ' ', @hosts ) );
        open_client_windows( resolve_names(@hosts) );
    }

    build_hosts_menu();
    $menus{host_entry} = "";

    # retile, or bring console to front
    if ( $config{window_tiling} eq "yes" ) {
        retile_hosts();
    }
    else {
        show_console();
    }
}

sub build_hosts_menu() {
    logmsg( 2, "Building hosts menu" );

    # first, empty the hosts menu from the 4th entry on
    my $menu = $menus{bar}->entrycget( 'Hosts', -menu );
    my $host_menu_static_items = 5;
    $menu->delete( $host_menu_static_items, 'end' );

    logmsg( 3, "Menu deleted" );

    # add back the seperator
    $menus{hosts}->separator;

    logmsg( 3, "Parsing list" );

    my $menu_item_counter = $host_menu_static_items;
    foreach my $svr ( sort( keys(%servers) ) ) {
        logmsg( 3, "Checking $svr and restoring active value" );
        my $colbreak = 0;
        if ( $menu_item_counter > $config{max_host_menu_items} ) {
            $colbreak          = 1;
            $menu_item_counter = 1;
        }
        $menus{hosts}->checkbutton(
            -label       => $svr,
            -variable    => \$servers{$svr}{active},
            -columnbreak => $colbreak,
        );
        $menu_item_counter++;
    }
    logmsg( 3, "Changing window title" );
    change_main_window_title();
    logmsg( 2, "Done" );
}

sub setup_repeat() {
    $config{internal_count} = 0;

    # if this is too fast then we end up with queued invocations
    # with no time to run anything else
    $windows{main_window}->repeat(
        500,
        sub {
            $config{internal_count} = 0
                if ( $config{internal_count} > 60000 );    # reset if too high
            $config{internal_count}++;
            my $build_menu = 0;
            logmsg( 5, "Running repeat (count=$config{internal_count})" );

     #logmsg( 3, "Number of servers in hash is: ", scalar( keys(%servers) ) );

            foreach my $svr ( keys(%servers) ) {
                if ( defined( $servers{$svr}{pid} ) ) {
                    if ( !kill( 0, $servers{$svr}{pid} ) ) {
                        $build_menu = 1;
                        delete( $servers{$svr} );
                        logmsg( 0, "$svr session closed" );
                    }
                }
                else {
                    warn("Lost pid of $svr; deleting\n");
                    delete( $servers{$svr} );
                }
            }

            # get current number of clients
            $config{internal_total} = int( keys(%servers) );

            #logmsg( 3, "Number after tidy is: ", $config{internal_total} );

            # get current number of clients
            $config{internal_total} = int( keys(%servers) );

            #logmsg( 3, "Number after tidy is: ", $config{internal_total} );

            # If there are no hosts in the list and we are set to autoquit
            if (   $config{internal_total} == 0
                && $config{auto_quit} =~ /yes/i )
            {

                # and some clients were actually opened...
                if ( $config{internal_activate_autoquit} ) {
                    logmsg( 2, "Autoquitting" );
                    exit_prog;
                }
            }

            # rebuild host menu if something has changed
            build_hosts_menu() if ($build_menu);

            # clean out text area, anyhow
            $menus{entrytext} = "";

            #logmsg( 3, "repeat completed" );
        }
    );
    logmsg( 2, "Repeat setup" );
}

sub write_default_user_config() {
    return if ( !$ENV{HOME} || -e "$ENV{HOME}/.csshrc" );

    if ( open( CONFIG, ">", "$ENV{HOME}/.csshrc" ) ) {
        foreach ( sort( keys(%config) ) ) {

            # do not output internal vars
            next if ( $_ =~ /^internal/ );
            print CONFIG "$_=$config{$_}\n";
        }
        close(CONFIG);
    }
    else {
        logmsg( 1, "Unable to write default $ENV{HOME}/.csshrc file" );
    }
}

### Window and menu definitions ###

sub create_windows() {
    logmsg( 2, "create_windows: started" );
    $windows{main_window} = MainWindow->new( -title => "ClusterSSH" );
    $windows{main_window}->withdraw;    # leave withdrawn until needed

    if ( defined( $config{console_position} )
        && $config{console_position} =~ /[+-]\d+[+-]\d+/ )
    {
        $windows{main_window}->geometry( $config{console_position} );
    }

    $menus{entrytext}    = "";
    $windows{text_entry} = $windows{main_window}->Entry(
        -textvariable      => \$menus{entrytext},
        -insertborderwidth => 4,
        -width             => 25,
        )->pack(
        -fill   => "x",
        -expand => 1,
        );

    $windows{history} = $windows{main_window}->Scrolled(
        "ROText",
        -insertborderwidth => 4,
        -width             => $config{history_width},
        -height            => $config{history_height},
        -state             => 'normal',
        -takefocus         => 0,
    );
    $windows{history}->bindtags(undef);

    if ( $config{show_history} ) {
        $windows{history}->pack(
            -fill   => "x",
            -expand => 1,
        );
    }

    $windows{main_window}->bind( '<Destroy>' => \&exit_prog );

    # remove all Paste events so we set them up cleanly
    $windows{main_window}->eventDelete('<<Paste>>');

    # Set up paste events from scratch
    if ( $config{key_paste} && $config{key_paste} ne "null" ) {
        $windows{main_window}
            ->eventAdd( '<<Paste>>' => '<' . $config{key_paste} . '>' );
    }

    if ( $config{mouse_paste} && $config{mouse_paste} ne "null" ) {
        $windows{main_window}
            ->eventAdd( '<<Paste>>' => '<' . $config{mouse_paste} . '>' );
    }

    $windows{main_window}->bind(
        '<<Paste>>' => sub {
            logmsg( 2, "PASTE EVENT" );

            $menus{entrytext} = "";
            my $paste_text = '';

            # SelectionGet is fatal if no selection is given
            Tk::catch {
                $paste_text = $windows{main_window}->SelectionGet;
            };

            if ( !length($paste_text) ) {
                warn("Got empty paste event\n");
                return;
            }

            logmsg( 2, "Got text :", $paste_text, ":" );

            update_display_text($paste_text);

            # now sent it on
            foreach my $svr ( keys(%servers) ) {
                send_text( $svr, $paste_text )
                    if ( $servers{$svr}{active} == 1 );
            }
        }
    );

    $windows{help} = $windows{main_window}->Dialog(
        -popover    => $windows{main_window},
        -overanchor => "c",
        -popanchor  => "c",
        -font       => [
            -family => "interface system",
            -size   => 10,
        ],
        -text =>
            "Cluster Administrator Console using SSH\n\nVersion: $VERSION.\n\n"
            . "Bug/Suggestions to http://clusterssh.sf.net/",
    );

    $windows{manpage} = $windows{main_window}->DialogBox(
        -popanchor  => "c",
        -overanchor => "c",
        -title      => "Cssh Documentation",
        -buttons    => ['Close'],
    );

    my $manpage = `pod2text -l -q=\"\" $0`;
    $windows{mantext}
        = $windows{manpage}->Scrolled( "Text", )->pack( -fill => 'both' );
    $windows{mantext}->insert( 'end', $manpage );
    $windows{mantext}->configure( -state => 'disabled' );

    $windows{addhost} = $windows{main_window}->DialogBox(
        -popover        => $windows{main_window},
        -popanchor      => 'n',
        -title          => "Add Host(s) or Cluster(s)",
        -buttons        => [ 'Add', 'Cancel' ],
        -default_button => 'Add',
    );

    if ( $config{max_addhost_menu_cluster_items}
        && scalar keys %clusters )
    {
        if ( scalar keys %clusters < $config{max_addhost_menu_cluster_items} )
        {
            $menus{listbox} = $windows{addhost}->Listbox(
                -selectmode => 'extended',
                -height     => scalar keys %clusters,
            )->pack();
        }
        else {
            $menus{listbox} = $windows{addhost}->Scrolled(
                'Listbox',
                -scrollbars => 'e',
                -selectmode => 'extended',
                -height     => $config{max_addhost_menu_cluster_items},
            )->pack();
        }
        $menus{listbox}->insert( 'end', sort keys %clusters );
    }

    $windows{host_entry} = $windows{addhost}->add(
        'LabEntry',
        -textvariable => \$menus{host_entry},
        -width        => 20,
        -label        => 'Host',
        -labelPack    => [ -side => 'left', ],
    )->pack( -side => 'left' );
    logmsg( 2, "create_windows: completed" );
}

sub capture_map_events() {

    # pick up on console minimise/maximise events so we can do all windows
    $windows{main_window}->bind(
        '<Map>' => sub {
            logmsg( 3, "Entering MAP" );

            my $state = $windows{main_window}->state();
            logmsg( 3,
                "state=$state previous=$config{internal_previous_state}" );
            logmsg( 3, "Entering MAP" );

            if ( $config{internal_previous_state} eq $state ) {
                logmsg( 3, "repeating the same" );
            }

            if ( $config{internal_previous_state} eq "mid-change" ) {
                logmsg( 3, "dropping out as mid-change" );
                return;
            }

            logmsg( 3,
                "state=$state previous=$config{internal_previous_state}" );

            if ( $config{internal_previous_state} eq "iconic" ) {
                logmsg( 3, "running retile" );

                retile_hosts();

                logmsg( 3, "done with retile" );
            }

            if ( $config{internal_previous_state} ne $state ) {
                logmsg( 3, "resetting prev_state" );
                $config{internal_previous_state} = $state;
            }
        }
    );

 #    $windows{main_window}->bind(
 #        '<Unmap>' => sub {
 #            logmsg( 3, "Entering UNMAP" );
 #
 #            my $state = $windows{main_window}->state();
 #            logmsg( 3,
 #                "state=$state previous=$config{internal_previous_state}" );
 #
 #            if ( $config{internal_previous_state} eq $state ) {
 #                logmsg( 3, "repeating the same" );
 #            }
 #
 #            if ( $config{internal_previous_state} eq "mid-change" ) {
 #                logmsg( 3, "dropping out as mid-change" );
 #                return;
 #            }
 #
 #            if ( $config{internal_previous_state} eq "normal" ) {
 #                logmsg( 3, "withdrawing all windows" );
 #                foreach my $server ( reverse( keys(%servers) ) ) {
 #                    $xdisplay->req( 'UnmapWindow', $servers{$server}{wid} );
 #                    if ( $config{unmap_on_redraw} =~ /yes/i ) {
 #                        $xdisplay->req( 'UnmapWindow',
 #                            $servers{$server}{wid} );
 #                    }
 #                }
 #                $xdisplay->flush();
 #            }
 #
 #            if ( $config{internal_previous_state} ne $state ) {
 #                logmsg( 3, "resetting prev_state" );
 #                $config{internal_previous_state} = $state;
 #            }
 #        }
 #    );
}

# for all key event, event hotkeys so there is only 1 key binding
sub key_event {
    my $event     = $Tk::event->T;
    my $keycode   = $Tk::event->k;
    my $keysymdec = $Tk::event->N;
    my $keysym    = $Tk::event->K;
    my $state     = $Tk::event->s || 0;

    $menus{entrytext} = "";

    logmsg( 3, "=========" );
    logmsg( 3, "event    =$event" );
    logmsg( 3, "keysym   =$keysym (state=$state)" );
    logmsg( 3, "keysymdec=$keysymdec" );
    logmsg( 3, "keycode  =$keycode" );
    logmsg( 3, "state    =$state" );
    logmsg( 3, "codetosym=$keycodetosym{$keysymdec}" )
        if ( $keycodetosym{$keysymdec} );
    logmsg( 3, "symtocode=$keysymtocode{$keysym}" );
    logmsg( 3, "keyboard =$keyboardmap{ $keysym }" )
        if ( $keyboardmap{$keysym} );

    #warn("debug stop point here");
    if ( $config{use_hotkeys} eq "yes" ) {
        my $combo = $Tk::event->s . $Tk::event->K;

        $combo =~ s/Mod\d-//;

        logmsg( 3, "combo=$combo" );

        foreach my $hotkey ( grep( /key_/, keys(%config) ) ) {
            my $key = $config{$hotkey};
            next if ( $key eq "null" );    # ignore disabled keys

            logmsg( 3, "key=:$key:" );
            if ( $combo =~ /^$key$/ ) {
                if ( $event eq "KeyRelease" ) {
                    logmsg( 2, "Received hotkey: $hotkey" );
                    send_text_to_all_servers('%s')
                        if ( $hotkey eq "key_clientname" );
                    add_host_by_name()
                        if ( $hotkey eq "key_addhost" );
                    retile_hosts("force")
                        if ( $hotkey eq "key_retilehosts" );
                    show_history() if ( $hotkey eq "key_history" );
                    exit_prog()    if ( $hotkey eq "key_quit" );
                }
                return;
            }
        }
    }

    # look for a <Control>-d and no hosts, so quit
    exit_prog()
        if ( $state =~ /Control/ && $keysym eq "d" and !%servers );

    update_display_text( $keycodetosym{$keysymdec} )
        if ( $event eq "KeyPress" && $keycodetosym{$keysymdec} );

    # for all servers
    foreach ( keys(%servers) ) {

        # if active
        if ( $servers{$_}{active} == 1 ) {
            logmsg( 3,
                "Sending event $event with code $keycode (state=$state) to window $servers{$_}{wid}"
            );

            $xdisplay->SendEvent(
                $servers{$_}{wid},
                0,
                $xdisplay->pack_event_mask($event),
                $xdisplay->pack_event(
                    'name'        => $event,
                    'detail'      => $keycode,
                    'state'       => $state,
                    'time'        => time(),
                    'event'       => $servers{$_}{wid},
                    'root'        => $xdisplay->root(),
                    'same_screen' => 1,
                )
            ) || warn("Error returned from SendEvent: $!");
        }
    }
    $xdisplay->flush();
}

sub create_menubar() {
    logmsg( 2, "create_menubar: started" );
    $menus{bar} = $windows{main_window}->Menu;
    $windows{main_window}->configure( -menu => $menus{bar} );

    $menus{file} = $menus{bar}->cascade(
        -label     => 'File',
        -menuitems => [
            [   "command",
                "Show History",
                -command     => \&show_history,
                -accelerator => $config{key_history},
            ],
            [   "command",
                "Exit",
                -command     => \&exit_prog,
                -accelerator => $config{key_quit},
            ]
        ],
        -tearoff => 0,
    );

    $menus{hosts} = $menus{bar}->cascade(
        -label     => 'Hosts',
        -tearoff   => 1,
        -menuitems => [
            [   "command",
                "Retile Windows",
                -command     => \&retile_hosts,
                -accelerator => $config{key_retilehosts},
            ],

#         [ "command", "Capture Terminal",    -command => \&capture_terminal, ],
            [   "command",
                "Toggle active state",
                -command => \&toggle_active_state,
            ],
            [   "command",
                "Close inactive sessions",
                -command => \&close_inactive_sessions,
            ],
            [   "command",
                "Add Host(s) or Cluster(s)",
                -command     => \&add_host_by_name,
                -accelerator => $config{key_addhost},
            ],
            '',
        ],
    );

    $menus{send} = $menus{bar}->cascade(
        -label   => 'Send',
        -tearoff => 1,
    );

    populate_send_menu();

    $menus{help} = $menus{bar}->cascade(
        -label     => 'Help',
        -menuitems => [
            [ 'command', "About", -command => sub { $windows{help}->Show } ],
            [   'command', "Documentation",
                -command => sub { $windows{manpage}->Show }
            ],
        ],
        -tearoff => 0,
    );

    #$windows{main_window}->bind(
    #'<Key>' => \&key_event,
    #);
    $windows{main_window}->bind( '<KeyPress>'   => \&key_event, );
    $windows{main_window}->bind( '<KeyRelease>' => \&key_event, );
    logmsg( 2, "create_menubar: completed" );
}

sub populate_send_menu_entries_from_xml {
    my ( $menu, $menu_xml ) = @_;

    foreach my $menu_ref ( @{ $menu_xml->{menu} } ) {
        if ( $menu_ref->{menu} ) {
            $menus{ $menu_ref->{title} }
                = $menu->cascade( -label => $menu_ref->{title}, );
            populate_send_menu_entries_from_xml( $menus{ $menu_ref->{title} },
                $menu_ref, );
            if ( $menu_ref->{detach} && $menu_ref->{detach} =~ m/y/i ) {
                $menus{ $menu_ref->{title} }->menu->tearOffMenu()->raise;
            }
        }
        else {
            my $command     = undef;
            my $accelerator = undef;
            if ( $menu_ref->{command} ) {
                $command = sub {
                    send_text_to_all_servers( $menu_ref->{command}[0] );
                };
            }
            if ( $menu_ref->{accelerator} ) {
                $accelerator = $menu_ref->{accelerator};
            }
            $menu->command(
                -label       => $menu_ref->{title},
                -command     => $command,
                -accelerator => $accelerator,
            );
        }
    }

    return;
}

sub populate_send_menu {

    #    my @menu_items = ();
    if ( !-r $config{send_menu_xml_file} ) {
        logmsg( 2, 'Using default send menu' );

        $menus{send}->command(
            -label       => 'Hostname',
            -command     => [ \&send_text_to_all_servers, '%s' ],
            -accelerator => $config{key_clientname},
        );
    }
    else {
        logmsg(
            2,
            'Using xml send menu definition from ',
            $config{send_menu_xml_file}
        );

        eval { require XML::Simple; };
        die 'Cannot load XML::Simple - has it been installed?  ', $@ if ($@);

        my $xml = XML::Simple->new( ForceArray => 1, );
        my $menu_xml = $xml->XMLin( $config{send_menu_xml_file} );

        logmsg( 3, 'xml send menu: ', $/, $xml->XMLout($menu_xml) );

        if ( $menu_xml->{detach} && $menu_xml->{detach} =~ m/y/i ) {
            $menus{send}->menu->tearOffMenu()->raise;
        }

        populate_send_menu_entries_from_xml( $menus{send}, $menu_xml );
    }

    return;
}

sub run {
    my ($self) = @_;
### main ###

    # Note: getopts returned "" if it finds any options it doesnt recognise
    # so use this to print out basic help
    pod2usage( -verbose => 1 )
        if ( !GetOptions( \%options, @options_spec ) );
    pod2usage( -verbose => 1 ) if ( $options{'?'} || $options{help} );
    pod2usage( -verbose => 2 ) if ( $options{H}   || $options{man} );

    if ( $options{version} ) {
        print "Version: $VERSION\n";
        exit 0;
    }

    $options{debug} ||= 0;

    # only get xdisplay if we got past usage and help stuff
    $xdisplay = X11::Protocol->new();

    if ( !$xdisplay ) {
        die("Failed to get X connection\n");
    }

    if ( $options{d} && $options{D} ) {
        $options{debug} += 3;
        logmsg( 0,
            'NOTE: -d and -D are deprecated - use "--debug 3" instead' );
    }
    elsif ( $options{d} ) {
        $options{debug} += 1;
        logmsg( 0, 'NOTE: -d is deprecated - use "--debug 1" instead' );
    }
    elsif ( $options{D} ) {
        $options{debug} += 2;
        logmsg( 0, 'NOTE: -D is deprecated - use "--debug 2" instead' );
    }

    # restrict to max level
    $options{debug} = 4 if ( $options{debug} && $options{debug} > 4 );
    $self->set_debug_level( $options{debug} );

    logmsg( 2, "VERSION: $VERSION" );

    load_config_defaults();
    load_configfile();

    dump_config() if ( $options{'output-config'} );

    check_ssh_hostnames();

    evaluate_commands() if ( $options{evaluate} );

    load_keyboard_map();

    get_clusters();

    list_tags() if ( $options{'list'} );

    if (@ARGV) {
        @servers = resolve_names(@ARGV);
    }
    else {
        if ( $clusters{default} ) {
            @servers = resolve_names( split( /\s+/, $clusters{default} ) );
        }
    }

    create_windows();
    create_menubar();

    change_main_window_title();

    logmsg( 2, "Capture map events" );
    capture_map_events();

    setup_helper_script();
    open_client_windows(@servers);

    # Check here if we are tiling windows.  Here instead of in func so
    # can be tiled from console window if wanted
    if ( $config{window_tiling} eq "yes" ) {
        retile_hosts();
    }
    else {
        show_console();
    }

    build_hosts_menu();

    logmsg( 2, "Sleeping for a mo" );
    select( undef, undef, undef, 0.5 );

    logmsg( 2, "Sorting focus on console" );
    $windows{text_entry}->focus();

    logmsg( 2, "Marking main window as user positioned" );
    $windows{main_window}->positionfrom('user')
        ;    # user puts it somewhere, leave it there

    logmsg( 2, "Setting up repeat" );
    setup_repeat();

    logmsg( 2, "Writing default user configuration" );
    write_default_user_config();

    # Start event loop
    logmsg( 2, "Starting MainLoop" );
    MainLoop();

    # make sure we leave program in an expected way
    exit_prog();
}

1;

__END__

=pod

=head1 NAME

App::ClusterSSH - A container for functions of the ClusterSSH programs

=head1 SYNOPSIS

There is nothing in this module for public consumption.  See documentation
for F<cssh>, F<crsh>, F<ctelnet>, or F<cscp> instead.

=head1 DESCRIPTION

THis is the core for App::ClusterSSH.  You should probably look at L<cssh> 
instead.

=head1 SUBROUTINES/METHODS

These methods are listed here to tidy up Pod::Coverage test reports but
will most likely be moved into other modules.  There are some notes within 
the code until this time.

=over 2

=item REAPER

=item add_host_by_name

=item build_hosts_menu

=item capture_map_events

=item capture_terminal

=item change_main_window_title

=item  check_config

=item  check_ssh_hostnames

=item  close_inactive_sessions

=item  create_menubar

=item  create_windows

=item  dump_config

=item  list_tags

=item  evaluate_commands

=item  exit_prog

=item  find_binary

=item  get_clusters

=item  get_font_size

=item  get_keycode_state

=item  key_event

=item  load_config_defaults

=item  load_configfile

=item  load_keyboard_map

=item  logmsg

=item  new

=item  open_client_windows

=item  parse_config_file

=item  pick_color

=item  populate_send_menu

=item  populate_send_menu_entries_from_xml

=item  resolve_names

=item  retile_hosts

=item  run

=item  send_resizemove

=item  send_text

=item  send_text_to_all_servers

=item  setup_helper_script

=item  setup_repeat

=item  show_console

=item  show_history

=item  split_hostname

=item  terminate_host

=item  toggle_active_state

=item  update_display_text

=item  write_default_user_config
                                           
=back

=head1 BUGS

Please report any bugs or feature requests to C<bug-app-clusterssh at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-ClusterSSH>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc App::ClusterSSH

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=App-ClusterSSH>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/App-ClusterSSH>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/App-ClusterSSH>

=item * Search CPAN

L<http://search.cpan.org/dist/App-ClusterSSH/>

=back

=head1 ACKNOWLEDGEMENTS

Please see the THANKS file from the original distribution.

=head1 AUTHOR

Duncan Ferguson, C<< <duncan_j_ferguson at yahoo.co.uk> >>

=head1 COPYRIGHT & LICENSE

Copyright 1999-2010 Duncan Ferguson, all rights reserved.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=cut

1;
