#!/usr/bin/perl -w
#
# hotkeyd - performs actions when specific (hot)keys are pressed
# Copyright (C) 2006  Christopher Zimmermann <madroach@zakweb.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
#
# last change 2006-07-29


use strict;

# required modules
require 'sys/ioctl.ph';

use Getopt::Long;
use Config::General;
use Pod::Usage;
use Proc::Daemon;

# sub declarations
sub to_int($);
sub parse_keys($\%\%);
##################################################
# configuration

# Create options parser
my $opt = new Getopt::Long::Parser;
$opt->configure('gnu_getopt');


# Just quickly scan the command line if we can find a -c conffile
my $conffile = '/etc/hotkeydrc'; #default location
$conffile = '/dev/null' unless -r $conffile; #fallback is empty config
$opt->configure('pass_through');
$opt->getoptions('config|c=s' => \$conffile);
$opt->configure('no_pass_through');

# Check if conffile is readable
die "configuration file $conffile not readable" unless -r $conffile;

# Make sure that conffile is only writable by root
my (undef, undef, $mode, undef, $uid, undef, undef,
undef, undef, undef, undef, undef, undef) = stat $conffile;
die "configuration file $conffile may only be writeable by root"
    if $mode & 0022 || $uid != 0;

# Parse the config file
my %config = ParseConfig(
    '-ConfigFile' => $conffile,
    '-AllowMultiOptions' => 'no',
    '-DefaultConfig' => {
	'device' => '/dev/input/event0',
	'pidfile' => '/var/run/hotkeyd.pid',
	'header_path' => '/usr/include/linux',
    },
);

# Now scan for other options
$opt->getoptions(
    'device|d=s' => \$config{device},
    'pidfile|p=s' => \$config{pidfile},
    'dump!' => \$config{dump},
    'help|h!' => sub { pod2usage(1) },
)
    or pod2usage(2);

# Check if the permissions on event device are set sensible
(undef, undef, $mode, undef, undef, undef, undef,
undef, undef, undef, undef, undef, undef) = stat $config{device};
warn "event defice file $config{device} should not be readable by ordinary users"
    if $mode & 0004;

# Get keycodes from linux kernel headers
parse_keys $config{header_path} . '/input.h', my %code, my %type;

# Create key hash with integers as keys
my %key;
while((my $key, my $val) = each %{$config{key}}) {
    # Initialise repeat counter if necessary
    $val->{repeat_counter} = $val->{repeat} if($val->{repeat});
    $key{to_int $key} = $val;
}

##################################################
# daemonize and start listening for events
sub dump_key($$);
sub process_key(\%$);
sub process_events();

# open event device and check the version of the event interface
open EVDEV, $config{device} or die "Can't open event device file: $!";
binmode EVDEV;
# allocate 256 zero bytes for ioctl call
my $ret = chr(0) x 256;
ioctl EVDEV, _IOC(&_IOC_READ, ord 'E', 0x01, 4), $ret
    or die "Couldn't call ioctl: $!";
die 'Event interface version mismatch'
    unless $type{EV_VERSION} == unpack 'I', $ret;
close EVDEV;

# daemonize
unless($config{dump}) {
    die "pidfile $config{pidfile} already exists. Is hotkeyd already running?"
	if -e $config{pidfile};
    Proc::Daemon::Init;
    umask 0133;
    open PIDFILE, '>', $config{pidfile} or die "Can't write pidfile: $!";
    print PIDFILE $$, "\n";
    close PIDFILE;
}

# open event device
open EVDEV, $config{device};
binmode EVDEV;

