commit 6f8bcff

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