#!/usr/bin/perl
use strict;
use warnings;
use Audio::MPD q{0.19.0};
use MpdToys;
use Getopt::Long;
use Term::ReadKey;
use Encode;

=head1 NAME

mprompt - simple prompt-based control for mpd

=head1 SYNOPSIS

mpompt [-s] [-m key=key] [-t n] [-f] [tty] [-T] [host]

=cut

sub usage {
	die "Usage: mprompt [-s] [-m key=key] [-t n] [-f] [-t] [tty] [host]\n";
}

=head1 DESCRIPTION

B<mprompt> is a mpd client with a prompt-based interface. It is 
designed to be usable on a headless machine.

At the prompt, enter the name of a playlist, or part of the name of an
album, artist, or song. Matching items will start playing. You can also
paste in urls to stream.

(If the perl String::Approx module is available, it will be used to handle
typos, etc in the names you enter.)

Use the left and right arrow keys to adjust volume, and the up and down
arrow keys to move through the playlist. 

The Tab and Enter keys can both be used to pause and unpause playback.
(Enter toggles pause only if nothing has been entered at the prompt.)

Example of how to run mprompt in /etc/inittab:

	1:2345:respawn:/usr/bin/mprompt /dev/tty1

=head1 OPTIONS

=over 4

=item -s

This option allows shell commands to be typed in to mprompt, to be
run by whatever user it is running as. (Typically root if it is run from
/etc/inittab).

To enter a shell command, type a "!", followed by the command to run,
followed by Enter.

=item -m key=key

This option allows remapping keys. Any key can be remapped to any other
key, which is useful to support keyboard with unusual key layouts, or
missing keys.

For alphanumeric and punctuation keys, individual symbols can be remapped.
For example, "-m a=b" will turn each entered "a" into "b".

For other keys, use the following names:

=over 4

=item <return>

=item <tab>

=item <space>

=item <up>

=item <down>

=item <left>

=item <right>

=item <backspace>

=back

For example, -m "n=<down>" will map the "n" key to the down arrow, causing
that key to change to the next track; -m "<space>=<return>" will make the space
bar act as a pause.

It's possible to swap keys too. For example, -m "<down>=<up>" -m "<up>=<down>"

A single key can also be bound to a series of keystrokes. For example,
-m "1=Mule Variations<return>" will cause the "1" key to play the "Mule
Variations" album, a nice choice.

=item -t n

Adds a timeout, a specified number of seconds after which the entry
on the command line will be cleared. Useful for headless systems, to avoid
cat-on-keyboard confusing your later commands.

=item -T

Enables terse output mode. This mode tries to avoid displaying excessive
or complex things, with the intent that mprompt's output can be piped into
a speech synthesiser, such as espeak.

=back

=head1 SEE ALSO

vipl(1) mptoggle(1) mpd(1)

=head1 AUTHOR

Copyright 2009 Joey Hess <joey@kitenet.net>

Licensed under the GNU GPL version 2 or higher.

http://kitenet.net/~joey/code/mpdtoys

=cut

my $tty;
my $shell=0;
my $timeout=0;
my $terse=0;
my %controlchars = GetControlChars;
my %keysyms = (
	"\n" => '<return>',
	"\t" => '<tab>',
	" " =>  '<space>',
	"\e[A" => '<up>',
	"\e[3~" => '<up>', # delete on some terminals, raw on others
	"\e[B" => '<down>',
	"\e[D" => '<left>',
	"\e[C" => '<right>',
	$controlchars{ERASE} => "<backspace>",
);
my %keymap;

Getopt::Long::Configure("no_ignore_case");
GetOptions(
	"s" => \$shell,
	"m=s" => sub {
		my ($old, $new)=split(/=/, $_[1], 2);
		$keymap{$old}=$new;
	},
	"t=i" => \$timeout,
	"f" => sub { print STDERR "the -f option is now enabled by default\n" },
	"T" => \$terse,
) || usage();

if (@ARGV) {
	$tty=shift;
	close STDIN;
	close STDOUT;
	open(STDIN, "<", $tty) || die "open $tty: $!";
	open(STDOUT, ">", $tty) || die "open $tty: $!";
}
if (@ARGV) {
	$ENV{MPD_HOST}=shift;
}
my $mpd=Audio::MPD->new(conntype => "reuse");

