441 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			441 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env perl
 | |
| #
 | |
| # Copyright (c) 2022 Omar Polo <op@omarpolo.com>
 | |
| #
 | |
| # Permission to use, copy, modify, and distribute this software for any
 | |
| # purpose with or without fee is hereby granted, provided that the above
 | |
| # copyright notice and this permission notice appear in all copies.
 | |
| #
 | |
| # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | |
| # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | |
| # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | |
| # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | |
| # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 | |
| # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | |
| # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | |
| 
 | |
| use strict;
 | |
| use warnings;
 | |
| use v5.12;
 | |
| 
 | |
| use open ":std", ":encoding(UTF-8)";
 | |
| use utf8;
 | |
| 
 | |
| use Curses;
 | |
| use POSIX qw(:sys_wait_h setlocale LC_ALL);
 | |
| use Text::CharWidth qw(mbswidth);
 | |
| use IO::Poll qw(POLLIN);
 | |
| use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
 | |
| use Getopt::Long qw(:config bundling);
 | |
| use Pod::Usage;
 | |
| 
 | |
| my $run = 1;
 | |
| 
 | |
| my $pfile;
 | |
| my $trim = "";
 | |
| 
 | |
| my $pair_n = 1;
 | |
| 
 | |
| my @songs;
 | |
| my $current_song;
 | |
| my $playlist_cur;
 | |
| my $playlist_max;
 | |
| my $time_cur;
 | |
| my $time_dur;
 | |
| my $status;
 | |
| my $mode;
 | |
| 
 | |
| my $last_lines;
 | |
| 
 | |
| sub round {
 | |
| 	return int(0.5 + shift);
 | |
| }
 | |
| 
 | |
| sub max {
 | |
| 	my ($a, $b) = @_;
 | |
| 	return $a > $b ? $a : $b;
 | |
| }
 | |
| 
 | |
| sub excerpt {
 | |
| 	my $lines = shift;
 | |
| 	my @tmp;
 | |
| 	my ($n, $idx, $cur) = (0, 0, -1);
 | |
| 
 | |
| 	open (my $fh, "-|", "amused", "show", "-p");
 | |
| 	while (<$fh>) {
 | |
| 		chomp;
 | |
| 		s,$trim,,;
 | |
| 		$tmp[$idx] = $_;
 | |
| 
 | |
| 		if (m/^>/) {
 | |
| 			$cur = $n;
 | |
| 			$current_song = s/^> //r;
 | |
| 		}
 | |
| 
 | |
| 		$n++;
 | |
| 		$idx = ++$idx % $lines;
 | |
| 
 | |
| 		last if $cur != -1 && $n - $cur > int($lines/2) &&
 | |
| 		    $#tmp == $lines-1;
 | |
| 	}
 | |
| 	close($fh);
 | |
| 
 | |
| 	return ("Empty playlist.") unless @tmp;
 | |
| 
 | |
| 	# reorder the entries
 | |
| 	my @r;
 | |
| 	my $len = $#tmp + 1;
 | |
| 	$idx = $idx % $len;
 | |
| 	for (1..$len) {
 | |
| 		push @r, $tmp[$idx];
 | |
| 		$idx = ++$idx % $len;
 | |
| 	}
 | |
| 	return @r;
 | |
| }
 | |
| 
 | |
| sub playlist_numbers {
 | |
| 	my ($cur, $tot, $found) = (0, 0, 0);
 | |
| 	open (my $fh, "-|", "amused", "show", "-p");
 | |
| 	while (<$fh>) {
 | |
| 		$tot++;
 | |
| 		$cur++ unless $found;
 | |
| 		$found = 1 if m/^>/;
 | |
| 	}
 | |
| 	close($fh);
 | |
| 	return ($cur, $tot);
 | |
| }
 | |
| 
 | |
