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 $mode;
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 > int($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 $idx = $idx % $len;
80 for (1..$len) {
81 push @r, $tmp[$idx];
82 $idx = ++$idx % $len;
83 }
84 return @r;
85 }
87 sub playlist_numbers {
88 my ($cur, $tot, $found) = (0, 0, 0);
89 open (my $fh, "-|", "amused", "show", "-p");
90 while (<$fh>) {
91 $tot++;
92 $cur++ unless $found;
93 $found = 1 if m/^>/;
94 }
95 close($fh);
96 return ($cur, $tot);
97 }
99 sub status {
100 my ($pos, $dur, $mode);
102 open (my $fh, "-|", "amused", "status", "-f",
103 "status,time:raw,mode:oneline");
104 my $status = <$fh>;
105 chomp $status;
106 while (<$fh>) {
107 chomp;
108 $pos = s/position //r if m/^position /;
109 $dur = s/duration //r if m/^duration /;
110 $mode = $_ if m/^repeat/;
112 close($fh);
113 return ($status, $pos, $dur, $mode);
116 sub showtime {
117 my $seconds = shift;
118 my $str = "";
120 if ($seconds > 3600) {
121 my $hours = int($seconds / 3600);
122 $seconds -= $hours * 3600;
123 $str = sprintf("%02:", $hours);
126 my $minutes = int($seconds / 60);
127 $seconds -= $minutes * 60;
128 $str .= sprintf "%02d:%02d", $minutes, $seconds;
129 return $str;
132 sub center {
133 my ($str, $pstr) = @_;
134 my $width = mbswidth($str);
135 return $str if $width > $COLS;
136 my $pre = ($COLS - $width) / 2;
137 my $lpad = $pstr x $pre;
138 my $rpad = $pstr x ($COLS - $width - $pre + 1);
139 return ($lpad, $str, $rpad);
142 sub offsets {
143 my ($y, $x, $cur, $max) = @_;
144 my ($pre, $c, $post) = center(" $cur / $max ", '-');
145 addstring $y, $x, "";
147 my $p = COLOR_PAIR($pair_n);
149 attron $p;
150 addstring $pre;
151 attroff $p;
153 addstring $c;
155 attron $p;
156 addstring $post;
157 attroff $p;
160 sub progress {
161 my ($y, $x, $pos, $dur) = @_;
163 my $pstr = showtime $pos;
164 my $dstr = showtime $dur;
166 my $len = $COLS - length($pstr) - length($dstr) - 4;
167 return if $len <= 0 or $dur <= 0;
168 my $filled = round($pos * $len / $dur);
170 addstring $y, $x, "$pstr [";
171 addstring "#" x $filled;
172 addstring " " x max($len - $filled, 0);
173 addstring "] $dstr";
176 sub show_status {
177 my ($y, $x, $status) = @_;
178 my ($pre, $c, $post) = center($status, ' ');
179 addstring $y, $x, $pre;
180 addstring $c;
181 addstring $post;
184 sub show_mode {
185 my ($y, $x, $mode) = @_;
186 my ($pre, $c, $post) = center($mode, ' ');
187 addstring $y, $x, $pre;
188 addstring $c;
189 addstring $post;
192 sub render {
193 erase;
194 if ($LINES < 4 || $COLS < 20) {
195 addstring "window too small";
196 refresh;
197 return;
200 my $song_pad = "";
201 my $longest = 0;
202 $longest = max $longest, length($_) foreach @songs;
203 if ($longest < $COLS) {
204 $song_pad = " " x (($COLS - $longest)/2);
207 my $line = 0;
208 map {
209 attron(A_BOLD) if m/^>/;
210 addstring $line++, 0, $song_pad . $_;
211 standend;
212 } @songs;
214 offsets $LINES - 4, 0, $playlist_cur, $playlist_max;
215 progress $LINES - 3, 0, $time_cur, $time_dur;
216 show_status $LINES - 2, 0, $status;
217 show_mode $LINES - 1, 0, $mode;
219 refresh;
222 sub getsongs {
223 $last_lines = $LINES;
224 @songs = excerpt $LINES - 4;
227 sub getnums {
228 ($playlist_cur, $playlist_max) = playlist_numbers;
231 sub getstatus {
232 ($status, $time_cur, $time_dur, $mode) = status;
235 sub save {
236 return unless defined $pfile;
238 open(my $fh, ">", $pfile);
239 open(my $ph, "-|", "amused", "show", "-p");
241 print $fh $_ while (<$ph>);
244 sub hevent {
245 my $fh = shift;
246 my $l = <$fh>;
247 die "monitor quit" unless defined($l);
248 getstatus;
249 getnums if $l =~ m/load|jump|next|prev/;
250 getsongs if $l =~ m/load|jump|next|prev/;
253 sub hinput {
254 my ($ch, $key) = getchar;
255 if (defined $key) {
256 if ($key == KEY_BACKSPACE) {
257 system "amused", " seek", "0";
259 } elsif (defined $ch) {
260 if ($ch eq " ") {
261 system "amused", "toggle";
262 } elsif ($ch eq "<" or $ch eq "p") {
263 system "amused", "prev";
264 } elsif ($ch eq ">" or $ch eq "n") {
265 system "amused", "next";
266 } elsif ($ch eq ",") {
267 system "amused", "seek", "-5";
268 } elsif ($ch eq ".") {
269 system "amused", "seek", "+5";
270 } elsif ($ch eq "S") {
271 system "amused show | sort -u | amused load";
272 } elsif ($ch eq "R") {
273 system "amused show | sort -R | amused load";
274 } elsif ($ch eq "s") {
275 save;
276 } elsif ($ch eq "q") {
277 $run = 0;
278 } elsif ($ch eq "\cH") {
279 system "amused", " seek", "0"
284 GetOptions(
285 "p:s" => \$pfile,
286 "t:s" => \$trim,
287 ) or pod2usage(1);
289 my $mpid = open(my $monitor, "-|", "amused", "monitor")
290 or die "can't spawn amused monitor";
292 initscr;
293 start_color;
294 use_default_colors;
295 init_pair $pair_n, 250, -1;
297 timeout 1000;
298 scrollok 0;
299 curs_set 0;
300 keypad 1;
302 my $poll = IO::Poll->new();
303 $poll->mask(\*STDIN => POLLIN);
304 $poll->mask($monitor => POLLIN);
306 my $tick = 0;
307 my $tbefore = clock_gettime(CLOCK_MONOTONIC);
309 getsongs;
310 getnums;
311 getstatus;
312 render;
314 while ($run) {
315 $poll->poll(0.25);
316 my $now = clock_gettime(CLOCK_MONOTONIC);
317 my $elapsed = $now - $tbefore;
318 if ($elapsed > 1) {
319 $tbefore = $now;
320 $time_cur += round($elapsed)
321 if $status =~ m/^playing/;
324 getstatus unless $tick++ % 8;
326 hinput if $poll->events(\*STDIN) & POLLIN;
327 hevent $monitor if $poll->events($monitor) & POLLIN;
329 getsongs if $LINES != $last_lines;
331 render;
334 endwin;
335 save;
337 kill 'INT', $mpid;
338 wait;
340 __END__
342 =pod
344 =head1 NAME
346 amused-monitor - curses interface for amused(1)
348 =head1 SYNOPSIS
350 B<amused-monitor> [B<-p> I<playlist>] [B<-t> I<string>]
352 =head1 DESCRIPTION
354 amused-monitor is a simple curses interface for amused(1).
356 The following options are available:
358 =over 12
360 =item B<-p> I<playlist>
362 Save the current playling queue to the file I<playlist> upon exit or
363 I<s> key.
365 =item B<-t> I<string>
367 Trim out the given I<string> from every song in the playlist view.
369 =back
371 The following key-bindings are available:
373 =over 8
375 =item backspace or C-h
377 Seek back to the beginning of the track.
379 =item space
381 Toggle play/pause.
383 =item < or p
385 Play previous song.
387 =item > or n
389 Play next song.
391 =item ,
393 Seek backward by five seconds.
395 =item .
397 Seek forward by five seconds.
399 =item R
401 Randomize the playlist.
403 =item S
405 Sort the playlist.
407 =item s
409 Save the status to the file given with the B<-p> flag.
411 =item q
413 Quit.
415 =back
417 =head1 SEE ALSO
419 amused(1)
421 =cut