#!/usr/bin/env perl
# gnome-inhibit -- temporarily suspend "session idle" check in GNOME
use v5.10;
use warnings;
use strict;
use locale;
use Getopt::Long qw(:config no_ignore_case bundling require_order);
use List::Util qw(max);
use Net::DBus;
use POSIX;

my %FLAGS = (
# Flags tracked by GNOME session manager (gnome-session/gsm-inhibitor-flag.h)
	"logout"	=> 1 << 0,
	"switch-user"	=> 1 << 1,
	"suspend"	=> 1 << 2,
	"idle"		=> 1 << 3,
	"automount"	=> 1 << 4,
);

BEGIN {
	if (eval {require Nullroute::Lib}) {
		Nullroute::Lib->import(qw(_debug _info _warn _err _die));
	} else {
		our ($warnings, $errors);
		$::arg0 = (split m!/!, $0)[-1];
		$::debug = !!$ENV{DEBUG};
		sub _debug { warn "debug: @_\n" if $::debug; }
		sub _info  { say "@_"; }
		sub _warn  { warn "warning: @_\n"; ++$::warnings; }
		sub _err   { warn "error: @_\n"; ! ++$::errors; }
		sub _die   { _err(@_); exit 1; }
	}
}

my $bus;

sub SessionManager {
	$bus
	->get_service("org.gnome.SessionManager")
	->get_object(shift // "/org/gnome/SessionManager")
}

sub maxlength {
	my ($attr, @items) = @_;
	(max map {length $_->{$attr}} @items) // 0;
}

sub print_table_row {
	my (@items) = @_;
	my $columns = @items;
	if ($columns % 2) {
		warn "Odd number of items";
		pop @items;
		--$columns;
	}
	my $fmt = "%-*s  " x ($columns/2);
	$fmt =~ s/\s+$/\n/;
	printf $fmt, @items;
}

sub flags_to_array {
	my ($bits) = @_;
	sort grep {$bits & $FLAGS{$_}} keys %FLAGS;
}

sub flags_to_string {
	my ($bits) = @_;
	$bits ? join(",", flags_to_array($bits)) : "none";
}

sub string_to_flags {
	my ($flag_str) = @_;
	my $flags = 0;
	for (split /[:,|]/, $flag_str) {
		if ($_ eq 'all') {
			$flags |= 0x7fffffff;
		} elsif ($_ eq 'sleep') {
			$flags |= $FLAGS{suspend};
		} elsif (defined $FLAGS{$_}) {
			$flags |= $FLAGS{$_};
		} elsif (/^([0-9]+)$/) {
			$flags |= int $_;
		} elsif (/^(0x[0-9a-fA-F]+)$/) {
			$flags |= hex $_;
		} else {
			_warn("unknown inhibit flag: '$_'");
		}
	}
	if ($flags >> 31) {
		_die("inhibit bitmask too large (must be an uint32)");
	}
	return $flags;
}

sub get_inhibitors {
	my ($flags) = @_;

	sort {$a->{app_id} cmp $b->{app_id}}
	map {
		my $ih = $_;
		my $flags = $ih->GetFlags;
		{
			path      => $ih->get_object_path,
			app_id    => $ih->GetAppId,
			client_id => eval {$ih->GetClientId} // "(none)",
			reason    => eval {$ih->GetReason} // "(none)",
			flags     => $flags,
			szflags   => flags_to_string($flags),
			top_xid   => $ih->GetToplevelXid,
		};
	}
	grep {!$flags or $_->GetFlags & $flags}
	map {SessionManager($_)}
	@{SessionManager->GetInhibitors};
}

sub display_table {
	my ($fields, $header, $items) = @_;

	my %len = map {$_ => max(
	                         length($header->{$_}),
				 maxlength($_, @$items)
	                     )} @$fields;

	print_table_row(map {$len{$_}, $header->{$_}} @$fields);

	for my $item (@$items) {
		print_table_row(map {$len{$_}, $item->{$_}} @$fields);
	}
}

sub display_inhibitors {
	my ($verbose, @inhibitors) = @_;

	my %header = (
		app_id    => "APPLICATION",
		client_id => "CLIENT",
		reason    => "REASON",
		flags     => "FLAGS",
		szflags   => "INHIBITS",
		top_xid   => "WINDOW",
	);

	my @fields = $verbose
		? qw(app_id reason flags szflags client_id top_xid)
		: qw(app_id reason szflags);

	display_table(\@fields, \%header, \@inhibitors);
}

sub usage {
	say for
	"Usage: $::arg0 [options] [<command> [args...]]",
	"       $::arg0 [options] {--list|--test} [flags...]",
	"",
	"  --who <application>           Application name (alias for --app-id)",
	"  --what <flags>                Inhibitor flags  (alias for --flags)",
	"  --why <reason>                Inhibit reason   (alias for --reason)",
	"",
	"  -a, --app-id <application>    Application name to identify ourselves as",
	"  -f, --flags <flags>           Inhibitor flags (default: \"idle\")",
	"  -r, --reason <reason>         Inhibit reason",
	"",
	"  -A, --always                  Run command even if gnome-session not running",
	"      --help                    This message",
	"  -l, --list                    List active inhibitors",
	"  -t, --test                    Test if any inhibitors are present",
	"  -v, --verbose                 More output for --list or --test",
	"",
	"Flags can be specified as a comma- or colon-separated list, or the word \'all\'.",
	"Supported flags are: {".join(" ", sort keys %FLAGS)."}",
	"",
	"If --list is given, --flags will be used as a list filter (default: show all).",
	"",
	"If --test is given, the tool will exit with code 1 if any of the given",
	"operations from --flags are inhibited.",
	"",
	"If both --list and --test are given, all inhibited actions will be listed in",
	"terse form, optionally with --flags used as a list filter.",
	"",
	"If <command> is given, inhibit will last until the command exits; otherwise,",
	"this tool calls pause() and keeps running until manually killed/terminated.";
}

# Option parser

my $do_list	= 0;
my $do_test	= 0;
my $always	= 0;
my $verbose	= 0;
my $app_id	= $::arg0;
my $reason	= "User-initiated inhibit";
my $flag_str	= undef;
my $top_xid	= 0;

GetOptions(
	'help'		=> sub { usage(); exit; },
	'A|always'	=> \$always,
	'a|app-id=s'	=> \$app_id,
	'f|flags=s'	=> \$flag_str,
	'l|list+'	=> \$do_list,
	'r|reason=s'	=> \$reason,
	't|test+'	=> \$do_test,
	'v|verbose'	=> \$verbose,
	'x|xid=o'	=> \$top_xid,
	# for compatibility with `systemd-inhibit`
	'what=s'	=> \$flag_str,
	'who=s'		=> \$app_id,
	'why=s'		=> \$reason,
	# for compatibility with `gnome-session-inhibit`
	'inhibit=s'	=> \$flag_str,
	'inhibit-only'	=> sub { },
) or exit 2;

if (@ARGV && ($do_list || $do_test)) {
	if (defined $flag_str) {
		_warn("extra arguments ignored");
	} else {
		$flag_str //= join(",", @ARGV);
	}
}

# Main code

if ($::arg0 ne $app_id) {
	_debug("using app ID \"$app_id\"");
}

if ($always) {
	eval {$bus = Net::DBus->session};
	if ($bus) {
		$bus = undef unless eval {SessionManager};
	}
} else {
	$bus = Net::DBus->session;
	# will die if no bus
}

if ($do_list && $do_test) {
	if ($do_list > 1 || $do_test > 1) {
		_err("specifying both -l and -t multiple times doesn't make sense");
		exit 2;
	}

	if (!$bus) {
		_warn("gnome-session is not running");
		exit 0;
	}

	my $filter = string_to_flags($flag_str //= "all");
	my $flags = SessionManager->Get("org.gnome.Session",
					"InhibitedActions") & $filter;
	say for $flags ? flags_to_array($flags) : "none";

	exit 0;
} elsif ($do_list) {
	$verbose ||= ($do_list > 1);

	if (!$bus) {
		_warn("gnome-session is not running");
		exit 0;
	}

	my $flags = string_to_flags($flag_str //= "0");
	display_inhibitors($verbose, get_inhibitors($flags));

	exit 0;
} elsif ($do_test) {
	$verbose ||= ($do_test > 1);

	if (!$bus) {
		say "no" if $verbose;
		exit 0;
	}

	my $flags = string_to_flags($flag_str //= "idle");
	if (!$flags) {
		_err("no inhibit flags given");
		exit 2;
	}

	if (SessionManager->IsInhibited($flags)) {
		say "yes" if $verbose;
		exit 1;
	} else {
		say "no" if $verbose;
		exit 0;
	}
} else {
	my $cookie;

	my $flags = string_to_flags($flag_str //= "idle");
	if (!$flags) {
		_err("no inhibit flags given");
		exit 2;
	}

	if ($bus) {
		$cookie = SessionManager->Inhibit($app_id, $top_xid, $reason, $flags);
		if ($cookie) {
			_debug("inhibited: ".flags_to_string($flags)." ($cookie)");
		} else {
			_err("inhibit failed");
			exit 1;
		}
	} else {
		_debug("gnome-session not running; inhibit not added");
	}

	my @cmd = @ARGV;
	my $return = 0;

	if (@cmd) {
		_debug("executing: @cmd");
		$return = system {$cmd[0]} @cmd;
		$return = POSIX::WEXITSTATUS($return);
		_debug("command returned status $return");
		exit $return;
	} else {
		_info("pausing until signal; use Ctrl-C to interrupt");
		POSIX::pause();
	}
}