| sub status {
 | |
| 	my ($pos, $dur, $mode);
 | |
| 
 | |
| 	open (my $fh, "-|", "amused", "status", "-f",
 | |
| 	      "status,time:raw,mode:oneline");
 | |
| 
 | |
| 	<$fh> =~ m/([a-z]+) (.*)/;
 | |
| 	my ($status, $current_song) = ($1, $2);
 | |
| 
 | |
| 	while (<$fh>) {
 | |
| 		chomp;
 | |
| 		$pos = s/position //r if m/^position /;
 | |
| 		$dur = s/duration //r if m/^duration /;
 | |
| 		$mode = $_ if m/^repeat/;
 | |
| 	}
 | |
| 	close($fh);
 | |
| 	return ($status, $current_song, $pos, $dur, $mode);
 | |
| }
 | |
| 
 | |
| sub showtime {
 | |
| 	my $seconds = shift;
 | |
| 	my $str = "";
 | |
| 
 | |
| 	if ($seconds > 3600) {
 | |
| 		my $hours = int($seconds / 3600);
 | |
| 		$seconds -= $hours * 3600;
 | |
| 		$str = sprintf("%02d:", $hours);
 | |
| 	}
 | |
| 
 | |
| 	my $minutes = int($seconds / 60);
 | |
| 	$seconds -= $minutes * 60;
 | |
| 	$str .= sprintf "%02d:%02d", $minutes, $seconds;
 | |
| 	return $str;
 | |
| }
 | |
| 
 | |
| sub center {
 | |
| 	my ($str, $pstr) = @_;
 | |
| 	my $width = mbswidth($str);
 | |
| 	return $str if $width > $COLS;
 | |
| 	my $pre = round(($COLS - $width) / 2);
 | |
| 	my $lpad = $pstr x $pre;
 | |
| 	my $rpad = $pstr x ($COLS - $width - $pre);
 | |
| 	return ($lpad, $str, $rpad);
 | |
| }
 | |
| 
 | |
| sub offsets {
 | |
| 	my ($y, $x, $cur, $max) = @_;
 | |
| 	my ($pre, $c, $post) = center(" $cur / $max ", '-');
 | |
| 	addstring $y, $x, "";
 | |
| 
 | |
| 	my $p = COLOR_PAIR($pair_n);
 | |
| 
 | |
| 	attron $p;
 | |
| 	addstring $pre;
 | |
| 	attroff $p;
 | |
| 
 | |
| 	addstring $c;
 | |
| 
 | |
| 	attron $p;
 | |
| 	addstring $post;
 | |
| 	attroff $p;
 | |
| }
 | |
| 
 | |
| sub progress {
 | |
| 	my ($y, $x, $pos, $dur) = @_;
 | |
| 
 | |
| 	my $pstr = showtime $pos;
 | |
| 	my $dstr = showtime $dur;
 | |
| 
 | |
| 	my $len = $COLS - length($pstr) - length($dstr) - 4;
 | |
| 	return if $len <= 0 or $dur <= 0;
 | |
| 	my $filled = round($pos * $len / $dur);
 | |
| 
 | |
| 	addstring $y, $x, "$pstr [";
 | |
| 	addstring "#" x $filled;
 | |
| 	addstring " " x max($len - $filled, 0);
 | |
| 	addstring "] $dstr";
 | |
| }
 | |
| 
 | |
| sub show_status {
 | |
| 	my ($y, $x, $status) = @_;
 | |
| 	my ($pre, $c, $post) = center($status, ' ');
 | |
| 	addstring $y, $x, $pre;
 | |
| 	addstring $c;
 | |
| 	addstring $post;
 | |
| }
 | |
| 
 | |
| sub show_mode {
 | |
| 	my ($y, $x, $mode) = @_;
 | |
| 	my ($pre, $c, $post) = center($mode, ' ');
 | |
| 	addstring $y, $x, $pre;
 | |
| 	addstring $c;
 | |
| 	addstring $post;
 | |
| }
 | |
| 
 | |
