#!/usr/bin/perl
#
# magmakeys
# by Stefan Tomanek <stefan@pico.ruhr.de>
# http://wertarbyte.de/magmakeys/
# 
# Magmakey is a system wide hotkey daemon.
# It watches all connected input devices for
# key and switch events and can launch arbitrary
# commands when certain events are observed.
#
# It is designed to handle hardware specific keys
# like wireless or suspend controls that usually
# are not user specific

use constant VERSION => "0.8.1";

use strict;

=pod

=head1 NAME

magmakeys -- global hotkey event daemon

=head1 SYNOPSIS

B<magmakeys> [B<-k> I<dir>] [B<--dump>] [B<-t> I<tablefile>] [B<--hal> | B<--nohal> [B<-d> I<device>] [I<devices...>]] [B<--ignore-event I<event>]

=head1 DESCRIPTION

Magmakeys is a hotkey daemon that operates on a system wide scale. It watches all connected input devices
for key, switch or button events and can launch arbitrary commands specified by the administrator. In contrast
to hotkey services provided by desktop environments, Magmakeys is especially suited to hardware related switches
like volume or wifi control; it works independently from a specific user being logged in.

=head1 OPTIONS AND ARGUMENTS

=over

=item B<--help>

Shows usage instructions

=item B<--keydir> F<directory> | B<-k> F<directory>

Read key configuration files from F<directory>
(mandatory when not using B<--dump>)

=item B<--hal>

Enables HAL for detecting input devices (default)

=item B<--nohal>

Disables the use of HAL for detecting input devices;
Devices that should be observed must be specified
on the command line (see B<--dev>)

=item B<--dev> F<dev> | B<-d> F<dev>

Specifies a single input device I<dev> to read from

=item B<--dump>

Dump all recognized events to STDOUT

=item B<--ignore-event> I<event>

Ignore received input events with the name I<event>. These events will show up in <B--dump>, but not change the key state. This is useful for excluding modifier keys (like the Fn key on notebooks) from showing up showing up erratically in the set of pressed keys.

=back

Additional command line arguments are considered filenames of input devices.

=head1 CONFIGURATION

=head2 Handling key combinations and switch state changes

The hotkey bindings used by Magmakeys are set in the configuration files placed in the directory F</etc/magmakeys/keys.d/> (or any other directory specified by B<-k>). Each line consists of three segments:
The symbolic name of the key combination or event name to react on, the value carried by the expected event, and of course the command to be launched.

The event names are translated by looking up the kernel codes through the file F</usr/share/magmakeys/eventcodes.txt> (or any other file specified by B<-t>) and can be found by pressing the selected key while running magmakeys with the option B<--dump>.

Key events carry the value I<1> for a key being pressed and transmit the payload I<0> when it is released; holding the key down constantly yields events with a value of I<2>.

The command can include any number of arguments. Please include the full path to avoid trouble through different $PATH settings for the daemon and your interactive session.

The three fields are seperated by an arbitrary number of whitespaces, while anything behind a # character is ignored and considered a comment.

=head1 EXAMPLE

=head2 Starting the daemon

B<magmakeys --hal --dump>

Dump all events processable by magmakeys to the console; this is useful to find out the correct event name for a specific key.

B<magmakeys -k /etc/magmakeys/keys.d/>

Retrieve the list of input devices from HAL (default) and react to events according to the configuration files placed in I</etc/magmakeys/keys.d/>.

B<magmakeys --dump --nohal /dev/input/event5 /dev/input/event6>

Disable the use of HAL and only dump events from the two devices specified

=head2 Configuration files

Any number of event handlers can be placed in the configuration file:

    # /etc/magmakeys/keys.d/example
    #
    # Suspend the system
    KEY_SLEEP       1       /usr/sbin/hibernate-ram
    
    # Change mixer volume when pressing the appropiate keys (or holding them)
    KEY_VOLUMEUP    1,2    /usr/bin/amixer set Master 5%+
    KEY_VOLUMEDOWN  1,2    /usr/bin/amixer set Master 5%-
    
    # Change the beeper volume when pressing shift
    KEY_LEFTSHIFT+KEY_VOLUMEUP    1,2     /usr/bin/amixer set Beep 5%+
    KEY_LEFTSHIFT+KEY_VOLUMEDOWN  1,2     /usr/bin/amixer set Beep 5%-

=head1 FILES

=over

=item F</etc/magmakeys/keys.d/>

Configuration file directory for hotkey definitions

=item F</usr/share/magmakeys/eventcodes.txt>

Translation table for symbolic event names and numeric event codes

