mistress emilia9/dotfiles / bin / amused-monitor
  1#!/usr/bin/env perl
  2#
  3# Copyright (c) 2022 Omar Polo <op@omarpolo.com>
  4#
  5# Permission to use, copy, modify, and distribute this software for any
  6# purpose with or without fee is hereby granted, provided that the above
  7# copyright notice and this permission notice appear in all copies.
  8#
  9# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 10# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 11# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 12# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 13# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 14# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 15# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 16
 17use strict;
 18use warnings;
 19use v5.12;
 20
 21use open ":std", ":encoding(UTF-8)";
 22use utf8;
 23
 24use Curses;
 25use POSIX qw(:sys_wait_h setlocale LC_ALL);
 26use Text::CharWidth qw(mbswidth);
 27use IO::Poll qw(POLLIN);
 28use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
 29use Getopt::Long qw(:config bundling);
 30use Pod::Usage;
 31
 32my $run = 1;
 33
 34my $pfile;
 35my $trim = "";
 36
 37my $pair_n = 1;
 38
 39my @songs;
 40my $current_song;
 41my $playlist_cur;
 42my $playlist_max;
 43my $time_cur;
 44my $time_dur;
 45my $status;
 46my $mode;
 47
 48my $last_lines;
 49
 50sub round {
 51	return int(0.5 + shift);
 52}
 53
 54sub max {
 55	my ($a, $b) = @_;
 56	return $a > $b ? $a : $b;
 57}
 58
 59sub excerpt {
 60	my $lines = shift;
 61	my @tmp;
 62	my ($n, $idx, $cur) = (0, 0, -1);
 63
 64	open (my $fh, "-|", "amused", "show", "-p");
 65	while (<$fh>) {
 66		chomp;
 67		s,$trim,,;
 68		$tmp[$idx] = $_;
 69
 70		if (m/^>/) {
 71			$cur = $n;
 72			$current_song = s/^> //r;
 73		}
 74
 75		$n++;
 76		$idx = ++$idx % $lines;
 77
 78		last if $cur != -1 && $n - $cur > int($lines/2) &&
 79		    $#tmp == $lines-1;
 80	}
 81	close($fh);
 82
 83	return ("Empty playlist.") unless @tmp;
 84
 85	# reorder the entries
 86	my @r;
 87	my $len = $#tmp + 1;
 88	$idx = $idx % $len;
 89	for (1..$len) {
 90		push @r, $tmp[$idx];
 91		$idx = ++$idx % $len;
 92	}
 93	return @r;
 94}
 95
 96sub playlist_numbers {
 97	my ($cur, $tot, $found) = (0, 0, 0);
 98	open (my $fh, "-|", "amused", "show", "-p");
 99	while (<$fh>) {
100		$tot++;
101		$cur++ unless $found;
102		$found = 1 if m/^>/;
103	}
104	close($fh);
105	return ($cur, $tot);
106}
107
108sub status {
109	my ($pos, $dur, $mode);
110
111	open (my $fh, "-|", "amused", "status", "-f",
112	      "status,time:raw,mode:oneline");
113
114	<$fh> =~ m/([a-z]+) (.*)/;
115	my ($status, $current_song) = ($1, $2);
116
117	while (<$fh>) {
118		chomp;
119		$pos = s/position //r if m/^position /;
120		$dur = s/duration //r if m/^duration /;
121		$mode = $_ if m/^repeat/;
122	}
123	close($fh);
124	return ($status, $current_song, $pos, $dur, $mode);
125}
126
127sub showtime {
128	my $seconds = shift;
129	my $str = "";
130
131	if ($seconds > 3600) {
132		my $hours = int($seconds / 3600);
133		$seconds -= $hours * 3600;
134		$str = sprintf("%02d:", $hours);
135	}
136
137	my $minutes = int($seconds / 60);
138	$seconds -= $minutes * 60;
139	$str .= sprintf "%02d:%02d", $minutes, $seconds;
140	return $str;
141}
142
143sub center {
144	my ($str, $pstr) = @_;
145	my $width = mbswidth($str);
146	return $str if $width > $COLS;
147	my $pre = round(($COLS - $width) / 2);
148	my $lpad = $pstr x $pre;
149	my $rpad = $pstr x ($COLS - $width - $pre);
150	return ($lpad, $str, $rpad);
151}
152
153sub offsets {
154	my ($y, $x, $cur, $max) = @_;
155	my ($pre, $c, $post) = center(" $cur / $max ", '-');
156	addstring $y, $x, "";
157
158	my $p = COLOR_PAIR($pair_n);
159
160	attron $p;
161	addstring $pre;
162	attroff $p;
163
164	addstring $c;
165
166	attron $p;
167	addstring $post;
168	attroff $p;
169}
170
171sub progress {
172	my ($y, $x, $pos, $dur) = @_;
173
174	my $pstr = showtime $pos;
175	my $dstr = showtime $dur;
176
177	my $len = $COLS - length($pstr) - length($dstr) - 4;
178	return if $len <= 0 or $dur <= 0;
179	my $filled = round($pos * $len / $dur);
180
181	addstring $y, $x, "$pstr [";
182	addstring "#" x $filled;
183	addstring " " x max($len - $filled, 0);
184	addstring "] $dstr";
185}
186
187sub show_status {
188	my ($y, $x, $status) = @_;
189	my ($pre, $c, $post) = center($status, ' ');
190	addstring $y, $x, $pre;
191	addstring $c;
192	addstring $post;
193}
194
195sub show_mode {
196	my ($y, $x, $mode) = @_;
197	my ($pre, $c, $post) = center($mode, ' ');
198	addstring $y, $x, $pre;
199	addstring $c;
200	addstring $post;
201}
202
203sub render {
204	erase;
205	if ($LINES < 4 || $COLS < 20) {
206		addstring "window too small";
207		refresh;
208		return;
209	}
210
211	my $song_pad = "";
212	my $longest = 0;
213	$longest = max $longest, length($_) foreach @songs;
214	if ($longest < $COLS) {
215		$song_pad = " " x (($COLS - $longest)/2);
216	}
217
218	my $line = 0;
219	map {
220		attron(A_BOLD) if m/^>/;
221		addstring $line++, 0, $song_pad . $_;
222		standend;
223	} @songs;
224
225	offsets $LINES - 4, 0, $playlist_cur, $playlist_max;
226	progress $LINES - 3, 0, $time_cur, $time_dur;
227	show_status $LINES - 2, 0, "$status $current_song";
228	show_mode $LINES - 1, 0, $mode;
229
230	refresh;
231}
232
233sub getsongs {
234	$last_lines = $LINES;
235	@songs = excerpt $LINES - 4;
236}
237
238sub getnums {
239	($playlist_cur, $playlist_max) = playlist_numbers;
240}
241
242sub save {
243	return unless defined $pfile;
244
245	open(my $fh, ">", $pfile);
246	open(my $ph, "-|", "amused", "show", "-p");
247
248	print $fh $_ while (<$ph>);
249}
250
251sub hevent {
252	my $fh = shift;
253	my $l = <$fh>;
254	die "monitor quit" unless defined($l);
255
256	$status = "playing" if $l =~ m/^play/;
257	$status = "paused"  if $l =~ m/^pause/;
258	$status = "stopped" if $l =~ m/^stop/;
259
260	($time_cur, $time_dur) = ($1, $2) if $l =~ m/^seek (\d+) (\d+)/;
261
262	$mode = $1 if $l =~ m/^mode (.*)/;
263
264	getnums if $l =~ m/load|jump|next|previous/;
265	getsongs if $l =~ m/load|jump|next|previous/;
266}
267
268sub hinput {
269	my ($ch, $key) = getchar;
270	if (defined $key) {
271		if ($key == KEY_BACKSPACE) {
272			system "amused", "seek", "0";
273		}
274	} elsif (defined $ch) {
275		if ($ch eq " ") {
276			system "amused", "toggle";
277		} elsif ($ch eq "<" or $ch eq "p") {
278			system "amused", "previous";
279		} elsif ($ch eq ">" or $ch eq "n") {
280			system "amused", "next";
281		} elsif ($ch eq ",") {
282			system "amused", "seek", "-5";
283		} elsif ($ch eq ".") {
284			system "amused", "seek", "+5";
285		} elsif ($ch eq "S") {
286			system "amused show | sort -u | amused load";
287		} elsif ($ch eq "R") {
288			system "amused show | sort -R | amused load";
289		} elsif ($ch eq "s") {
290			save;
291		} elsif ($ch eq "q") {
292			$run = 0;
293		} elsif ($ch eq "\cH") {
294			system "amused", "seek", "0"
295		}
296	}
297}
298
299GetOptions(
300	"p:s" => \$pfile,
301	"t:s" => \$trim,
302    ) or pod2usage(1);
303
304my $mpid = open(my $monitor, "-|", "amused", "monitor")
305    or die "can't spawn amused monitor";
306
307setlocale(LC_ALL, "");
308initscr;
309start_color;
310use_default_colors;
311init_pair $pair_n, 250, -1;
312
313timeout 1000;
314scrollok 0;
315curs_set 0;
316keypad 1;
317
318my $poll = IO::Poll->new();
319$poll->mask(\*STDIN => POLLIN);
320$poll->mask($monitor => POLLIN);
321
322if (`uname` =~ "OpenBSD") {
323	use OpenBSD::Pledge;
324	use OpenBSD::Unveil;
325
326	my $prog = `which amused`;
327	chomp $prog;
328
329	unveil($prog, 'rx') or die "unveil $prog: $!";
330	if (defined($pfile)) {
331		unveil($pfile, 'wc') or die "unveil $pfile: $!";
332		pledge qw(stdio wpath cpath tty proc exec) or die "pledge: $!";
333	} else {
334		pledge qw(stdio tty proc exec) or die "pledge: $!";
335	}
336}
337
338getsongs;
339getnums;
340($status, $current_song, $time_cur, $time_dur, $mode) = status;
341render;
342
343while ($run) {
344	$poll->poll();
345	hinput if $poll->events(\*STDIN) & POLLIN;
346	hevent $monitor if $poll->events($monitor) & POLLIN;
347
348	getsongs if $LINES != $last_lines;
349
350	render;
351}
352
353endwin;
354save;
355
356kill 'INT', $mpid;
357wait;
358
359__END__
360
361=pod
362
363=head1 NAME
364
365amused-monitor - curses interface for amused(1)
366
367=head1 SYNOPSIS
368
369B<amused-monitor> [B<-p> I<playlist>] [B<-t> I<string>]
370
371=head1 DESCRIPTION
372
373amused-monitor is a simple curses interface for amused(1).
374
375The following options are available:
376
377=over 12
378
379=item B<-p> I<playlist>
380
381Save the current playling queue to the file I<playlist> upon exit or
382I<s> key.
383
384=item B<-t> I<string>
385
386Trim out the given I<string> from every song in the playlist view.
387
388=back
389
390The following key-bindings are available:
391
392=over 8
393
394=item backspace or C-h
395
396Seek back to the beginning of the track.
397
398=item space
399
400Toggle play/pause.
401
402=item < or p
403
404Play previous song.
405
406=item > or n
407
408Play next song.
409
410=item ,
411
412Seek backward by five seconds.
413
414=item .
415
416Seek forward by five seconds.
417
418=item R
419
420Randomize the playlist.
421
422=item S
423
424Sort the playlist.
425
426=item s
427
428Save the status to the file given with the B<-p> flag.
429
430=item q
431
432Quit.
433
434=back
435
436=head1 SEE ALSO
437
438amused(1)
439
440=cut