| sub render {
 | |
| 	erase;
 | |
| 	if ($LINES < 4 || $COLS < 20) {
 | |
| 		addstring "window too small";
 | |
| 		refresh;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	my $song_pad = "";
 | |
| 	my $longest = 0;
 | |
| 	$longest = max $longest, length($_) foreach @songs;
 | |
| 	if ($longest < $COLS) {
 | |
| 		$song_pad = " " x (($COLS - $longest)/2);
 | |
| 	}
 | |
| 
 | |
| 	my $line = 0;
 | |
| 	map {
 | |
| 		attron(A_BOLD) if m/^>/;
 | |
| 		addstring $line++, 0, $song_pad . $_;
 | |
| 		standend;
 | |
| 	} @songs;
 | |
| 
 | |
| 	offsets $LINES - 4, 0, $playlist_cur, $playlist_max;
 | |
| 	progress $LINES - 3, 0, $time_cur, $time_dur;
 | |
| 	show_status $LINES - 2, 0, "$status $current_song";
 | |
| 	show_mode $LINES - 1, 0, $mode;
 | |
| 
 | |
| 	refresh;
 | |
| }
 | |
| 
 | |
| sub getsongs {
 | |
| 	$last_lines = $LINES;
 | |
| 	@songs = excerpt $LINES - 4;
 | |
| }
 | |
| 
 | |
| sub getnums {
 | |
| 	($playlist_cur, $playlist_max) = playlist_numbers;
 | |
| }
 | |
| 
 | |
| sub save {
 | |
| 	return unless defined $pfile;
 | |
| 
 | |
| 	open(my $fh, ">", $pfile);
 | |
| 	open(my $ph, "-|", "amused", "show", "-p");
 | |
| 
 | |
| 	print $fh $_ while (<$ph>);
 | |
| }
 | |
| 
 | |
| sub hevent {
 | |
| 	my $fh = shift;
 | |
| 	my $l = <$fh>;
 | |
| 	die "monitor quit" unless defined($l);
 | |
| 
 | |
| 	$status = "playing" if $l =~ m/^play/;
 | |
| 	$status = "paused"  if $l =~ m/^pause/;
 | |
| 	$status = "stopped" if $l =~ m/^stop/;
 | |
| 
 | |
| 	($time_cur, $time_dur) = ($1, $2) if $l =~ m/^seek (\d+) (\d+)/;
 | |
| 
 | |
| 	$mode = $1 if $l =~ m/^mode (.*)/;
 | |
| 
 | |
| 	getnums if $l =~ m/load|jump|next|prev/;
 | |
| 	getsongs if $l =~ m/load|jump|next|prev/;
 | |
| }
 | |
| 
 | |
| sub hinput {
 | |
| 	my ($ch, $key) = getchar;
 | |
| 	if (defined $key) {
 | |
| 		if ($key == KEY_BACKSPACE) {
 | |
| 			system "amused", "seek", "0";
 | |
| 		}
 | |
| 	} elsif (defined $ch) {
 | |
| 		if ($ch eq " ") {
 | |
| 			system "amused", "toggle";
 | |
| 		} elsif ($ch eq "<" or $ch eq "p") {
 | |
| 			system "amused", "prev";
 | |
| 		} elsif ($ch eq ">" or $ch eq "n") {
 | |
| 			system "amused", "next";
 | |
| 		} elsif ($ch eq ",") {
 | |
| 			system "amused", "seek", "-5";
 | |
| 		} elsif ($ch eq ".") {
 | |
| 			system "amused", "seek", "+5";
 | |
| 		} elsif ($ch eq "S") {
 | |
| 			system "amused show | sort -u | amused load";
 | |
| 		} elsif ($ch eq "R") {
 | |
| 			system "amused show | sort -R | amused load";
 | |
| 		} elsif ($ch eq "s") {
 | |
| 			save;
 | |
| 		} elsif ($ch eq "q") {
 | |
| 			$run = 0;
 | |
| 		} elsif ($ch eq "\cH") {
 | |
| 			system "amused", "seek", "0"
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| GetOptions(
 | |
| 	"p:s" => \$pfile,
 | |
| 	"t:s" => \$trim,
 | |
|     ) or pod2usage(1);
 | |
| 
 | |
| my $mpid = open(my $monitor, "-|", "amused", "monitor")
 | |
|     or die "can't spawn amused monitor";
 | |
| 
 | |
| setlocale(LC_ALL, "");
 | |
| initscr;
 | |
| start_color;
 | |
| use_default_colors;
 | |
| init_pair $pair_n, 250, -1;
 | |
| 
 | |
| timeout 1000;
 | |
| scrollok 0;
 | |
| curs_set 0;
 | |
| keypad 1;
 | |
| 
 | |
| my $poll = IO::Poll->new();
 | |
| $poll->mask(\*STDIN => POLLIN);
 | |
| $poll->mask($monitor => POLLIN);
 | |
| 
 | |
| if (`uname` =~ "OpenBSD") {
 | |
| 	use OpenBSD::Pledge;
 | |
| 	use OpenBSD::Unveil;
 | |
| 
 | |
| 	my $prog = `which amused`;
 | |
| 	chomp $prog;
 | |
| 
 | |
| 	unveil($prog, 'rx') or die "unveil $prog: $!";
 | |
| 	if (defined($pfile)) {
 | |
| 		unveil($pfile, 'wc') or die "unveil $pfile: $!";
 | |
| 		pledge qw(stdio wpath cpath tty proc exec) or die "pledge: $!";
 | |
| 	} else {
 | |
| 		pledge qw(stdio tty proc exec) or die "pledge: $!";
 | |
| 	}
 | |
| }
 | |
| 
 | |
| getsongs;
 | |
| getnums;
 | |
| ($status, $current_song, $time_cur, $time_dur, $mode) = status;
 | |
| render;
 | |
| 
 | |
| while ($run) {
 | |
| 	$poll->poll();
 | |
| 	hinput if $poll->events(\*STDIN) & POLLIN;
 | |
| 	hevent $monitor if $poll->events($monitor) & POLLIN;
 | |
| 
 | |
| 	getsongs if $LINES != $last_lines;
 | |
| 
 | |
| 	render;
 | |
| }
 | |
| 
 | |
| endwin;
 | |
| save;
 | |
| 
 | |
| kill 'INT', $mpid;
 | |
| wait;
 | |
| 
 | |
| __END__
 | |
| 
 | |
| =pod
 | |
| 
 | |
| =head1 NAME
 | |
| 
 | |
| amused-monitor - curses interface for amused(1)
 | |
| 
 | |
| =head1 SYNOPSIS
 | |
| 
 | |
| B<amused-monitor> [B<-p> I<playlist>] [B<-t> I<string>]
 | |
| 
 | |
| =head1 DESCRIPTION
 | |
| 
 | |
| amused-monitor is a simple curses interface for amused(1).
 | |
| 
 | |
| The following options are available:
 | |
| 
 | |
| =over 12
 | |
| 
 | |
| =item B<-p> I<playlist>
 | |
| 
 | |
| Save the current playling queue to the file I<playlist> upon exit or
 | |
| I<s> key.
 | |
| 
 | |
| =item B<-t> I<string>
 | |
| 
 | |
| Trim out the given I<string> from every song in the playlist view.
 | |
| 
 | |
| =back
 | |
| 
 | |
| The following key-bindings are available:
 | |
| 
 | |
| =over 8
 | |
| 
 | |
| =item backspace or C-h
 | |
| 
 | |
| Seek back to the beginning of the track.
 | |
| 
 | |
| =item space
 | |
| 
 | |
| Toggle play/pause.
 | |
| 
 | |
| =item < or p
 | |
| 
 | |
| Play previous song.
 | |
| 
 | |
| =item > or n
 | |
| 
 | |
| Play next song.
 | |
| 
 | |
| =item ,
 | |
| 
 | |
| Seek backward by five seconds.
 | |
| 
 | |
| =item .
 | |
| 
 | |
| Seek forward by five seconds.
 | |
| 
 | |
| =item R
 | |
| 
 | |
| Randomize the playlist.
 | |
| 
 | |
| =item S
 | |
| 
 | |
| Sort the playlist.
 | |
| 
 | |
| =item s
 | |
| 
 | |
| Save the status to the file given with the B<-p> flag.
 | |
| 
 | |
| =item q
 | |
| 
 | |
| Quit.
 | |
| 
 | |
| =back
 | |
| 
 | |
| =head1 SEE ALSO
 | |
| 
 | |
| amused(1)
 | |
| 
 | |
| =cut
 | 
