Repository navigation
Expand file tree
/
Copy pathsetup
More file actions
executable file
·783 lines (740 loc) · 35.4 KB
/
Copy pathsetup
File metadata and controls
executable file
·783 lines (740 loc) · 35.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
#!/bin/sh
# ┌─┐┌─┐┌┐┌┬ ┬┌─┐┬─┐┬┌─┌─┐
# ┌─┘├┤ │││││││ │├┬┘├┴┐└─┐
# └─┘└─┘┘└┘└┴┘└─┘┴└─┴ ┴└─┘
# spoot Spotify Client ~ Part of the ZENWORKS Suite
# https://github.com/kbuckleys/
#
# EVERYTHING SPOOT NEEDS, IN ONE GO. Probe, install what is missing, build, and
# hand over to spoot -- at which point the only thing left for a human is
# authentication.
#
# It used to be two halves. This script installed a build floor, and spoot itself
# installed the rest on first run: a package manager driven from inside a music
# player, picking a manager, working out how to become root and raising a pkexec
# dialog over its own surface. That half is gone. A terminal can answer a
# password prompt, which is the entire reason the app never should have been
# doing this.
#
# POSIX sh, not bash: OpenBSD and FreeBSD base installs do not have bash, and
# "works on the BSDs" has to mean without one. Nothing here uses [[, local,
# arrays or $'...'.
#
# NOTHING PRESENT IS EVER REINSTALLED. Every dependency is probed first and only
# what is genuinely missing is passed to the package manager -- downgraded
# packages people are holding on purpose stay exactly as they are.
#
# SPOOT_SETUP_DRYRUN=1 prints every probe result and every command it would run,
# and touches nothing.
set -eu
# WHERE THIS SCRIPT IS, not where you ran it from. `cd ~/somewhere && sh
# /path/to/setup` has to work, and so does ./setup -- and neither may write into
# whatever directory happened to be current.
here=$(cd -- "$(dirname -- "$0")" && pwd)
cd "$here"
DRY=${SPOOT_SETUP_DRYRUN:-0}
# --- talking to a terminal -------------------------------------------------
#
# ESC through a variable rather than written into every format string: POSIX
# printf is not required to expand \033 in a format, and the shells that do not
# would print the escape rather than act on it.
ESC=$(printf '\033')
# ZENON's own three, by ROLE rather than by name -- the same values the app
# paints with, so the installer and the thing it installs look like one program.
# C_LOGO was a fourth name for the green and is gone: the logo and Proceed are
# the same colour because they are the same idea, spoot saying yes.
if [ -t 1 ]; then
C_GREEN="${ESC}[38;2;182;224;164m" # #b6e0a4 -- spoot's green (foreground)
C_GREEN_BG="${ESC}[48;2;182;224;164m" # #b6e0a4 -- spoot's green (background)
C_BLUE="${ESC}[38;2;159;203;252m" # #9fcbfc -- a link
C_BLACK="${ESC}[38;2;0;0;0m" # #000000 -- black text
C_GREY="${ESC}[38;2;106;112;127m" # #6a707f -- the road not taken
C_WHITE_BG="${ESC}[48;2;223;223;221m" # #dfdfdd -- the road not taken background
C_RED_BG="${ESC}[48;2;231;130;132m" # #e78284 -- abort red (background)
C_DIM="${ESC}[2m"; C_BOLD="${ESC}[1m"; C_OFF="${ESC}[0m"
C_OK="${ESC}[1;36m"; C_WARN="${ESC}[1;33m"; C_ERR="${ESC}[1;31m"
else
C_GREEN=""; C_GREEN_BG=""; C_BLUE=""; C_BLACK=""; C_GREY=""; C_WHITE_BG=""; C_RED_BG=""
C_DIM=""; C_BOLD=""; C_OFF=""; C_OK=""; C_WARN=""; C_ERR=""
fi
say() { printf '%s::%s %s\n' "$C_OK" "$C_OFF" "$*"; }
warn() { printf '%s::%s %s\n' "$C_WARN" "$C_OFF" "$*" >&2; }
die() { printf '%s::%s %s\n' "$C_ERR" "$C_OFF" "$*" >&2; exit 1; }
step() { printf '%s %s%s\n' "$C_DIM" "$*" "$C_OFF"; }
[ -f engine/spoot.lua ] && [ -f src/main.cpp ] \
|| die "run this from inside the spoot tree (engine/ and src/ are missing)"
# --- the door ---------------------------------------------------------------
banner() {
printf '%s┌─┐┌─┐┌─┐┌─┐┌┬┐%s\n' "$C_GREEN" "$C_OFF"
printf '%s└─┐├─┘│ ││ │ │ %s\n' "$C_GREEN" "$C_OFF"
printf '%s└─┘┴ └─┘└─┘ ┴ %s\n' "$C_GREEN" "$C_OFF"
printf '\n'
printf ' Welcome to spoot -- a blazing fast, keyboard-first Spotify client\n'
# THE URL IS ITS OWN COLOUR, so the line is printed in three pieces rather
# than one: everything between the escapes is the link and nothing else, which
# is also what lets a terminal that linkifies by eye pick it out.
printf ' Visit %s%s%s to see more of my projects\n' \
"$C_BLUE" "https://github.com/kbuckleys/" "$C_OFF"
printf '\n'
printf ' %s%s%sThis process is fully automated and will perform these tasks once permitted%s\n' \
"$C_BOLD" "$C_BLACK" "$C_GREEN_BG" "$C_OFF"
printf ' - Detect your Linux/BSD distribution and fetch missing dependencies\n'
printf ' - Compile the spoot binary\n'
printf " - Redirect you to Spotify's authentication page\n"
printf ' - Quit upon completion\n'
printf '\n'
}
# Two lines, one of them lit. Redrawn in place by walking the cursor back up
# over itself, so choosing does not scroll the banner away.
#
# EACH ROW KEEPS ITS OWN COLOUR whichever one is selected -- green for yes, grey
# for no -- because that is what the row MEANS, and a menu whose two answers swap
# colours as you move between them is asking to be misread at a glance. What
# moves is the weight: the selected row is bold, the other faint. So the colour
# says which answer it is and the weight says which one you are on, and neither
# has to carry both jobs.
draw_choice() {
if [ "$1" = 0 ]; then
printf ' %s%s%sProceed%s\n' "$C_BOLD" "$C_BLACK" "$C_GREEN_BG" "$C_OFF"
printf ' %sAbort%s\n' "$C_GREY" "$C_OFF"
else
printf ' %sProceed%s\n' "$C_GREEN" "$C_OFF"
printf ' %s%s%sAbort%s\n' "$C_BOLD" "$C_BLACK" "$C_WHITE_BG" "$C_OFF"
fi
}
# One keypress. `min 0 time 1` on the follow-up reads is what stops a lone
# Escape from hanging: an arrow arrives as three bytes and Escape as one, and
# there is no way to tell them apart except by waiting a tenth of a second for
# bytes that never come.
read_key() {
k=$(dd bs=1 count=1 2>/dev/null)
if [ "$k" = "$ESC" ]; then
stty min 0 time 1 2>/dev/null || true
rest=$(dd bs=1 count=2 2>/dev/null)
stty min 1 time 0 2>/dev/null || true
case "$rest" in
"[A") printf 'up' ;;
"[B") printf 'down' ;;
"") printf 'abort' ;; # Escape on its own
*) printf 'other' ;;
esac
return
fi
case "$k" in
k) printf 'up' ;;
j) printf 'down' ;;
q|Q) printf 'abort' ;;
"") printf 'enter' ;; # dd gives an empty string for CR
*) printf 'other' ;;
esac
}
# A pipe has nobody to ask, so it proceeds -- `sh setup < /dev/null` is a valid
# way to run this and must not stall waiting for a keystroke that cannot come.
ask() {
banner
if [ ! -t 0 ] || [ ! -t 1 ]; then
draw_choice 0
return 0
fi
stty_saved=$(stty -g 2>/dev/null) || stty_saved=""
# RESTORED WHATEVER HAPPENS. A script that leaves a terminal in raw mode is
# worse than one that fails: the shell it returns to is unusable.
trap 'if [ -n "$stty_saved" ]; then stty "$stty_saved" 2>/dev/null || true; fi' EXIT
trap 'if [ -n "$stty_saved" ]; then stty "$stty_saved" 2>/dev/null || true; fi
printf "\n"; exit 130' INT
# -icanon rather than raw: raw would also turn off ISIG, and a menu is
# not worth a terminal where Ctrl-C does nothing.
stty -echo -icanon min 1 time 0 2>/dev/null || true
sel=0
draw_choice "$sel"
while :; do
key=$(read_key)
case "$key" in
up) sel=0 ;;
down) sel=1 ;;
enter) break ;;
abort) sel=1; break ;;
*) continue ;;
esac
printf '%s[2A' "$ESC"
draw_choice "$sel"
done
if [ -n "$stty_saved" ]; then stty "$stty_saved" 2>/dev/null || true; fi
trap - INT
printf '\n'
return "$sel"
}
if ! ask; then
printf ' %s%s%sAborted -- nothing was installed%s\n' "$C_BOLD" "$C_BLACK" "$C_RED_BG" "$C_OFF"
exit 0
fi
# --- which package manager, and how it is spoken ---------------------------
#
# Ported from what the engine used to carry, minus brew: spoot is a Wayland
# client for Linux and the BSDs, and macOS is not a target.
#
# `mgr_query` is how to ask whether a name exists WITHOUT installing it. It
# matters more here than it looks: pacman and apt fail an entire transaction over
# one name they do not recognise, so a package this script guessed wrong would
# take every correct name down with it.
mgr=""; mgr_cmd=""; mgr_root=1; mgr_query=""
use_mgr() {
mgr=$1
case "$mgr" in
pacman) mgr_cmd="pacman -S --needed --noconfirm"
mgr_query="pacman -Si %s" ;;
apt-get) mgr_cmd="apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y"
mgr_query="apt-cache show %s" ;;
dnf) mgr_cmd="dnf install -y" ; mgr_query="dnf -q info %s" ;;
zypper) mgr_cmd="zypper --non-interactive install"
mgr_query="zypper -q search --match-exact %s" ;;
apk) mgr_cmd="apk add" ; mgr_query="apk search -e %s" ;;
xbps-install) mgr_cmd="xbps-install -Sy" ; mgr_query="xbps-query -R %s" ;;
emerge) mgr_cmd="emerge --quiet --noreplace" ; mgr_query="" ;;
# Into the user's own profile, so this one must NOT be run through sudo.
nix-env) mgr_cmd="nix-env -iA"; mgr_root=0 ; mgr_query="" ;;
pkg) mgr_cmd="pkg install -y" ; mgr_query="pkg search -e %s" ;; # FreeBSD
pkg_add) mgr_cmd="pkg_add" ; mgr_query="pkg_info -Q %s" ;; # Open/NetBSD
*) mgr="" ;;
esac
}
# The distribution's own name for itself is a PREFERENCE, never a requirement: it
# only reorders managers that are actually installed, so a box with both nix and
# apt gets apt. The BSDs have no os-release and fall straight through to the scan,
# which is the right answer for them.
want=""
if [ -r /etc/os-release ]; then
distro_id=$(. /etc/os-release 2>/dev/null; printf '%s' "${ID:-}")
case "$distro_id" in
arch|manjaro|endeavouros|garuda|artix|cachyos) want=pacman ;;
debian|ubuntu|linuxmint|pop|raspbian|kali|devuan) want=apt-get ;;
fedora|rhel|centos|rocky|almalinux|nobara) want=dnf ;;
opensuse*|sles) want=zypper ;;
alpine) want=apk ;;
void) want=xbps-install ;;
gentoo) want=emerge ;;
nixos) want=nix-env ;;
freebsd) want=pkg ;;
openbsd|netbsd) want=pkg_add ;;
esac
else
distro_id=""
fi
if [ -n "$want" ] && command -v "$want" >/dev/null 2>&1; then
use_mgr "$want"
else
for m in pacman apt-get dnf zypper apk xbps-install emerge nix-env pkg pkg_add; do
if command -v "$m" >/dev/null 2>&1; then use_mgr "$m"; break; fi
done
fi
# --- becoming root, and no polkit anywhere ---------------------------------
#
# This runs in a terminal, which is the whole point: sudo can simply ask. That is
# why none of these are the non-interactive forms the app used to need -- it had
# no way to answer a prompt, and a graphical dialog was the only thing left. Here
# a prompt is a question, not a hang.
priv=""
if [ "$mgr_root" -eq 1 ] && [ "$(id -u)" -ne 0 ]; then
if command -v sudo >/dev/null 2>&1; then priv="sudo"
elif command -v doas >/dev/null 2>&1; then priv="doas"
elif command -v su >/dev/null 2>&1; then priv="su-c"
fi
fi
# One shell for the whole batch, because apt's form is two commands joined by &&
# and only a shell knows what to do with that.
run_priv() {
if [ "$DRY" = 1 ]; then
printf '%s would run: %s%s\n' "$C_DIM" "$*" "$C_OFF"
return 0
fi
case "$priv" in
"") sh -c "$*" ;;
su-c) su -c "$*" ;;
*) "$priv" sh -c "$*" ;;
esac
}
# --- what spoot needs -------------------------------------------------------
#
# ONE TABLE, and it is the only description of any of this anywhere. The app used
# to carry a second one; it does not any more, which is why a dependency added
# here is added everywhere.
#
# Two halves, and both are needed before spoot runs: the floor it is BUILT with,
# and the programs it reaches for once it is running. There is no reason to
# install them at different times, and doing so is what left first-run doing
# package management.
DEPS="cmake compiler pkgconfig qtbase qtdeclarative layershell lua fontconfig font
spotifyd xdgopen openssl pkill notify songrec parec curl"
# Is it here already? One probe per dependency, and the probe is the real
# question rather than a package name: `pkg-config --exists` for the development
# packages the build links against, loading the module for cjson, and fc-list for
# a font, which has no binary to look for at all.
have_dep() {
case "$1" in
cmake) command -v cmake >/dev/null 2>&1 ;;
compiler) command -v c++ >/dev/null 2>&1 || command -v g++ >/dev/null 2>&1 \
|| command -v clang++ >/dev/null 2>&1 ;;
pkgconfig) command -v pkg-config >/dev/null 2>&1 || command -v pkgconf >/dev/null 2>&1 ;;
qtbase) pkg-config --exists Qt6Gui Qt6Network 2>/dev/null ;;
qtdeclarative) pkg-config --exists Qt6Qml Qt6Quick 2>/dev/null ;;
# LayerShellQt ships a CMake package and usually no .pc file, so the
# config file is the real probe. Confined to the handful of directories
# cmake itself looks in -- a probe that walks /usr is a probe nobody
# waits for.
layershell) pkg-config --exists LayerShellQtInterface 2>/dev/null \
|| [ -n "$(find /usr/lib /usr/lib64 /usr/local/lib /usr/share/cmake \
-maxdepth 4 -name 'LayerShellQt*Config.cmake' \
2>/dev/null | head -n 1)" ] ;;
# The interpreter AND the module in one question, because the module has
# to match the interpreter's ABI and a name cannot tell you whether it
# does. This is also the cjson probe -- there is one Lua requirement, not
# two.
lua) command -v lua >/dev/null 2>&1 && lua -e 'require("cjson")' >/dev/null 2>&1 ;;
fontconfig) command -v fc-list >/dev/null 2>&1 && command -v fc-cache >/dev/null 2>&1 ;;
font) command -v fc-list >/dev/null 2>&1 \
&& fc-list 2>/dev/null | grep -qi 'jetbrainsmono nerd' ;;
spotifyd) command -v spotifyd >/dev/null 2>&1 || command -v librespot >/dev/null 2>&1 ;;
xdgopen) command -v xdg-open >/dev/null 2>&1 ;;
openssl) command -v openssl >/dev/null 2>&1 ;;
pkill) command -v pkill >/dev/null 2>&1 ;;
notify) command -v notify-send >/dev/null 2>&1 ;;
songrec) command -v songrec >/dev/null 2>&1 ;;
parec) command -v parec >/dev/null 2>&1 ;;
curl) command -v curl >/dev/null 2>&1 ;;
*) return 0 ;;
esac
}
# What it is for, said in the one place it is decided. Printed when something is
# missing and when it cannot be installed, so nobody is left guessing why spoot
# wants an openssl.
why_dep() {
case "$1" in
cmake) printf 'the build' ;;
compiler) printf 'the build' ;;
pkgconfig) printf 'finding Qt and Lua at build time' ;;
qtbase) printf 'Qt 6, which spoot is written against' ;;
qtdeclarative) printf 'Qt Quick, which draws every menu' ;;
layershell) printf 'the layer-shell surface spoot opens on' ;;
lua) printf 'the engine, and the json module it opens with' ;;
fontconfig) printf 'finding and registering fonts' ;;
font) printf 'the glyphs the interface is built out of' ;;
spotifyd) printf 'playback itself' ;;
xdgopen) printf 'opening the login page in your browser' ;;
openssl) printf 'the login handshake' ;;
pkill) printf "stopping spoot's own background helpers" ;;
notify) printf 'desktop notifications' ;;
songrec) printf 'identifying what is playing in the room' ;;
parec) printf 'recording the room for songrec to identify' ;;
curl) printf 'requests when the engine runs outside spoot' ;;
esac
}
# The package that provides it, per manager. A blank answer means this manager
# has no name for it and the dependency is reported as a manual step instead --
# which is the honest outcome for a font on OpenBSD, and better than a batch that
# fails as a whole.
pkg_for() {
case "$1" in
cmake) printf 'cmake' ;;
compiler)
case "$mgr" in
pacman) printf 'gcc' ;; apt-get) printf 'g++' ;;
dnf|zypper) printf 'gcc-c++' ;; apk) printf 'g++' ;;
xbps-install) printf 'gcc' ;; nix-env) printf 'gcc' ;;
emerge) printf '' ;; pkg|pkg_add) printf '' ;; # in base
esac ;;
pkgconfig)
case "$mgr" in
pacman) printf 'pkgconf' ;; apt-get) printf 'pkg-config' ;;
dnf) printf 'pkgconf-pkg-config' ;; zypper) printf 'pkg-config' ;;
apk) printf 'pkgconf' ;; xbps-install) printf 'pkg-config' ;;
nix-env) printf 'pkg-config' ;; pkg) printf 'pkgconf' ;;
pkg_add) printf 'pkgconf' ;; emerge) printf 'dev-util/pkgconf' ;;
esac ;;
qtbase)
case "$mgr" in
pacman) printf 'qt6-base' ;; apt-get) printf 'qt6-base-dev' ;;
dnf) printf 'qt6-qtbase-devel' ;; zypper) printf 'qt6-base-devel' ;;
apk) printf 'qt6-qtbase-dev' ;; xbps-install) printf 'qt6-base-devel' ;;
nix-env) printf 'qt6.qtbase' ;; pkg) printf 'qt6-base' ;;
pkg_add) printf 'qt6' ;; emerge) printf 'dev-qt/qtbase' ;;
esac ;;
qtdeclarative)
case "$mgr" in
pacman) printf 'qt6-declarative' ;; apt-get) printf 'qt6-declarative-dev' ;;
dnf) printf 'qt6-qtdeclarative-devel' ;; zypper) printf 'qt6-declarative-devel' ;;
apk) printf 'qt6-qtdeclarative-dev' ;; xbps-install) printf 'qt6-declarative-devel' ;;
nix-env) printf 'qt6.qtdeclarative' ;; pkg) printf 'qt6-declarative' ;;
pkg_add) printf '' ;; emerge) printf 'dev-qt/qtdeclarative' ;;
esac ;;
layershell)
case "$mgr" in
pacman) printf 'layer-shell-qt' ;; apt-get) printf 'liblayershellqtinterface-dev' ;;
dnf) printf 'layer-shell-qt-devel' ;; zypper) printf 'layer-shell-qt-devel' ;;
apk) printf 'layer-shell-qt-dev' ;; xbps-install) printf 'layer-shell-qt-devel' ;;
nix-env) printf 'layer-shell-qt' ;; emerge) printf 'kde-plasma/layer-shell-qt' ;;
*) printf '' ;;
esac ;;
lua)
case "$mgr" in
pacman) printf 'lua' ;; # cjson is AUR-only, below
apt-get) printf 'lua5.4 lua-cjson' ;;
dnf) printf 'lua lua-cjson' ;; zypper) printf 'lua54 lua54-luajson' ;;
apk) printf 'lua5.4 lua5.4-cjson' ;;
xbps-install) printf 'lua54 lua54-cjson' ;;
nix-env) printf 'lua luaPackages.cjson' ;;
pkg) printf 'lua54 lua54-cjson' ;; pkg_add) printf 'lua' ;;
emerge) printf 'dev-lang/lua dev-lua/lua-cjson' ;;
esac ;;
fontconfig)
case "$mgr" in
pacman) printf 'fontconfig' ;; apt-get) printf 'fontconfig' ;;
dnf) printf 'fontconfig' ;; zypper) printf 'fontconfig' ;;
apk) printf 'fontconfig' ;; xbps-install) printf 'fontconfig' ;;
nix-env) printf 'fontconfig' ;; pkg|pkg_add) printf 'fontconfig' ;;
emerge) printf 'media-libs/fontconfig' ;;
esac ;;
# ONLY WHERE THE PATCHED FACE IS PACKAGED. Debian and Ubuntu ship the plain
# JetBrains Mono, which is the same font WITHOUT the glyphs the interface is
# made of -- installing it would satisfy a package manager and leave the UI
# full of empty boxes. Blank here sends it to the download below instead.
font)
case "$mgr" in
pacman) printf 'ttf-jetbrains-mono-nerd' ;;
dnf) printf 'jetbrainsmono-nerd-fonts' ;;
xbps-install) printf 'nerd-fonts-ttf' ;;
nix-env) printf 'nerdfonts' ;;
*) printf '' ;;
esac ;;
spotifyd)
case "$mgr" in
pacman) printf 'spotifyd' ;; nix-env) printf 'spotifyd' ;;
xbps-install) printf 'spotifyd' ;; apk) printf 'spotifyd' ;;
pkg) printf 'spotifyd' ;; emerge) printf 'media-sound/spotifyd' ;;
*) printf '' ;;
esac ;;
xdgopen)
case "$mgr" in
pacman) printf 'xdg-utils' ;; emerge) printf 'x11-misc/xdg-utils' ;;
*) printf 'xdg-utils' ;;
esac ;;
openssl) case "$mgr" in emerge) printf 'dev-libs/openssl' ;; *) printf 'openssl' ;; esac ;;
pkill)
case "$mgr" in
pacman) printf 'procps-ng' ;; apt-get) printf 'procps' ;;
dnf) printf 'procps-ng' ;; zypper) printf 'procps' ;;
apk) printf 'procps' ;; xbps-install) printf 'procps-ng' ;;
nix-env) printf 'procps' ;; emerge) printf 'sys-process/procps' ;;
*) printf '' ;; # BSD has it in base
esac ;;
notify)
case "$mgr" in
apt-get) printf 'libnotify-bin' ;; zypper) printf 'libnotify-tools' ;;
emerge) printf 'x11-libs/libnotify' ;; *) printf 'libnotify' ;;
esac ;;
songrec)
case "$mgr" in
pacman) printf 'songrec' ;; nix-env) printf 'songrec' ;;
*) printf '' ;;
esac ;;
# Whatever ships pactl: libpulse on Arch, the -utils split elsewhere. Only
# the Arch name is confirmed on a real system; mgr_query checks the rest
# before anything is installed.
parec)
case "$mgr" in
pacman) printf 'libpulse' ;; emerge) printf 'media-libs/libpulse' ;;
apt-get|dnf|zypper|apk|xbps-install) printf 'pulseaudio-utils' ;;
nix-env|pkg|pkg_add) printf 'pulseaudio' ;;
*) printf '' ;;
esac ;;
curl) case "$mgr" in emerge) printf 'net-misc/curl' ;; *) printf 'curl' ;; esac ;;
esac
}
# Does this manager actually carry the name? A manager with no query answers yes
# -- attempting an install and reporting what it said beats refusing to try.
pkg_exists() {
[ -n "$mgr_query" ] || return 0
tool=${mgr_query%% *}
command -v "$tool" >/dev/null 2>&1 || return 0
# shellcheck disable=SC2059
q=$(printf "$mgr_query" "$1")
sh -c "$q" >/dev/null 2>&1
}
# --- the scan ---------------------------------------------------------------
say "checking what is already here"
missing=""; batch=""; manual=""
for d in $DEPS; do
if have_dep "$d"; then
[ "$DRY" = 1 ] && step "present $d"
continue
fi
missing="$missing $d"
names=$(pkg_for "$d")
got=""
for n in $names; do
if [ -n "$mgr" ] && pkg_exists "$n"; then
got="$got $n"
fi
done
if [ -n "$got" ]; then
batch="$batch$got"
[ "$DRY" = 1 ] && step "MISSING $d ->$got"
else
manual="$manual $d"
[ "$DRY" = 1 ] && step "MISSING $d -> no package (manual)"
fi
done
if [ -z "$missing" ]; then
say "nothing to install -- everything spoot needs is already here"
else
printf '%s missing:%s%s\n' "$C_DIM" "$missing" "$C_OFF"
fi
# --- the install ------------------------------------------------------------
if [ -n "$batch" ]; then
if [ -z "$mgr" ]; then
warn "no package manager recognised -- install these and run setup again:$missing"
else
args=""
for p in $batch; do
if [ "$mgr" = nix-env ]; then args="$args nixpkgs.$p"; else args="$args $p"; fi
done
say "installing with $mgr:$args"
# NOT quiet, and not captured. Someone is looking at this terminal, a
# package manager is exactly the thing they want to watch, and sudo may
# need to ask them something.
run_priv "$mgr_cmd$args" \
|| warn "the package manager reported a failure -- carrying on, the probes below are the real test"
fi
fi
# --- lua-cjson, where no repository has it ---------------------------------
#
# Arch keeps it in the AUR, so the batch above cannot include it. An AUR helper
# is never run through sudo: makepkg refuses to build as root, and that refusal
# is correct.
if ! have_dep lua; then
say "installing lua-cjson"
if [ "$DRY" = 1 ]; then
step "would try: paru/yay -S lua-cjson, or luarocks install lua-cjson"
elif command -v paru >/dev/null 2>&1; then paru -S --needed --noconfirm lua-cjson || true
elif command -v yay >/dev/null 2>&1; then yay -S --needed --noconfirm lua-cjson || true
elif command -v luarocks >/dev/null 2>&1; then run_priv "luarocks install lua-cjson" || true
fi
fi
# --- the font, guaranteed ---------------------------------------------------
#
# The interface is built out of nerd-font glyphs -- the transport marks, the
# status icons, the prompt -- so this is not decoration and a distribution that
# does not package the patched face cannot be left to it. Into the user's own
# font directory, which needs no privilege and touches nothing outside $HOME.
FONT_URL="https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip"
FONT_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/fonts/JetBrainsMonoNerd"
if ! have_dep font; then
if [ "$DRY" = 1 ]; then
step "would download $FONT_URL -> $FONT_DIR"
else
say "fetching JetBrains Mono Nerd Font"
tmp=$(mktemp -d 2>/dev/null || printf '/tmp/spoot-font.%s' $$)
mkdir -p "$tmp" "$FONT_DIR"
ok=1
if command -v curl >/dev/null 2>&1; then curl -fsSL "$FONT_URL" -o "$tmp/f.zip" || ok=0
elif command -v wget >/dev/null 2>&1; then wget -q "$FONT_URL" -O "$tmp/f.zip" || ok=0
elif command -v ftp >/dev/null 2>&1; then ftp -o "$tmp/f.zip" "$FONT_URL" || ok=0
else ok=0; warn "no curl, wget or ftp to download the font with"
fi
# ONLY THE FACES THE INTERFACE ASKS FOR. The release archive is every
# width and style of the family -- 96 files, 233MB on disk -- and
# Theme.qml names exactly one of them: "JetBrainsMono Nerd Font Propo".
# Taking the Propo faces alone is a third of that, and dropping a
# quarter of a gigabyte of fonts nobody selected on someone's machine is
# not a reasonable thing for an installer to do.
if [ "$ok" = 1 ]; then
if command -v unzip >/dev/null 2>&1; then
unzip -qo "$tmp/f.zip" '*Propo*' -d "$FONT_DIR" 2>/dev/null \
|| unzip -qo "$tmp/f.zip" -d "$FONT_DIR" || ok=0
elif command -v bsdtar >/dev/null 2>&1; then
bsdtar -xf "$tmp/f.zip" -C "$FONT_DIR" '*Propo*' 2>/dev/null \
|| bsdtar -xf "$tmp/f.zip" -C "$FONT_DIR" || ok=0
else ok=0; warn "no unzip or bsdtar to unpack the font with"
fi
fi
rm -rf "$tmp"
[ "$ok" = 1 ] && command -v fc-cache >/dev/null 2>&1 && fc-cache -f "$FONT_DIR" >/dev/null 2>&1
# Asked again rather than assumed: a download that unpacked into the
# wrong place is a download that did not work.
if have_dep font; then
say "font installed into $FONT_DIR"
else
warn "the nerd font is still missing -- the interface will draw empty"
warn "boxes where its icons are. Install it from $FONT_URL"
fi
fi
fi
# --- what could not be installed -------------------------------------------
left=""
for d in $missing; do
have_dep "$d" || left="$left $d"
done
if [ -n "$left" ]; then
warn "still missing after the install:"
for d in $left; do
printf '%s %-14s %s%s\n' "$C_WARN" "$d" "$(why_dep "$d")" "$C_OFF" >&2
done
warn "spoot may still build and run -- the build below is the real test"
fi
if [ "$DRY" = 1 ]; then
say "dry run -- nothing was installed, built or removed"
exit 0
fi
# --- build ------------------------------------------------------------------
#
# THIS is the verification. Package names above are best-effort per distribution;
# cmake either finds Qt6 and LayerShellQt or it does not, and what it prints when
# it does not is more useful than anything this script could guess.
#
# A BUILD DIRECTORY FROM SOMEWHERE ELSE IS NOT A BUILD DIRECTORY. CMakeCache.txt
# records the absolute path it was generated for, so one that arrives with a
# download makes cmake refuse before it does anything, naming a path that means
# nothing on this computer. Removed rather than reported.
if [ -f build/CMakeCache.txt ] \
&& ! grep -qxF "CMAKE_HOME_DIRECTORY:INTERNAL=$here" build/CMakeCache.txt; then
say "discarding a build directory generated for another path"
rm -rf build
fi
say "building"
if ! cmake -S . -B build; then
die "cmake could not configure -- it needs Qt 6 (Gui, Qml, Quick, Network),
LayerShellQt and Lua. The message above names which one it could not find."
fi
cmake --build build -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)"
# A GitHub source download is a zip, and a zip carries no permission bits -- so
# every script in the tree arrives unexecutable, this one included.
chmod +x bin/spoot 2>/dev/null || true
chmod +x engine/smoke.sh engine/views.sh ui/check.sh setup 2>/dev/null || true
# THE BUILD TREE IS SCAFFOLDING. It is several hundred megabytes of object files
# whose only product is bin/spoot, and it is regenerated by the line above
# whenever it is needed again. The cost is that every run of this script is a
# full rebuild, which is what "Compile the spoot daemon binary" in the banner
# says it will do.
say "removing the build tree"
rm -rf build
say "built $here/bin/spoot"
say "bind that path to a key -- a second run hands over to the first and exits"
# --- a face and a name ------------------------------------------------------
#
# spoot is summoned with a key and has never needed a launcher entry -- but
# without one it has no ICON either, and a notification server looking up the
# `desktop-entry` hint every toast carries finds nothing to draw. That is the same
# file answering both questions, so it is installed even though nobody has to
# click it.
#
# Into the user's own data dirs, not /usr: this script installs a tree wherever it
# was unpacked and asks for no root anywhere else.
#
# Exec is the absolute path to THIS checkout, for the reason the banner gives
# about binding a key: there is no `spoot` on PATH to fall back on.
#
# AT MORE THAN ONE SIZE, and this is "the icon shows up for me and for nobody
# else". The face was installed into hicolor/512x512 alone -- the file's true
# size, and the honest place for it -- but 512x512 is a LATE addition to the
# hicolor theme's index, and an icon in a directory the index does not declare is
# an icon the lookup cannot see. On a machine whose hicolor-icon-theme predates
# it, `Icon=spoot` resolves to nothing and the launcher draws its fallback. The
# entry was never the problem and neither were its paths: nothing here is written
# out, `$here` is where this script found itself and the rest is XDG.
#
# So it goes in at the sizes every theme has always declared as well. Downscaled
# where there is something to downscale with, copied as-is where there is not --
# a 512px file in a 48x48 directory is a little untidy and every implementation
# loads and scales it, which is the difference between an icon that is slightly
# too big and no icon at all.
say "installing the launcher entry"
share_dir="${XDG_DATA_HOME:-$HOME/.local/share}"
app_dir="$share_dir/applications"
mkdir -p "$app_dir"
if [ ! -f "$here/assets/spoot.png" ]; then
# LOUDLY. This was `2>/dev/null || true`, so a checkout missing its assets
# installed a .desktop naming an icon that was never copied and said it had
# installed the launcher entry.
warn "assets/spoot.png is missing -- the launcher entry will have no icon"
else
for px in 512 256 128 64 48; do
icon_dir="$share_dir/icons/hicolor/${px}x${px}/apps"
mkdir -p "$icon_dir" || continue
# `command -v`, not have_dep: that function is a roster of spoot's actual
# dependencies and a scaler is not one -- it is only nice to have.
if [ "$px" = 512 ] || ! command -v magick >/dev/null 2>&1; then
cp -f "$here/assets/spoot.png" "$icon_dir/spoot.png" \
|| warn "could not write $icon_dir/spoot.png"
else
magick "$here/assets/spoot.png" -resize "${px}x${px}" \
"$icon_dir/spoot.png" 2>/dev/null \
|| cp -f "$here/assets/spoot.png" "$icon_dir/spoot.png" \
|| warn "could not write $icon_dir/spoot.png"
fi
done
fi
cat > "$app_dir/spoot.desktop" <<DESKTOP
[Desktop Entry]
Type=Application
Name=spoot
GenericName=Spotify Client
Comment=Spotify, as a menu you summon with a key
Exec=$here/bin/spoot
Icon=spoot
Terminal=false
Categories=AudioVideo;Audio;Player;
Keywords=spotify;music;player;
StartupWMClass=spoot
Actions=Listen;
[Desktop Action Listen]
Name=Identify what is playing
Exec=$here/bin/spoot --listen
DESKTOP
# BEST EFFORT, both of them. The caches make the entry and the icon visible to a
# launcher sooner; neither is required for the files to work, and a machine
# without them is not a machine with a broken install.
#
# `command -v` rather than have_dep: that function is a roster of spoot's actual
# dependencies, answering by NAME, and neither of these is one -- they are nice to
# have run if they happen to exist.
command -v update-desktop-database >/dev/null 2>&1 \
&& update-desktop-database "$app_dir" >/dev/null 2>&1 || true
command -v gtk-update-icon-cache >/dev/null 2>&1 \
&& gtk-update-icon-cache -qtf \
"$share_dir/icons/hicolor" >/dev/null 2>&1 || true
# Plasma reads neither of the above: its menu is built from its own sycoca
# database, which is why a fresh entry can be invisible there until the next
# login. Whichever major version is installed, or neither.
for _k in kbuildsycoca6 kbuildsycoca5; do
command -v "$_k" >/dev/null 2>&1 && "$_k" >/dev/null 2>&1 && break
done || true
# --- over to spoot ----------------------------------------------------------
#
# STARTED AND LET GO. This used to `exec ./bin/spoot`, which made the installer
# BECOME spoot -- so the terminal you ran it in was occupied for as long as spoot
# lived, and the script never finished from where you were standing.
#
# spoot is a resident daemon you summon with a key, not a foreground program, so
# it is detached and this returns. nohup and the redirections are what let it
# outlive the shell: without them it dies with the terminal, and a first run that
# ends by closing the thing it just started has installed nothing you can use.
# --- whose rate limit this install spends -----------------------------------
#
# spoot ships with one Spotify app id compiled into it, and Spotify's rate limit
# belongs to the APP rather than to your account -- so every install sharing that
# id is sharing one pool of requests. When it runs out you get 429 on everything,
# including a cold start where you have done nothing at all, and there is no
# amount of good behaviour on this machine that gets it back.
#
# Said rather than asked. This script installs and gets out of the way; a prompt
# here would block an unattended run for something that is optional and takes two
# minutes to do later.
if [ "$("$here/bin/spoot" --client-id 2>/dev/null | grep -c shared)" != 0 ]; then
say "using the shared Spotify app id -- its rate limit is shared too"
say "for a quota of your own: make an app at developer.spotify.com/dashboard"
say "with redirect URI http://127.0.0.1:8989/login, then run"
say " $here/bin/spoot --client-id <your client id>"
fi
say "starting spoot"
nohup ./bin/spoot >/dev/null 2>&1 </dev/null &
say "done -- bind $here/bin/spoot to a key and press it"
exit 0