#!/usr/bin/perl

# drakroam: wireless network roaming GUI
# beta version uses wlandetect as backend
# Austin Acton, 2004
# <austin@mandrake.org>
# Licensed under the GPL

# problems
# - deletes comments in config file
# - expects an ifcfg file for static IP configurations (not uncommon)
# - roaming status fails (no idea why)
#   maybe same reason bash-completion killall can't see wlandetect?

# todo
# - make known and available lists have more rows by default (why so small?)
# - refresh status every x seconds
# - find a good way to drop the access point and resume roaming
# - make 'key' column wider by default

use lib qw(/usr/lib/libDrakX);

use standalone;
use common;
use run_program;

require ugtk2;
ugtk2->import(qw(:wrappers :create));
use Gtk2::SimpleList;

require_root_capability();

# global settings
my $config = '/etc/wlandetect.conf';
my $route = '/sbin/route';
my $AboutFile = 'ABOUT';
my $HelpFile = 'README';
my $WLanDetect = '/usr/sbin/wlandetect';
my $IWList = '/sbin/iwlist';
my $IWConfig = '/sbin/iwconfig';
my $IFConfig = '/sbin/ifconfig';
my $IFUp = '/sbin/ifup';
my $IFDown = '/sbin/ifdown';
my $DHClient = '/sbin/dhclient';
my $device;

# initialize variables
my $ScanInterval = 30; # tell deamon to search for new nets every x seconds


my $ScanEntry = Gtk2::Entry->new;
$ScanEntry->set_width_chars(4);
$ScanEntry->set_text($ScanInterval);

my $KnownList = Gtk2::SimpleList->new(
                                      "ESSID" => "text",
                                      "Mode" => "text",
                                      "Channel" => "int",
                                      "Key" => "text",
                                      "DHCP" => "bool"
                                     );
$KnownList->get_selection->set_mode('single');
$KnownList->set_reorderable(1);
$KnownList->set_column_editable(1, TRUE); # allow to change mode
$KnownList->set_column_editable(2, TRUE); # allow to change channel
$KnownList->set_column_editable(3, TRUE); # allow to change key

my $AvailableList = Gtk2::SimpleList->new(
                                          "ESSID" => "text",
                                          "Type" => "text",
                                          "Encryption" => "text",
                                          "Signal (%)" => "int"
                                         );
$AvailableList->get_selection->set_mode('single');

my $NetLabel = Gtk2::Label->new;
my $IpLabel = Gtk2::Label->new;
my $GwLabel = Gtk2::Label->new;
my $ModeLabel = Gtk2::Label->new;
my $WepLabel = Gtk2::Label->new;
my $SignalLabel = Gtk2::Label->new;

