Blob


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.
17 use strict;
18 use warnings;
19 use v5.12;
21 use Curses;
22 use POSIX ":sys_wait_h";
23 use Text::CharWidth qw(mbswidth);
24 use IO::Poll qw(POLLIN);
25 use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
26 use Getopt::Long qw(:config bundling);
27 use Pod::Usage;
29 my $run = 1;
31 my $pfile;
32 my $trim = "";
34 my $pair_n = 1;
36 my @songs;
37 my $current_song;
38 my $playlist_cur;
39 my $playlist_max;
40 my $time_cur;
41 my $time_dur;
42 my $status;
43 my $mode;
45 my $last_lines;
47 sub round {
48 return int(0.5 + shift);
49 }
51 sub max {
52 my ($a, $b) = @_;
53 return $a > $b ? $a : $b;
54 }
56 sub excerpt {
57 my $lines = shift;
58 my @tmp;
59 my ($n, $idx, $cur) = (0, 0, -1);
61 open (my $fh, "-|", "amused", "show", "-p");
62 while (<$fh>) {
63 chomp;
64 s,$trim,,;
65 $tmp[$idx] = $_;
67 if (m/^>/) {
68 $cur = $n;
69 $current_song = s/^> //r;
70 }
72 $n++;
73 $idx = ++$idx % $lines;
75 last if $cur != -1 && $n - $cur > int($lines/2) &&
76 $#tmp == $lines-1;
77 }
78 close($fh);
80 return ("Empty playlist.") unless @tmp;
82 # reorder the entries
83 my @r;
84 my $len = $#tmp + 1;
85 $idx = $idx % $len;
86 for (1..$len) {
87 push @r, $tmp[$idx];
88 $idx = ++$idx % $len;
89 }
90 return @r;
91 }
93 sub playlist_numbers {
94 my ($cur, $tot, $found) = (0, 0, 0);
95 open (my $fh, "-|", "amused", "show", "-p");
96 while (<$fh>) {
97 $tot++;
98 $cur++ unless $found;
99 $found = 1 if m/^>/;
101 close($fh);
102 return ($cur, $tot);
105 sub status {
106 my ($pos, $dur, $mode);
108 open (my $fh, "-|", "amused", "status", "-f",
109 "status,time:raw,mode:oneline");
111 <$fh> =~ m/([a-z]+) (.*)/;
112 my ($status, $current_song) = ($1, $2);
114 while (<$fh>) {
115 chomp;
116 $pos = s/position //r if m/^position /;
117 $dur = s/duration //r if m/^duration /;
118 $mode = $_ if m/^repeat/;
120 close($fh);
121 return ($status, $current_song, $pos, $dur, $mode);
124 sub showtime {
125 my $seconds = shift;
126 my $str = "";
128 if ($seconds > 3600) {
129 my $hours = int($seconds / 3600);
130 $seconds -= $hours * 3600;
131 $str = sprintf("%02d:", $hours);
134 my $minutes = int($seconds / 60);
135 $seconds -= $minutes * 60;
136 $str .= sprintf "%02d:%02d", $minutes, $seconds;
137 return $str;
140 sub center {
141 my ($str, $pstr) = @_;
142 my $width = mbswidth($str);
143 return $str if $width > $COLS;
144 my $pre = round(($COLS - $width) / 2);
145 my $lpad = $pstr x $pre;
146 my $rpad = $pstr x ($COLS - $width - $pre);
147 return ($lpad, $str, $rpad);
150 sub offsets {
151 my ($y, $x, $cur, $max) = @_;
152 my ($pre, $c, $post) = center(" $cur / $max ", '-');
153 addstring $y, $x, "";
155 my $p = COLOR_PAIR($pair_n);
157 attron $p;
158 addstring $pre;
159 attroff $p;
161 addstring $c;
163 attron $p;
164 addstring $post;
165 attroff $p;
168 sub progress {
169 my ($y, $x, $pos, $dur) = @_;
171 my $pstr = showtime $pos;
172 my $dstr = showtime $dur;
174 my $len = $COLS - length($pstr) - length($dstr) - 4;
175 return if $len <= 0 or $dur <= 0;
176 my $filled = round($pos * $len / $dur);
178 addstring $y, $x, "$pstr [";
179 addstring "#" x $filled;
180 addstring " " x max($len - $filled, 0);
181 addstring "] $dstr";
184 sub show_status {
185 my ($y, $x, $status) = @_;
186 my ($pre, $c, $post) = center($status, ' ');
187 addstring $y, $x, $pre;
188 addstring $c;
189 addstring $post;
192 sub show_mode {
193 my ($y, $x, $mode) = @_;
194 my ($pre, $c, $post) = center($mode, ' ');
195 addstring $y, $x, $pre;
196 addstring $c;
197 addstring $post;
200 sub render {
201 erase;
202 if ($LINES < 4 || $COLS < 20) {
203 addstring "window too small";
204 refresh;
205 return;
208 my $song_pad = "";
209 my $longest = 0;
210 $longest = max $longest, length($_) foreach @songs;
211 if ($longest < $COLS) {
212 $song_pad = " " x (($COLS - $longest)/2);
215 my $line = 0;
216 map {
217 attron(A_BOLD) if m/^>/;
218 addstring $line++, 0, $song_pad . $_;
219 standend;
220 } @songs;
222 offsets $LINES - 4, 0, $playlist_cur, $playlist_max;
223 progress $LINES - 3, 0, $time_cur, $time_dur;
224 show_status $LINES - 2, 0, "$status $current_song";
225 show_mode $LINES - 1, 0, $mode;
227 refresh;
230 sub getsongs {
231 $last_lines = $LINES;
232 @songs = excerpt $LINES - 4;
235 sub getnums {
236 ($playlist_cur, $playlist_max) = playlist_numbers;
239 sub save {
240 return unless defined $pfile;
242 open(my $fh, ">", $pfile);
243 open(my $ph, "-|", "amused", "show", "-p");
245 print $fh $_ while (<$ph>);
248 sub hevent {
249 my $fh = shift;
250 my $l = <$fh>;
251 die "monitor quit" unless defined($l);
253 $status = "playing" if $l =~ m/^play/;
254 $status = "paused" if $l =~ m/^pause/;
255 $status = "stopped" if $l =~ m/^stop/;
257 ($time_cur, $time_dur) = ($1, $2) if $l =~ m/^seek (\d+) (\d+)/;
259 $mode = $1 if $l =~ m/^mode (.*)/;
261 getnums if $l =~ m/load|jump|next|prev/;
262 getsongs if $l =~ m/load|jump|next|prev/;
265 sub hinput {
266 my ($ch, $key) = getchar;
267 if (defined $key) {
268 if ($key == KEY_BACKSPACE) {
269 system "amused", "seek", "0";
271 } elsif (defined $ch) {
272 if ($ch eq " ") {
273 system "amused", "toggle";
274 } elsif ($ch eq "<" or $ch eq "p") {
275 system "amused", "prev";
276 } elsif ($ch eq ">" or $ch eq "n") {
277 system "amused", "next";
278 } elsif ($ch eq ",") {
279 system "amused", "seek", "-5";
280 } elsif ($ch eq ".") {
281 system "amused", "seek", "+5";
282 } elsif ($ch eq "S") {
283 system "amused show | sort -u | amused load";
284 } elsif ($ch eq "R") {
285 system "amused show | sort -R | amused load";
286 } elsif ($ch eq "s") {
287 save;
288 } elsif ($ch eq "q") {
289 $run = 0;
290 } elsif ($ch eq "\cH") {
291 system "amused", "seek", "0"
296 GetOptions(
297 "p:s" => \$pfile,
298 "t:s" => \$trim,
299 ) or pod2usage(1);
301 my $mpid = open(my $monitor, "-|", "amused", "monitor")
302 or die "can't spawn amused monitor";
304 initscr;
305 start_color;
306 use_default_colors;
307 init_pair $pair_n, 250, -1;
309 timeout 1000;
310 scrollok 0;
311 curs_set 0;
312 keypad 1;
314 my $poll = IO::Poll->new();
315 $poll->mask(\*STDIN => POLLIN);
316 $poll->mask($monitor => POLLIN);
318 if (`uname` =~ "OpenBSD") {
319 use OpenBSD::Pledge;
320 use OpenBSD::Unveil;
322 my $prog = `which amused`;
323 chomp $prog;
325 unveil($prog, 'rx') or die "unveil $prog: $!";
326 if (defined($pfile)) {
327 unveil($pfile, 'wc') or die "unveil $pfile: $!";
328 pledge qw(stdio wpath cpath tty proc exec) or die "pledge: $!";
329 } else {
330 pledge qw(stdio tty proc exec) or die "pledge: $!";
334 getsongs;
335 getnums;
336 ($status, $current_song, $time_cur, $time_dur, $mode) = status;
337 render;
339 while ($run) {
340 $poll->poll();
341 hinput if $poll->events(\*STDIN) & POLLIN;
342 hevent $monitor if $poll->events($monitor) & POLLIN;
344 getsongs if $LINES != $last_lines;
346 render;
349 endwin;
350 save;
352 kill 'INT', $mpid;
353 wait;
355 __END__
357 =pod
359 =head1 NAME
361 amused-monitor - curses interface for amused(1)
363 =head1 SYNOPSIS
365 B<amused-monitor> [B<-p> I<playlist>] [B<-t> I<string>]
367 =head1 DESCRIPTION
369 amused-monitor is a simple curses interface for amused(1).
371 The following options are available:
373 =over 12
375 =item B<-p> I<playlist>
377 Save the current playling queue to the file I<playlist> upon exit or
378 I<s> key.
380 =item B<-t> I<string>
382 Trim out the given I<string> from every song in the playlist view.
384 =back
386 The following key-bindings are available:
388 =over 8
390 =item backspace or C-h
392 Seek back to the beginning of the track.
394 =item space
396 Toggle play/pause.
398 =item < or p
400 Play previous song.
402 =item > or n
404 Play next song.
406 =item ,
408 Seek backward by five seconds.
410 =item .
412 Seek forward by five seconds.
414 =item R
416 Randomize the playlist.
418 =item S
420 Sort the playlist.
422 =item s
424 Save the status to the file given with the B<-p> flag.
426 =item q
428 Quit.
430 =back
432 =head1 SEE ALSO
434 amused(1)
436 =cut