/*
 * Daemon to interpret /dev/funkey.
 * Based on Rick Van Rein's funky.c from
 * http://rick.vanrein.org/linux/funkey/
 *
 * Written by Greg Ward <gward@python.net>
 *
 * Compile like this:
 *   gcc -Wall funkeyd.c -o funkeyd
 * 
 * This funkeyd reads a config file to determine what command to
 * execute when a particular keycode is read from /dev/funkey.
 * The default location of this config file is /etc/funkeyd.conf;
 * override with the -f option.
 *
 * The config file syntax is pretty simple: each line supplies a keycode
 * and a shell command.  Comments and blank lines are ignored; comments
 * can come anywhere in a line.  Backslash-joining of physical lines
 * is not supported.  As usual with the funkey driver, it's your
 * responsibility to setup a kernel keymap that maps "functional keys"
 * into keycodes that they will be output via /dev/funkey and seen
 * by this daemon.
 *
 * Sample config file (which of course depends on my local keymap):

# Pause key -- pause/restart music
0x05 xmms -t

# Ctrl-Alt-Power off -- play a burst of random noise
0x10 head -1024c /dev/urandom > /dev/sound/dsp

# Ctrl-Alt-Sleep -- put the system into "suspend" mode (only
# works if this daemon is run as root -- better idea is to
# write a setuid script for suspend!)
0x11 /usr/bin/apm --suspend
#0x11 /root/bin/suspend

 * By default, runs in the foreground with a moderate level of debugging
 * output.  To run as a daemon, use -d.  To run in the foreground with
 * no output (apart from errors), use -q.  Run with any bogus option
 * (eg. -h) for help.
 */

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define _GNU_SOURCE
#include <getopt.h>


char __revision__[] = "$Id: funkeyd.c,v 1.6 2002/11/06 21:42:19 greg Exp greg $";

/* The kernel's max keycode is really 127; using 255 lets us distinguish
 * keypress and keyrelease events (keyrelease == keypress | 0x80).
 */
#define MAX_KEYCODE 255

/* Override with -f option */
#define DEFAULT_CONFIG_FILE "/etc/funkeyd.conf"

/* Modify with -v/-q options */
static int VERBOSE = 1;

static void
log (int threshold, char * format, ...)
{
   va_list arglist;
   
   va_start(arglist, format);
   if (VERBOSE >= threshold) {
      printf("funkeyd: ");
      vprintf(format, arglist);
      putchar('\n');
   }
   va_end(arglist);
}


static char *
munge_line (char * line)
{
   char * c;

   c = line+strlen(line)-1;
   if (*c == '\n')
      *c = (char) 0;

   c = strchr(line, '#');
   if (c != NULL)
      *c = (char) 0;
   else
      c = line + strlen(line);

   c--;
   while (c > line && isspace(*c)) {
      *c = (char) 0;
      c--;
   }
   c++;
   if (c == line)                     /* blank line */
      return NULL;
   else
      return line;
}   


static void
config_warning (char * filename, int linenum, char * msgformat, ...)
{
   va_list arglist;
   va_start(arglist, msgformat);
   fprintf(stderr, "%s, line %d: warning: ", filename, linenum);
   vfprintf(stderr, msgformat, arglist);
   fputc('\n', stderr);
}

static int
parse_line (char * filename, int linenum, char * line, char ** commandlist)
{
   char * c;
   char * keycode_s;
   char * command;
   int keycode;

   c = line;
   while (*c && !isspace(*c))
      c++;
   if (*c == (char) 0) {
      config_warning(filename, linenum, "invalid syntax (no whitespace)");
      return 0;
   }

   *c = (char) 0;
   c++;
   while (*c && isspace(*c))
      c++;
   if (*c == (char) 0) {
      config_warning(filename, linenum, "invalid syntax (no command)");
      return 0;
   }

   keycode_s = line;
   command = c;

   keycode = (int) strtoul(keycode_s, NULL, 0);
   if (keycode < 0 || keycode > MAX_KEYCODE) {
      config_warning(filename, linenum, "invalid keycode %s", keycode_s);
      return 0;
   }
   log(2, "command for keycode 0x%02x: %s", keycode, command);
   commandlist[keycode] = strdup(command);
   return 1;
}