my $w = ugtk2->new('Drakroam');
gtkadd(gtkset_border_width($w->{window}, 2),
       gtkpack_(Gtk2::VBox->new,
                0, gtkadd(gtkset_border_width(Gtk2::Frame->new("Status"), 2),
                          gtkpack(Gtk2::VBox->new,
                                  create_packtable({ col_spacings => 5, row_spacings => 5, homogenous => 1 },
                                                   [ $NetLabel, $IpLabel, $GwLabel ],
                                                   [ $ModeLabel, $WepLabel, $SignalLabel ],
                                                  ),
                                  gtkpack(create_hbox(),
                                          gtksignal_connect(Gtk2::Button->new("Disconnect"), clicked => sub { &Disconnect }),
                                          gtksignal_connect(Gtk2::Button->new("Refresh"), clicked => sub { &UpdateStatus })
                                         )
                                 )
                         ),
                0, gtkadd(gtkset_border_width(Gtk2::Frame->new("Roaming"), 2),
                          gtkpack(create_hbox(),
                                  gtkpack(Gtk2::VBox->new,
                                          my $RoamStatus = Gtk2::Label->new("Roaming: off"),
                                          gtkpack(create_hbox(),
						  gtksignal_connect(Gtk2::Button->new("Start"), clicked => sub { &StartRoam }),
						  gtksignal_connect(Gtk2::Button->new("Stop"), clicked => sub { &StopRoam })
                                                 )
                                         ),
                                  gtkpack(Gtk2::VBox->new,
                                          Gtk2::Label->new("Scan interval (sec): "),
                                          gtkpack(Gtk2::HBox->new,
                                                  $ScanEntry,
                                                  gtksignal_connect(Gtk2::Button->new("Set"), clicked => sub { &SetInterval })
                                                 )
                                         )
                                 )
                         ),
                1, gtkadd(gtkset_border_width(Gtk2::Frame->new("Known Networks (Drag up/down or edit)"), 2),
                          gtkpack_(Gtk2::VBox->new,
                                   1, create_scrolled_window($KnownList),
                                   0, gtkpack(create_hbox(),
					      gtksignal_connect(Gtk2::Button->new("Remove"), clicked => sub {
								    my ($selected) = $KnownList->get_selected_indices;
								    splice @{$KnownList->{data}}, $selected, 1;
								}),
					      gtksignal_connect(Gtk2::Button->new("Connect"), clicked => sub {
								    my ($selected) = $KnownList->get_selected_indices;
								    &ConnectNow($selected);
								}),
					      gtksignal_connect(Gtk2::Button->new("Save"), clicked => sub { &WriteConfig })
                                             )
                                  )
                         ),
                1, gtkadd(gtkset_border_width(Gtk2::Frame->new("Available Networks"), 2),
                          gtkpack_(Gtk2::VBox->new,
                                   1, create_scrolled_window($AvailableList),
                                   0, gtkpack(create_hbox(),
					      gtksignal_connect(Gtk2::Button->new("Add"), clicked => sub {
								    my @selected = $KnownList->get_selected_indices;
								    my ($mode, $channel, $key);
								    my $essid = $AvailableList->{data}[@selected][0];
								    my $dhcp = 1; # assume dhcp for new networks
								    &AddNet($essid, $mode, $channel, $key, $dhcp);
								}),
					      gtksignal_connect(Gtk2::Button->new("Rescan"), clicked => sub { &UpdateAvailable })
                                             )
                                  )
                         ),
                0, gtkpack(create_hbox(),
                           gtksignal_connect(Gtk2::Button->new("Help"), clicked => sub { &Dialog($HelpFile) }),
                           gtksignal_connect(Gtk2::Button->new("About"), clicked => sub { &Dialog($AboutFile) }),
                           gtksignal_connect(Gtk2::Button->new("Save and close"), clicked => sub {
                                                 &WriteConfig;
                                                 Gtk2->main_quit;
                                             })
                          )
               )
      );

# fill the GUI
&ReadConfig;
&UpdateAll;

sub UpdateAll {
	&UpdateAvailable; #must go first as it defines the device name
	&UpdateStatus;
	&UpdateRoaming;
}

sub UpdateRoaming() {
	my $status = (any { /wlandetect$/ } run_program::get_stdout("ps", "-A")) ? "on" : "off";
	$RoamStatus->set_text("Roaming: $status");
	return FALSE; #- do not update again if launched on timeout
}

sub UpdateStatus() {
	my $CurrentNet = "-";
	my $CurrentIP = "---.---.---.---";
	my $CurrentGW = "---.---.---.---";
	my $CurrentMode = "";
	my $CurrentWEP = "";
	my $CurrentSignal = "-";
	print("Updating\n");
	foreach (run_program::get_stdout($IWConfig)) {
		/ESSID:"(\S*)"/ and $CurrentNet = $1;
		/Mode:(\S*)\s/ and $CurrentMode = $1;
		/key:(\S*)\s/ and $CurrentWEP = $1;
		m!Quality:(\S*)/! and $CurrentSignal = $1;
	}
	foreach (run_program::get_stdout($IFConfig, $device)) {
		if (/inet addr:(\S*)\s/) { $CurrentIP = $1 }
	}
	foreach (run_program::get_stdout($route)) {
		if (/default\s*(\S*)\s/) { $CurrentGW = $1 }
		else { $CurrentGW = "---.---.---.---" }
	}
	$NetLabel->set_text("Net: $CurrentNet");
        $ModeLabel->set_text("Mode: $CurrentMode");
	$IpLabel->set_text("IP: $CurrentIP");
	$WepLabel->set_text("Encryption: $CurrentWEP");
	$GwLabel->set_text("Gateway: $CurrentGW");
	$SignalLabel->set_text("Signal: $CurrentSignal");
}

sub UpdateAvailable() {
	my ($essid, $mode, $wep, $signal);
	print("Running iwlist\n");
	@{$AvailableList->{data}} = ();
	foreach (run_program::get_stdout($IWList, "scan")) {
		/([^ ]+)([ \t]+)Scan completed :/ and $device = $1;
		/([^ ]+)([ \t]+)No scan results/ and  $device = $1;
		/ESSID:"(\S*)"/ and $essid = $1;
		/Mode:(\S*)/ and $mode = $1;
		m!Quality:(\S*)/! and $signal = $1;
		if (/key:(\S*)\s/) {
			$wep = $1;
			print("Mode: $mode, WEP: $wep, Signal: $signal\n");
			push @{$AvailableList->{data}}, [$essid, $mode, $wep, $signal];
		}
	}
}