sub quit {
	ReadMode("restore");
	exit(0);
};
$SIG{INT}=$SIG{TERM}=\&quit;
ReadMode("raw");

$|=1;

my $line="";
my $sequence;
my $laststroke=time;

showprompt();

KEY: while (my $key = ReadKey(0)) {
	if ($timeout) {
	       if (length $line && time - $laststroke > $timeout) {
			$line="";
			print "  <timeout>\n" unless $terse;
			showprompt();
		}
		$laststroke=time;
	}

	if ($key eq $controlchars{INTERRUPT} ||
	    $key eq $controlchars{EOF}) {
		quit();
	}

	# Sequences are started with escape, and accumulated
	# until a recognised sequence is seen, or until it becomes clear
	# that it is not part of a recognised sequence.
	if (defined $sequence) {
		$sequence.=$key;
		if (exists $keysyms{$sequence}) {
			$key=$sequence;
			$sequence=undef;
		}
		else {
			foreach my $sym (keys %keysyms) {
				if ($sym=~/^\Q$sequence\E/) {
					next KEY; # unfinished sequence
				}
			}
			$key=$sequence;
			$sequence=undef;
		}
	}

	$key = $keysyms{$key} if exists $keysyms{$key};
	$key = $keymap{$key} if exists $keymap{$key};
	
	# The key may be mapped to a multiple letter sequence.
	while (length $key) {
		if ($key=~s/^(<[^>]+>)//) {
			handle($1);
		}
		elsif ($key=~s/(.)//) {
			handle($1);
		}
	}
}

sub handle {
	my $key=shift;

	if ($key eq "\e") {
		$sequence=$key;
	}
	elsif ($key eq '<return>') {
		if ($shell && $line =~ /^\!(.*)/) {
			print "\nrunning $1\n";
			system($1);
		}
		elsif (length $line && $line !~ /^\s*$/) {
			queue($line);
		}
		else {
			toggle();
		}
		$line="";
		showprompt();
	}
	elsif ($key eq '<backspace>') {
		if (length $line) {
			chop $line;
			print "\b \b";
		}
	}
	elsif ($key eq '<space>') {
		print " ";
		$line.=" ";
	}
	elsif ($key eq '<tab>') {
		toggle();
		showprompt();
	}
	elsif ($key eq '<left>') {
		adjustvolume(-5);
	}
	elsif ($key eq '<right>') {
		adjustvolume(+5);
	}
	elsif ($key eq '<up>') {
		$mpd->prev;
		$mpd->play;
		showplaying();
		showprompt();
	}
	elsif ($key eq '<down>') {
		$mpd->next;
		$mpd->play;
		showplaying();
		showprompt();
	}
	else {
		print "$key";
		$line.=$key;
	}
}
		
sub adjustvolume {
	my $amount=shift;
	my $vol=$mpd->status->volume;

	$vol+=$amount;
	if ($vol > 100) {
		$vol=100;
	}
	elsif ($vol < 0) {
		$vol=0;
	}

	if (! $terse) {
		print "\nvolume: $vol%\n";
	}
	$mpd->volume($vol);
	showprompt();
}

sub showprompt {
	print "> $line";
}

sub showplaying {
	print "\n";
	my $song=$mpd->current;
	if (! defined $song) {
		print "nothing queued\n";
	}
	else {
		if (! $terse) {
			print encode_utf8($song->as_string)."\n";
		}
	}
}

sub toggle {
	$mpd->pause;
	my $state=$mpd->status->state;
	print "\n";
	print "$state\n" if ! $terse || $state ne "play";
}

sub queue {
	my $line=shift;

	print "\n";

	my $pl=$mpd->playlist;

	eval q{$pl->load($line)};
	if (! $@) {
		$pl->clear;
		$pl->load($line);
		$mpd->play;

		print "added $line playlist";
		showplaying();
		return;
	}

	my @matches=MpdToys::findmatchingsongs($line, $mpd);
	if (! @matches && MpdToys::canmatch_fuzzy()) {
		print "trying fuzzy match..\n";
		@matches=MpdToys::findmatchingsongs_fuzzy($line, $mpd);
	}
	if (@matches) {
		$pl->clear;
		foreach (@matches) {
			$pl->add($_->file);
		}
		$mpd->play;
		print "added ".int(@matches)." songs";
		showplaying();
	}
	else {
		print "no matches found for \"$line\"\n";
	}
}
