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 open ":std", ":encoding(UTF-8)";
22 use utf8;
24 use Curses;
25 use POSIX qw(:sys_wait_h setlocale LC_ALL);
26 use Text::CharWidth qw(mbswidth);
27 use IO::Poll qw(POLLIN);
28 use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
29 use Getopt::Long qw(:config bundling);
30 use Pod::Usage;
32 my $run = 1;
34 my $pfile;
35 my $trim = "";
37 my $pair_n = 1;
39 my @songs;
40 my $current_song;
41 my $playlist_cur;
42 my $playlist_max;
43 my $time_cur;
44 my $time_dur;
45 my $status;
46 my $mode;
48 my $last_lines;
50 sub round {
51 return int(0.5 + shift);
52 }
54 sub max {
55 my ($a, $b) = @_;
56 return $a > $b ? $a : $b;
57 }
59 sub excerpt {
60 my $lines = shift;
61 my @tmp;
62 my ($n, $idx, $cur) = (0, 0, -1);
64 open (my $fh, "-|", "amused", "show", "-p");
65 while (<$fh>) {
66 chomp;
67 s,$trim,,;
68 $tmp[$idx] = $_;
70 if (m/^>/) {
71 $cur = $n;
72 $current_song = s/^> //r;
73 }
75 $n++;
76 $idx = ++$idx % $lines;
78 last if $cur != -1 && $n - $cur > int($lines/2) &&
79 $#tmp == $lines-1;
80 }
81 close($fh);
83 return ("Empty playlist.") unless @tmp;
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 }
96 sub 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/^>/;
104 close($fh);
105 return ($cur, $tot);
108 sub status {
109 my ($pos, $dur, $mode);
111 open (my $fh, "-|", "amused", "status", "-f",
112 "status,time:raw,mode:oneline");
114 <$fh> =~ m/([a-z]+) (.*)/;
115 my ($status, $current_song) = ($1, $2);
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/;
123 close($fh);
124 return ($status, $current_song, $pos, $dur, $mode);
127 sub showtime {
128 my $seconds = shift;
129 my $str = "";
131 if ($seconds > 3600) {
132 my $hours = int($seconds / 3600);
133 $seconds -= $hours * 3600;
134 $str = sprintf("%02d:", $hours);
137 my $minutes = int($seconds / 60);
138 $seconds -= $minutes * 60;
139 $str .= sprintf "%02d:%02d", $minutes, $seconds;
140 return $str;
143 sub 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);
153 sub offsets {
154 my ($y, $x, $cur, $max) = @_;
155 my ($pre, $c, $post) = center(" $cur / $max ", '-');
156 addstring $y, $x, "";
158 my $p = COLOR_PAIR($pair_n);
160 attron $p;
161 addstring $pre;
162 attroff $p;
164 addstring $c;
166 attron $p;
167 addstring $post;
168 attroff $p;
171 sub progress {
172 my ($y, $x, $pos, $dur) = @_;
174 my $pstr = showtime $pos;
175 my $dstr = showtime $dur;
177 my $len = $COLS - length($pstr) - length($dstr) - 4;
178 return if $len <= 0 or $dur <= 0;
179 my $filled = round($pos * $len / $dur);
181 addstring $y, $x, "$pstr [";
182 addstring "#" x $filled;
183 addstring " " x max($len - $filled, 0);
184 addstring "] $dstr";
187 sub 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;
195 sub 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;
203 sub render {
204 erase;
205 if ($LINES < 4 || $COLS < 20) {
206 addstring "window too small";
207 refresh;
208 return;
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);
218 my $line = 0;
219 map {
220 attron(A_BOLD) if m/^>/;
221 addstring $line++, 0, $song_pad . $_;
222 standend;
223 } @songs;
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;
230 refresh;
233 sub getsongs {
234 $last_lines = $LINES;
235 @songs = excerpt $LINES - 4;
238 sub getnums {
239 ($playlist_cur, $playlist_max) = playlist_numbers;
242 sub save {
243 return unless defined $pfile;
245 open(my $fh, ">", $pfile);
246 open(my $ph, "-|", "amused", "show", "-p");
248 print $fh $_ while (<$ph>);
251 sub hevent {
252 my $fh = shift;
253 my $l = <$fh>;
254 die "monitor quit" unless defined($l);
256 $status = "playing" if $l =~ m/^play/;
257 $status = "paused" if $l =~ m/^pause/;
258 $status = "stopped" if $l =~ m/^stop/;
260 ($time_cur, $time_dur) = ($1, $2) if $l =~ m/^seek (\d+) (\d+)/;
262 $mode = $1 if $l =~ m/^mode (.*)/;
264 getnums if $l =~ m/load|jump|next|prev/;
265 getsongs if $l =~ m/load|jump|next|prev/;
268 sub hinput {
269 my ($ch, $key) = getchar;
270 if (defined $key) {
271 if ($key == KEY_BACKSPACE) {
272 system "amused", "seek", "0";
274 } elsif (defined $ch) {
275 if ($ch eq " ") {
276 system "amused", "toggle";
277 } elsif ($ch eq "<" or $ch eq "p") {
278 system "amused", "prev";
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"
299 GetOptions(
300 "p:s" => \$pfile,
301 "t:s" => \$trim,
302 ) or pod2usage(1);
304 my $mpid = open(my $monitor, "-|", "amused", "monitor")
305 or die "can't spawn amused monitor";
307 setlocale(LC_ALL, "");
308 initscr;
309 start_color;
310 use_default_colors;
311 init_pair $pair_n, 250, -1;
313 timeout 1000;
314 scrollok 0;
315 curs_set 0;
316 keypad 1;
318 my $poll = IO::Poll->new();
319 $poll->mask(\*STDIN => POLLIN);
320 $poll->mask($monitor => POLLIN);
322 if (`uname` =~ "OpenBSD") {
323 use OpenBSD::Pledge;
324 use OpenBSD::Unveil;
326 my $prog = `which amused`;
327 chomp $prog;
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: $!";
338 getsongs;
339 getnums;
340 ($status, $current_song, $time_cur, $time_dur, $mode) = status;
341 render;
343 while ($run) {
344 $poll->poll();
345 hinput if $poll->events(\*STDIN) & POLLIN;
346 hevent $monitor if $poll->events($monitor) & POLLIN;
348 getsongs if $LINES != $last_lines;
350 render;
353 endwin;
354 save;
356 kill 'INT', $mpid;
357 wait;
359 __END__
361 =pod
363 =head1 NAME
365 amused-monitor - curses interface for amused(1)
367 =head1 SYNOPSIS
369 B<amused-monitor> [B<-p> I<playlist>] [B<-t> I<string>]
371 =head1 DESCRIPTION
373 amused-monitor is a simple curses interface for amused(1).
375 The following options are available:
377 =over 12
379 =item B<-p> I<playlist>
381 Save the current playling queue to the file I<playlist> upon exit or
382 I<s> key.
384 =item B<-t> I<string>
386 Trim out the given I<string> from every song in the playlist view.
388 =back
390 The following key-bindings are available:
392 =over 8
394 =item backspace or C-h
396 Seek back to the beginning of the track.
398 =item space
400 Toggle play/pause.
402 =item < or p
404 Play previous song.
406 =item > or n
408 Play next song.
410 =item ,
412 Seek backward by five seconds.
414 =item .
416 Seek forward by five seconds.
418 =item R
420 Randomize the playlist.
422 =item S
424 Sort the playlist.
426 =item s
428 Save the status to the file given with the B<-p> flag.
430 =item q
432 Quit.
434 =back
436 =head1 SEE ALSO
438 amused(1)
440 =cut