summaryrefslogtreecommitdiff
path: root/mprompt
diff options
context:
space:
mode:
Diffstat (limited to 'mprompt')
-rwxr-xr-xmprompt370
1 files changed, 370 insertions, 0 deletions
diff --git a/mprompt b/mprompt
new file mode 100755
index 0000000..c58cc75
--- /dev/null
+++ b/mprompt
@@ -0,0 +1,370 @@
+#!/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";
+ }
+}