=item F</etc/init.d/magmakeys>

Init script for the daemon

=item F</etc/default/magmakeys>

Configuration file to pass additional options to the daemon when using the init script

=back

=head1 AUTHOR

Stefan Tomanek E<lt>stefan@pico.ruhr.deE<gt>

=cut

sub showHelp {
    my ($message, $error) = @_;

    my $msg = "Magmakeys ".VERSION."\n";
    $msg .= "\n$message\n" if $message;
    my $exit_status = ($error ? 2 : 0);
    my $filehandle = ($error ? \*STDERR : \*STDOUT);
    pod2usage(   -msg     => $msg,
                 -exitval => $exit_status,
                 -verbose => 1,
                 -output  => $filehandle );
}

use magmakeys::InputEvent;
use magmakeys::DeviceFile;
use magmakeys::SelectDeviceWatcher;
use magmakeys::DBusDeviceWatcher;
use magmakeys::EventFileManager;

use POSIX ();

use Getopt::Long;
use Pod::Usage;

my $use_hal = 1;
my $help = 0;
my $dump_events = 0;
my $config_file = undef;
my $key_dir = undef;
my $table_file = "/usr/share/magmakeys/eventcodes.txt";
my @manual_devs = ();
my @ignore = ();

my $event_manager;
my $watcher;

sub say {
    my ($error, @msg) = @_;
    if ($error) {
        print STDERR @msg, "\n";
    } else {
        print STDOUT @msg, "\n";
    }
}

sub signal_hup {
    say 0, "SIGHUP received, reloading configuration";
    load_config();
    
    # restart device watcher
    $watcher->watch();
}

# install signal handlers
$SIG{CHLD} = "IGNORE";
my $sigset = POSIX::SigSet->new();
my $hup_action = POSIX::SigAction->new('signal_hup', $sigset, &POSIX::SA_NODEFER);
POSIX::sigaction(&POSIX::SIGHUP, $hup_action);

# parse command line
my $result = GetOptions ("help|h"     => \$help,
                         "hal!"       => \$use_hal,
                         "dump"       => \$dump_events,
                         "dev|d=s"    => \@manual_devs,
                         "keydir|k=s" => \$key_dir,
                         "ignore-event|i=s" => \@ignore,
                         "table|t=s"  => \$table_file);

if ($help || not $result) {
    showHelp("", not $help);
}

# additional arguments not touched by Getopt are considered input devices
for my $d (@ARGV) {
    push @manual_devs, $d;
}

if (@manual_devs && $use_hal) {
    showHelp("HAL and manually specified devices are mutually exclusive.", 1);
}

# load constants from file
unless ( magmakeys::InputEvent->load_constants($table_file) ) {
    say 1, "Error loading constants from '$table_file'";
    exit 1;
}

if ($use_hal) {
    $watcher = new magmakeys::DBusDeviceWatcher();
} else {
    $watcher = new magmakeys::SelectDeviceWatcher();
    # add specified devices
    for my $d (@manual_devs) {
        my $dev = new magmakeys::DeviceFile($d);
        unless ($dev) {
            say 1, "Error opening '$d'";
            exit 1;
        }
        $watcher->add_filehandle($dev, "");
    }
}
$watcher->dump($dump_events);

$event_manager = new magmakeys::EventFileManager();
$event_manager->dump($dump_events);
# feed ignored events
for (@ignore) {
    $event_manager->ignore_event($_, 1);
}
$watcher->add_listener($event_manager);

sub load_config {
    $event_manager->flush_handlers();
    if (defined $key_dir) {
        # load all config files from the directory
        unless ( -d $key_dir ) {
            say 1, "Error reading key config directory '$key_dir'";
        }

        foreach my $c ( <$key_dir/*> ) {
            my $confname = substr($c,length($key_dir)+1);
            next unless ( $confname =~ /^[a-z0-9]+$/ || 
                          $confname =~ /^_?([a-z0-9_.]+-)+[a-z0-9]+$/ || 
                          $confname =~ /^[a-z0-9][a-z0-9-]*$/ );
            next if ($confname =~ /\.dpkg-(?:dist|new|old|tmp)$/ ||
                     $confname =~ /\.disabled$/);
            say 0, "Loading config file '$c'";
            my $success = $event_manager->read_handlers_from_file($c);
            unless ($success) {
                say 1, "Error reading config file '$config_file'";
            }
        }
    }
}
load_config();

unless ( $dump_events || defined $key_dir ) {
    showHelp("Either --keydir or --dump is required. Nothing to do, quitting.", 1);
}

$watcher->watch();