sub AddNet {
	my ($essid, $mode, $channel, $key, $dhcp) = @_;
	print "Adding net $essid\n";
	push @{$KnownList->{data}}, [ $essid, $mode, $channel, $key, $dhcp ];
}

sub ReadConfig() {
	print "Reading config file.\n\n";
	my $line = 0;
	foreach (cat_($config)) {
		$line++;
                if (/^#/) {} #ignore comments
		elsif (/^\n/) {} #ignore blank lines
		elsif (/([^ \t]+)([ \t]+)(.*)/) {
                	my $essid = $1;
			my $command = $3;
			# setup new network entry
			my $mode;
			my $channel;
			my $key;
			my $dhcp;
			if ($command =~ /mode\s(\S*);/) { $mode = $1 }
			elsif ($command =~ /mode\s(\S*)\s/) { $mode = $1 }
			if ($command =~ /channel\s(\S*);/) { $channel = $1 }
			elsif ($command =~ /channel\s(\S*)\s/) { $channel = $1 }
			if ($command =~ /key\s(\S*);/) { $key = $1 }
			elsif ($command =~ /key\s(\S*)\s/) { $key = $1 }
			if ($command =~ /dhclient/) { $dhcp = 1 }
			else { $dhcp = 0 }
			&AddNet($essid, $mode, $channel, $key, $dhcp);
		}
		else { die "Line $line of configuration file is not parseable.\n" }
	}
}

sub WriteConfig() {
	open(my $CONFIG, "> $config") or die("Can't open configuration file\n");
	print $CONFIG "#wlandetect configuration file\n#format: essid<tab><tab>commands\n#use \@DEV\@ for device name\n";
	foreach my $row (@{$KnownList->{data}}) { # again, lame
		print $CONFIG "$row->[0]\t\t$IWConfig essid $row->[0]";
		if ($row->[1]) {
			print $CONFIG "mode $row->[1] ";
		}
		if ($row->[2]) {
			print $CONFIG "channel $row->[2] ";
		}
		if ($row->[3]) {
			print $CONFIG "key $row->[3] ";
		}
		print $CONFIG "; ";
		if ($row->[4]) {
			print $CONFIG "$IFConfig \@DEV\@ up; $DHClient \@DEV\@";
		}
		else {
			print $CONFIG "$IFUp \@DEV\@"
		}
		print $CONFIG "\n";
	}
	close $CONFIG;
}

sub StartRoam {
	system("killall wlandetect; $WLanDetect -d -t $ScanInterval &");
	Glib::Timeout->add(1000, \&UpdateRoaming);
}

sub StopRoam {
	system("killall wlandetect");
	Glib::Timeout->add(1000, \&UpdateRoaming);
}

sub SetInterval() {
	$ScanInterval = $ScanEntry->get_text;
	system("killall wlandetect; $WLanDetect -d -t $ScanInterval &");
}

sub ConnectNow {
	my ($row) = @_;
	my @command = "";
	push @command, "$IWConfig essid $KnownList->{data}[$row][0] ";
	if ($KnownList->{data}[$row][1]) {
		push @command, "mode $KnownList->{data}[$row][1] ";
	}
	if ($KnownList->{data}[$row][2]) {
		push @command, "channel $KnownList->{data}[$row][2] ";
	}
	if ($KnownList->{data}[$row][3]) {
		push @command, "key $KnownList->{data}[$row][3] ";
	}
	push @command, "; ";
	if ($KnownList->{data}[$row][4]) {
		push @command, "$IFConfig $device up; $DHClient $device";
	}
	else {
		push @command, "$IFUp $device"
	}
	my $ToBash = join("", @command);
	print("Sending $ToBash\n");
	system($ToBash);
	&UpdateStatus;
}

sub Disconnect {
	system("$IFDown $device");
	&UpdateStatus;
}

sub Dialog {
    my ($FilePointer) = @_;
    my $content = join('', cat_($FilePointer));
    # dump into a dialog
    my $AboutWindow = Gtk2::Dialog->new("Drakroam Info", $w->{window},
                                        'destroy-with-parent',
                                        N("Ok") => 'none');
    $AboutWindow->vbox->add(create_scrolled_window(Gtk2::Label->new($content)));
    $AboutWindow->signal_connect(response => sub { $_[0]->destroy });
    $AboutWindow->show_all;
}

# start GUI
print "Starting GUI\n";

$w->main;