#!/usr/bin/perl
#
# This script listen to ACPI events and acts accordingly.
# Written by fnux. I want to be able to mute my laptop even if it's locked !

### Disable some Perl::Critic rules:
## no critic (ValuesAndExpressions::ProhibitAccessOfPrivateData)

=encoding utf8
=head1 NAME

acpi-handler - Configurable handling of ACPI events.

=head1 VERSION

This documentation refers to acpi-handler 0.1.0.

=head1 SYNOPSIS

  acpi-handler [options]

  The available options are:
    --help: display usage
    --quiet: do not log incoming events to STDOUT
    --config=$PATH: use arbitrary configuration file

=head1 CONFIGURATION

The configuration file is formatted in TOML and is searched (unless C<--config>
is provided) in:

  $XDG_CONFIG_HOME/acpi-handler.toml
  /etc/acpi-handler.toml

Supported options:

  acpid_socket (String) path to ACPID's socket
  quiet (Boolean) do not log incoming events to STDOUT, overrided by the
    --quiet command-line argument

Actions are trigerred on events matching their name, with C<$_> behing replaced
by the matching raw input from ACPID. Actions definition could look like:

  [action]
  ac_adapter = "ac-toggle-handler-script $_"

  [action.button]
  mute = "pavolume toggle"
  volumeup = "pavolume up"
  volumedown = "pavolume down"

  [action.video]
  brightnessup = "brightnessctl set 10%+"
  brightnessdown = "brightnessctl set 10%-"

In the above configuration, C<pavolume up> will be called for
C<button/volumeup> ACPI events and C<ac-toggle-handler-script $_> will be expanded to
C<ac-toggle-handler-script ac_adapter ACPI0003:00 00000080 00000000> when my
AC adapter is unplugged.

You can use acpi_listen to determine the name of your ACPI events.

=head1 SEE ALSO

L<acpid(8)> L<acpid_listen(8)>

=head1 AUTHOR

Timothée Floure (<timothee.floure@posteo.net>)

=head1 LICENSE AND COPYRIGHT

acpi-handler - Configurable handling of ACPI events.
Copyright (C) 2019  Timothée Floure

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 3 of the License, or
(at your option) 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.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

=cut

use warnings;
use strict;
use feature "switch";

use Getopt::Long;
use IO::Socket::UNIX;
use Pod::Usage;
use TOML::Parser;
use Data::Dumper;

# Helper used to exit in case of error.
sub fail {
  my ($msg) = @_;
  if (defined $msg) { print $msg . "\n"; }
  exit 1;
}

sub get_configuration_file {
  # XDG support
  my $xdg_config_home = $ENV{'HOME'} . '/.config';
  if (defined $ENV{'XDG_CONFIG_HOME'}) {
    $xdg_config_home = $ENV{'XDG_CONFIG_HOME'};
  }

  # Configuration files, ordered by priority
  my @candidates = (
    $xdg_config_home . '/acpi-handler.toml',
    '/etc/acpi-handler.toml',
  );

  # Return the first file found
  for my $path (@candidates) {
    return $path if -f $path;
  }

  return;
}

# Parse configuration file, returning a Hash reference
sub parse_configuration {
  my ($path) = @_;

  my $parser = TOML::Parser->new();
  my $raw = $parser->parse_file($path);
  return $raw;
}

# Main loop
sub main {
  my ($actions, $acpid_sock, $quiet) = @_;

  # Open ACPI socket
  print "Opening ACPID socket $acpid_sock...";
  my $stream = IO::Socket::UNIX->new(
    Type => SOCK_STREAM(),
    Peer => $acpid_sock,
  );
  if (defined $stream) {
    print " OK\n";
  } else {
    fail ' FAILED';
  }

  # Wait for events, execute actions specified in configuration file
  my $line;
  while ($line= <$stream>) {
    my ($prefix, $event) = ($line =~ m/^(\w+)\/(\w+)/);

    print " - Event: $prefix/$event";

    # Extract matching action
    my $command;
    if (defined $prefix && defined $actions->{$prefix}) {
      $command = $actions->{$prefix}->{$event};
    } else {
      $command = $actions->{$event};
    }

    # Insert runtime arguments and execute action
    if (defined $command) {
      print " (handled)\n";
      $command =~ s/\$_/$line/;
      system $command . "&> /dev/null";
    } else {
      print " (ignored)\n";
    }
  }

  return;
}

# Default options
my $help = 0;
my $man = 0;
my $quiet = 0;
my $config_file = get_configuration_file();
my $acpid_sock = '/var/run/acpid.socket';

# Parse command-line arguments
GetOptions(
  'help'        => \$help,
  'man'         => \$man,
  'quiet'       => \$quiet,
  'config=s'    => \$config_file,
);

# Display help and exit if requested
pod2usage(1) if $help;
pod2usage(-exitval => 0, -verbose => 2) if $man;

# Parse configuration file
unless (defined $config_file && -f $config_file) {
  fail 'Could not find configuration file.';
}
print "Loading configuration from $config_file...";
my $config = parse_configuration($config_file);
print " OK\n";

# Override default options with values from configuration file
if (defined $config->{'acpid_socket'}) {
  $acpid_sock = $config->{'acpid_socket'};
}
$quiet = $quiet || $config->{'quiet'};
my $actions = $config->{'action'};

# Run main loop
main($actions, $acpid_sock, $quiet);