static char **
read_config (char * filename)
{
   char buf[1024];
   FILE * file;
   int i;
   char ** commandlist;
   int linenum;

   commandlist = (char **) malloc(sizeof(char *) * MAX_KEYCODE+1);
   for (i = 0; i < sizeof(commandlist); i++)
      commandlist[i] = NULL;

   file = fopen(filename, "r");
   if (!file) {
      perror(filename);
      exit(1);
   }

   linenum = 0;
   while (!feof(file)) {
      char * line;

      if (!fgets(buf, 1024, file)) {
         break;
      }
      linenum++;

      /* Ignore comments and blank lines. */
      line = munge_line(buf);
      if (line == NULL)                 /* blank line */
         continue;

      /* Split line on first whitespace, into keycode and command,
         and store command in commandlist. */
      parse_line(filename, linenum, line, commandlist);

   }
   fclose(file);
   return commandlist;
}


static void
dump_config (char ** commandlist)
{
   int i;

   for (i = 0; i < MAX_KEYCODE+1; i++) {
      if (commandlist[i])
         printf("keycode 0x%02x: %s\n", i, commandlist[i]);
   }
}


static void
handle_key (unsigned char ch, char ** commandlist) {
   char * command;

   if (ch > MAX_KEYCODE)
      return;
   command = commandlist[ch];
   if (command) {
      log(1, "got keycode 0x%02x; executing %s", ch, command);
      system(command);
   }
   else {
      log(2, "got keycode 0x%02x; nothing to do", ch);
   }
}


static void
daemonize (void)
{
   pid_t pid;

   pid = fork();
   if (pid) {
      /* parent -- just exit */
      printf("parent process exiting\n");
      exit(0);
   }
   else {
      /* child -- clean up and continue */
      printf("child process detaching\n");
      setsid();
      chdir("/");
      close(0); open("/dev/null", O_RDONLY);
      close(1); open("/dev/null", O_WRONLY);
      close(2); open("/dev/null", O_WRONLY);
   }
}


static char * USAGE =
   "usage: funkeyd [options]\n"
   "\n"
   "options:\n"
   "  -d, --daemon         detach and run as a daemon process\n"
   "  -v                   increase verbosity by 1\n"
   "  -q, --quiet          run with no output\n"
   "  -fFILE, --file=FILE  read key-to-command mapping from FILE\n"
   "                       (default: " DEFAULT_CONFIG_FILE ")\n"
   "\n"
   "-d implies -q; non-zero verbosity disables -d\n";



int main (int argc, char *argv[]) {
   int daemon = 0;
   char * config_file = DEFAULT_CONFIG_FILE;

   char * optstring = "dvqf:";
   struct option longopts[] = {
      { "daemon", no_argument, NULL, 'd' },
      { "quiet", no_argument, NULL, 'q' },
      { "file", required_argument, NULL, 'f' },
      { NULL, 0, NULL, 0 }
   };
   int opt;
   int longindex = 0;

   char ** commandlist;
   int funkey;
   unsigned char buf;

   /* Parse options. */
   opt = getopt_long(argc, argv, optstring, longopts, &longindex);
   while (opt != -1) {
      if (opt == 'd') {
         daemon = 1;
         VERBOSE = 0;
      }
      else if (opt == 'v') {
         VERBOSE++;
         daemon = 0;
      }
      else if (opt == 'q') {
         VERBOSE = 0;
      }
      else if (opt == 'f') {
         config_file = strdup(optarg);
      }
      else if (opt == '?') {
         fprintf(stderr, USAGE);
         exit(1);
      }
      opt = getopt_long(argc, argv, optstring, longopts, &longindex);
   }

   commandlist = read_config(config_file);

   if (VERBOSE >= 2) {
      printf("commands read from %s:\n", config_file);
      dump_config(commandlist);
   }

   funkey = open("/dev/funkey", O_RDONLY);
   if (funkey < 0) {
      perror("Cannot open /dev/funkey");
      exit(1);
   }

   if (daemon)
      daemonize();

   while (read(funkey, &buf, 1)) {
      handle_key(buf, commandlist);
   }
   close(funkey);

   return 0;
}