# main event loop
my $on = 1;
$SIG{TERM} = $SIG{INT} = sub {$on = 0};
while($on) {
    # Get next event and unpack it.
    # pack/unpack template for struct input_event
    my $template = 'L! L! S S l';
    # be careful about machine word size
    read EVDEV, my $event, length pack $template;
    (my $times, my $timeus, my $type, my $code, my $value) =
	unpack $template, $event;

    # Is this a keypress?
    if($type eq $type{'EV_KEY'}) {
	# Is key pressed, released or is it just an auto-repeat?
	my $state;
	$state = 'up' if $value == 0;
	$state = 'down' if $value == 1;
	$state = 'repeat' if $value == 2;

	# Are we only dumping?
	if($config{dump}) {
	    dump_key $code, $state;
	}
	# Do we care about this key?
	elsif(exists $key{$code}) {
	    process_key %{$key{$code}}, $state;
	    dump_key $code, $state;
	}
    }
}
  
close EVDEV;

unlink $config{pidfile} unless $config{dump};
    
sub dump_key($$) {
    my %rev_code = reverse %code;
    if(exists $rev_code{$_[0]}) {
	print "$rev_code{$_[0]} $_[1]\n";
    }
}

sub process_key(\%$) {
    # Only process keypresses or auto-repeated keys if desired
    if(
	$_[1] eq 'down'
	    or
	$_[1] eq 'repeat'
	    and
	$_[0]->{repeat}
	    and
	!--$_[0]->{repeat_counter} and
	$_[0]->{repeat_counter} = $_[0]->{repeat}
    ) {

	# Is an action for this key defined?
	# Actions may be simple shell commands or complex scripts, 
	# scripts should start with a shebang: #!/bin/interpreter
	# #!perleval is a special case. In this case the script
	# will be evaluated in an eval.
	if(defined $_[0]->{action}) {
	    if($_[0]->{action} !~ '^#!(\S*)\n') {
		system $_[0]->{action};
	    }
	    elsif($1 eq 'perleval') {
		eval $_[0]->{action};
	    }
	    else {
		open INTERPRETER, '|-', $1;
		print INTERPRETER $_[0]->{action};
		close INTERPRETER;
	    }
	}
    }
}

# Converts a decimal, octal or hexadecimal number to an integer
# It uses %code to convert keysyms to integers.
sub to_int($)
{
    return $code{$_[0]} if exists $code{$_[0]};
    $_[0] = oct $_[0] if $_[0] =~ /^0/;
    return $_[0];
    die "'$_[0]' is not a valid key.\n";
}

# Scans the input.h header of the linux kernel for keysyms and other
# useful definitions.
sub parse_keys($\%\%)
{
    open INPUT, $_[0] or die "Can't open $_[0]: $!";;
    foreach(<INPUT>) {
	if(/#define\s+((KEY_|BTN_|EV_)\w+)\s+((?:0x)?[[:xdigit:]]+)/) {
	    my $name = $1;
	    my $prefix = $2;
	    my $num = $3;

	    if($prefix eq 'KEY_' || $prefix eq 'BTN_') {
		$num = to_int $num;
		$_[1]{$name} = $num;
	    }
	    elsif($prefix eq 'EV_') {
		$num = to_int $num;
		$_[2]{$name} = $num;
	    }
	}
    }
    close INPUT;
}

__END__

=head1 NAME

hotkeyd - performs actions when specific (hot)keys are pressed

=head1 SYNOPSIS

B<hotkeyd> [B<-h>] [B<--dump>] [B<--config> I<FILE>] [B<--device>
I<DEV>] [B<--pidfile> I<FILE>]

=head1 DESCRIPTION

hotkeyd executes simple commands or complex scripts when certain
(hot)keys are pressed.

To get notified of the keypresses the daemon uses the input event
interface of the 2.6 series of the linux kernel. This means that the
daemon does not rely on X.

=head1 OPTIONS

=over

=item B<-c> I<FILE>, B<--config>=I<FILE>

Use I<FILE> as config file (default is F</etc/hotkeydrc>).

=item B<-d> I<DEV>, B<--device>=I<DEV>

Use I<DEV> as input event device file (default is F</dev/input/event0>).

=item B<-p> I<FILE>, B<--pidfile>=I<FILE>

Write pid in I<FILE> when running as daemon. I<FILE> is removed when
daemon terminates (default is F</var/run/hotkeyd.pid>).

=item B<--dump>

Print the symbolic names of keys pressed

=item B<-h> B<--help>

