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 $playlist_cur;
38 my $playlist_max;
39 my $time_cur;
40 my $time_dur;
41 my $status;
42 my $repeat;
44 my $last_lines;
46 sub round {
47 return int(0.5 + shift);
48 }
50 sub max {
51 my ($a, $b) = @_;
52 return $a > $b ? $a : $b;
53 }
55 sub excerpt {
56 my $lines = shift;
57 my @tmp;
58 my ($n, $idx, $cur) = (0, 0, -1);
60 open (my $fh, "-|", "amused", "show", "-p");
61 while (<$fh>) {
62 chomp;
63 s,$trim,,;
64 $tmp[$idx] = $_;
65 $cur = $n if m/^>/;
66 $n++;
67 $idx = ++$idx % $lines;
69 last if $cur != -1 && $n - $cur > round($lines/2) &&
70 $#tmp == $lines-1;
71 }
72 close($fh);
74 return ("Empty playlist.") unless @tmp;
76 # reorder the entries
77 my @r;
78 my $len = $#tmp + 1;
79 for (1..$len) {
80 push @r, $tmp[$idx];
81 $idx = ++$idx % $len;
82 }
83 return @r;
84 }
86 sub playlist_numbers {
87 my ($cur, $tot, $found) = (0, 0, 0);
88 open (my $fh, "-|", "amused", "show", "-p");
89 while (<$fh>) {
90 $tot++;
91 $cur++ unless $found;
92 $found = 1 if m/^>/;
93 }
94 close($fh);
95 return ($cur, $tot);
96 }
98 sub status {
99 my ($pos, $dur, $repeat);
101 open (my $fh, "-|", "amused", "status", "-f",
102 "status,time:raw,repeat:oneline");
103 my $status = <$fh>;
104 chomp $status;
105 while (<$fh>) {
106 chomp;
107 $pos = s/position //r if m/^position /;
108 $dur = s/duration //r if m/^duration /;
109 $repeat = $_ if m/^repeat/;
111 close($fh);
112 return ($status, $pos, $dur, $repeat);
115 sub showtime {
116 my $seconds = shift;
117 my $str = "";
119 if ($seconds > 3600) {
120 my $hours = int($seconds / 3600);
121 $seconds -= $hours * 3600;
122 $str = sprintf("%02:", $hours);
125 my $minutes = int($seconds / 60);
126 $seconds -= $minutes * 60;
127 $str .= sprintf "%02d:%02d", $minutes, $seconds;
128 return $str;
131 sub center {
132 my ($str, $pstr) = @_;
133 my $width = mbswidth($str);
134 return $str if $width > $COLS;
135 my $pre = ($COLS - $width) / 2;
136 my $lpad = $pstr x $pre;
137 my $rpad = $pstr x ($COLS - $width - $pre + 1);
138 return ($lpad, $str, $rpad);
141 sub offsets {
142 my ($y, $x, $cur, $max) = @_;
143 my ($pre, $c, $post) = center(" $cur / $max ", '-');
144 addstring $y, $x, "";
146 my $p = COLOR_PAIR($pair_n);
148 attron $p;
149 addstring $pre;
150 attroff $p;
152 addstring $c;
154 attron $p;
155 addstring $post;
156 attroff $p;
159 sub progress {
160 my ($y, $x, $pos, $dur) = @_;
162 my $pstr = showtime $pos;
163 my $dstr = showtime $dur;
165 my $len = $COLS - length($pstr) - length($dstr) - 4;
166 return if $len <= 0 or $dur <= 0;
167 my $filled = round($pos * $len / $dur);
169 addstring $y, $x, "$pstr [";
170 addstring "#" x $filled;
171 addstring " " x max($len - $filled, 0);
172 addstring "] $dstr";
175 sub show_status {
176 my ($y, $x, $status) = @_;
177 my ($pre, $c, $post) = center($status, ' ');
178 addstring $y, $x, $pre;
179 addstring $c;
180 addstring $post;
183 sub show_repeat {
184 my ($y, $x, $repeat) = @_;
185 my ($pre, $c, $post) = center($repeat, ' ');
186 addstring $y, $x, $pre;
187 addstring $c;
188 addstring $post;
191 sub render {
192 erase;
193 if ($LINES < 4 || $COLS < 20) {
194 addstring "window too small";
195 refresh;
196 return;
199 my $song_pad = "";
200 my $longest = 0;
201 $longest = max $longest, length($_) foreach @songs;
202 if ($longest < $COLS) {
203 $song_pad = " " x (($COLS - $longest)/2);
206 my $line = 0;
207 map {
208 attron(A_BOLD) if m/^>/;
209 addstring $line++, 0, $song_pad . $_;
210 standend;
211 } @songs;
213 offsets $LINES - 4, 0, $playlist_cur, $playlist_max;
214 progress $LINES - 3, 0, $time_cur, $time_dur;
215 show_status $LINES - 2, 0, $status;
216 show_repeat $LINES - 1, 0, $repeat;
218 refresh;
221 sub getsongs {
222 $last_lines = $LINES;
223 @songs = excerpt $LINES - 4;
226 sub getnums {
227 ($playlist_cur, $playlist_max) = playlist_numbers;
230 sub getstatus {
231 ($status, $time_cur, $time_dur, $repeat) = status;
234 sub save {
235 return unless defined $pfile;
237 open(my $fh, ">", $pfile);
238 open(my $ph, "-|", "amused", "show", "-p");
240 print $fh $_ while (<$ph>);
243 sub hevent {
244 my $fh = shift;
245 my $l = <$fh>;
246 die "monitor quit" unless defined($l);
247 getstatus;
248 getnums if $l =~ m/load|jump|next|prev/;
249 getsongs if $l =~ m/load|jump|next|prev/;
252 sub hinput {
253 my ($ch, $key) = getchar;
254 if (defined $ch) {
255 if ($ch eq " ") {
256 system "amused", "toggle";
257 } elsif ($ch eq "<" or $ch eq "p") {
258 system "amused", "prev";
259 } elsif ($ch eq ">" or $ch eq "n") {
260 system "amused", "next";
261 } elsif ($ch eq ",") {
262 system "amused", "seek", "-5";
263 } elsif ($ch eq ".") {
264 system "amused", "seek", "+5";
265 } elsif ($ch eq "S") {
266 system "amused show | sort -u | amused load";
267 } elsif ($ch eq "R") {
268 system "amused show | sort -R | amused load";
269 } elsif ($ch eq "s") {
270 save;
271 } elsif ($ch eq "q") {
272 $run = 0;
274 } elsif (defined $key) {
275 # todo?
279 GetOptions(
280 "p:s" => \$pfile,
281 "t:s" => \$trim,
282 ) or pod2usage(1);
284 my $mpid = open(my $monitor, "-|", "amused", "monitor")
285 or die "can't spawn amused monitor";
287 initscr;
288 start_color;
289 use_default_colors;
290 init_pair $pair_n, 250, -1;
292 timeout 1000;
293 scrollok 0;
294 curs_set 0;
296 my $poll = IO::Poll->new();
297 $poll->mask(\*STDIN => POLLIN);
298 $poll->mask($monitor => POLLIN);
300 my $tick = 0;
301 my $tbefore = clock_gettime(CLOCK_MONOTONIC);
303 getsongs;
304 getnums;
305 getstatus;
306 render;
308 while ($run) {
309 $poll->poll(0.25);
310 my $now = clock_gettime(CLOCK_MONOTONIC);
311 my $elapsed = $now - $tbefore;
312 if ($elapsed > 1) {
313 $tbefore = $now;
314 $time_cur += round($elapsed)
315 if $status =~ m/^playing/;
318 ($status, $time_cur, $time_dur, $repeat) = status
319 unless $tick++ % 8;
321 hinput if $poll->events(\*STDIN) & POLLIN;
322 hevent $monitor if $poll->events($monitor) & POLLIN;
324 getsongs if $LINES != $last_lines;
326 render;
329 endwin;
330 save;
332 kill 'INT', $mpid;
333 wait;
335 __END__
337 =pod
339 =head1 NAME
341 amused-monitor - curses interface for amused(1)
343 =head1 SYNOPSIS
345 B<amused-monitor> [B<-p> I<playlist>] [B<-t> I<string>]
347 =head1 DESCRIPTION
349 amused-monitor is a simple curses interface for amused(1).
351 The following options are available:
353 =over 12
355 =item B<-p> I<playlist>
357 Save the current playling queue to the file I<playlist> upon exit or
358 I<s> key.
360 =item B<-t> I<string>
362 Trim out the given I<string> from every song in the playlist view.
364 =back
366 The following key-bindings are available:
368 =over 8
370 =item space
372 Toggle play/pause.
374 =item < or p
376 Play previous song.
378 =item > or n
380 Play next song.
382 =item ,
384 Seek backward by five seconds.
386 =item .
388 Seek forward by five seconds.
390 =item R
392 Randomize the playlist.
394 =item S
396 Sort the playlist.
398 =item s
400 Save the status to the file given with the B<-p> flag.
402 =item q
404 Quit.
406 =back
408 =head1 SEE ALSO
410 amused(1)
412 =cut