display short usage information

=back

=head1 CONFIGURATION

hotkeyd uses an apache style config file like documented in
L<Config::General/CONFIG FILE FORMAT>. Here's an example:

=begin

 device = /dev/input/event0

 <key KEY_MUTE>
   action = "amixer -q sset Master toggle"
 </key>

 <key KEY_VOLUMEUP>
   action = "amixer -q sset Master 1+"
   repeat = 5
 </key>

 <key KEY_VOLUMEDOWN>
   action = "amixer -q sset Master 1-"
   repeat = 5
 </key>

 <key KEY_POWER>
   action <<EOF
     #!/bin/bash
     i=10
     while ((i--))
     do
	 echo -e \\a >/dev/console
	 sleep 1
     done
   EOF
 </key>

=end

hotkeyd recognizes few global keywords:

=over

=item B<device> = <DEV>

Use I<DEV> as input event device file (default is F</dev/input/event0>).
Overridden by the B<-d> command line switch

=item B<pidfile> = <FILE>

Write pid in I<FILE> when running as daemon. I<FILE> is removed when
daemon terminates (default is F</var/run/hotkeyd.pid>). Overridden by
the B<-p> command line switch

=item B<header_path> = <PATH>

I<PATH> to the linux kernel headers (default is F</usr/include/linux>).

=back

other keywords are ignored.

the C<E<lt>key key_identifierE<gt> options E<lt>/keyE<gt>> blocks define
what action hotkeyd should take when a keypress of the key associated
with I<key_identifier> is received. Using C<hotkey --dump> you can
easily find out to which key identifier a certain key is associated.
The following keywords are recognized in these blocks:

=over

=item B<action> = I<ACTION>

The I<ACTION> hotkeyd should take when the given key is pressed. This
can be a simple command like C<shutdown -h -t 10 +5> or even a complex
script which will have to start with a shebang (C<#!/bin/interpreter>).
For such scripts it is practical to use here-documents like in the
example above.

=item B<repeat> = I<NUM>

For some keys key repeat events are generated when the key is pressed
for more than two second or so. This option says that only every
I<NUM>th key repeat event should be regarded as keypress. In the example
this option is used to slow down the speed at which the volume is
adjusted. When set to 0 (the default) all key repeat events are
discarded.

=head1 FILES

=over

=item F</etc/hotkeydrc>

The default configuration file. Overridden by the B<-c> command line
option.

=item F</dev/input/event0>

The default input event interface file. Overridden by the B<device>
option in the config file or the B<-d> command line option.

=item F</var/run/hotkeyd.pid>

hotkeyd stores it's pid in this file when started as daemon. The file is
removed when the daemon terminates. Overridden by the B<pidfile> option
in the config file or the B<-p> command line option.

=head1 CAVEATS

hotkeyd will refuse to start if the configuration file is writable by
other users than root. If you don't like this behavior, you have the
source. But see also L<SECURITY>

=head1 SECURITY

Because the kernel reports every single keypress over the input event
device one could easily eavesdrop using a program similar to hotkeyd to
spy out passwords or other sensible data. Therefore the permissions on
the input event device file should be set sensibly.

hotkeyd will refuse to start if its configuration file is not owned by
root or if its permissions exceed 755. Otherwise a user could configure
hotkeyd to report every letter pressed on the keyboard.

=head1 BUGS

None known yet. But if you find one, feel free to report it to
Christopher Zimmermann <madroach@zakweb.de>

=head1 RESTRICTIONS

Since the daemon runs as root and has no idea about xservers or logged
in users it cannot be used to start your browser, email client or sound
player. This would be the purpose of a program like hotkeys or keylaunch
which rely on a running X.

=head1 SEE ALSO

L<Config::General/CONFIG FILE FORMAT> about the syntax used in the
configuration file

=head1 AUTHOR

Christopher Zimmermann <madroach@zakweb.de>

=head1 COPYRIGHT AND LICENSE

Copyright E<169> 2006  Christopher Zimmermann <madroach@zakweb.de>

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

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

=cut
