pingthings 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/bin/pingthings.js +6 -0
- package/package.json +1 -1
- package/packs/fighting-announcer/manifest.json +62 -0
- package/packs/fighting-announcer/sounds/begin.wav +0 -0
- package/packs/fighting-announcer/sounds/complete.wav +0 -0
- package/packs/fighting-announcer/sounds/congratulations.wav +0 -0
- package/packs/fighting-announcer/sounds/defeat.wav +0 -0
- package/packs/fighting-announcer/sounds/failure.wav +0 -0
- package/packs/fighting-announcer/sounds/fight.wav +0 -0
- package/packs/fighting-announcer/sounds/final-round.wav +0 -0
- package/packs/fighting-announcer/sounds/finish.wav +0 -0
- package/packs/fighting-announcer/sounds/game-over.wav +0 -0
- package/packs/fighting-announcer/sounds/get-set.wav +0 -0
- package/packs/fighting-announcer/sounds/go.wav +0 -0
- package/packs/fighting-announcer/sounds/great.wav +0 -0
- package/packs/fighting-announcer/sounds/high-score.wav +0 -0
- package/packs/fighting-announcer/sounds/new-record.wav +0 -0
- package/packs/fighting-announcer/sounds/no-contest.wav +0 -0
- package/packs/fighting-announcer/sounds/ready.wav +0 -0
- package/packs/fighting-announcer/sounds/sudden-death.wav +0 -0
- package/packs/fighting-announcer/sounds/victory.wav +0 -0
- package/packs/fighting-announcer/sounds/you-lose.wav +0 -0
- package/packs/fighting-announcer/sounds/you-win.wav +0 -0
- package/packs/wesnoth-combat/manifest.json +1 -1
- package/packs/xonotic-announcer/manifest.json +52 -0
- package/packs/xonotic-announcer/sounds/1fragleft.wav +0 -0
- package/packs/xonotic-announcer/sounds/airshot.wav +0 -0
- package/packs/xonotic-announcer/sounds/amazing.wav +0 -0
- package/packs/xonotic-announcer/sounds/awesome.wav +0 -0
- package/packs/xonotic-announcer/sounds/begin.wav +0 -0
- package/packs/xonotic-announcer/sounds/botlike.wav +0 -0
- package/packs/xonotic-announcer/sounds/electrobitch.wav +0 -0
- package/packs/xonotic-announcer/sounds/headshot.wav +0 -0
- package/packs/xonotic-announcer/sounds/impressive.wav +0 -0
- package/packs/xonotic-announcer/sounds/lastsecond.wav +0 -0
- package/packs/xonotic-announcer/sounds/leadgained.wav +0 -0
- package/packs/xonotic-announcer/sounds/narrowly.wav +0 -0
- package/packs/xonotic-announcer/sounds/prepareforbattle.wav +0 -0
- package/packs/xonotic-announcer/sounds/terminated.wav +0 -0
- package/packs/xonotic-announcer/sounds/yoda.wav +0 -0
- package/src/cli/completions.js +182 -0
- package/src/cli/config.js +32 -3
- package/src/cli/create.js +1 -1
- package/src/cli/doctor.js +134 -0
- package/src/cli/init.js +11 -8
- package/src/cli/play.js +22 -1
- package/src/cli/random-pack.js +44 -0
- package/src/cli/select.js +36 -32
- package/src/cli/uninstall.js +5 -0
- package/src/config.js +30 -0
- package/src/player.js +17 -12
package/README.md
CHANGED
|
@@ -137,8 +137,11 @@ For different sounds based on what Claude is doing, set up multiple hooks:
|
|
|
137
137
|
| `pingthings browse [category]` | Browse packs by category |
|
|
138
138
|
| `pingthings search <term>` | Search packs and sounds |
|
|
139
139
|
| `pingthings sounds [pack]` | List individual sounds in a pack |
|
|
140
|
+
| `pingthings random-pack` | Switch to a random pack |
|
|
140
141
|
| `pingthings install <source>` | Install a pack from GitHub or local path |
|
|
141
142
|
| `pingthings uninstall <pack>` | Remove a user-installed pack |
|
|
143
|
+
| `pingthings doctor` | Diagnose audio setup and configuration |
|
|
144
|
+
| `pingthings completions <shell>` | Generate shell completions (bash/zsh/fish) |
|
|
142
145
|
|
|
143
146
|
## Configuration
|
|
144
147
|
|
|
@@ -231,6 +234,12 @@ Clean modern UI notification sounds by **Kenney** — 18 sounds including confir
|
|
|
231
234
|
### kenney-scifi
|
|
232
235
|
Futuristic sci-fi notification sounds by **Kenney** — 18 sounds including computer noises, force fields, lasers, explosions, and thrusters. License: CC0.
|
|
233
236
|
|
|
237
|
+
### xonotic-announcer
|
|
238
|
+
Arena FPS announcer voice lines from **Xonotic** — 15 sounds including "awesome!", "amazing!", "impressive!", "prepare for battle!", "terminated!". License: GPL v2+.
|
|
239
|
+
|
|
240
|
+
### fighting-announcer
|
|
241
|
+
Fighting game announcer voice lines — 20 sounds including "Fight!", "Victory!", "K.O!", "Game Over!", "Ready?", "You Win!". License: CC-BY 4.0.
|
|
242
|
+
|
|
234
243
|
## Custom packs
|
|
235
244
|
|
|
236
245
|
Place packs in `~/.config/pingthings/packs/<pack-name>/`:
|
|
@@ -291,6 +300,9 @@ pingthings theme reset # back to defaults
|
|
|
291
300
|
| `arena` | Arena announcer with FPS weapons for errors |
|
|
292
301
|
| `fantasy` | Medieval fantasy — Wesnoth + 0 A.D. civilizations |
|
|
293
302
|
| `ancient` | Ancient world — 7kaa soldiers + 0 A.D. voices |
|
|
303
|
+
| `professional` | Clean and minimal — Kenney UI sounds for everything |
|
|
304
|
+
| `8bit` | Pure retro — 8-bit chiptune for everything |
|
|
305
|
+
| `space` | Space station — Warzone 2100 + Kenney sci-fi |
|
|
294
306
|
| `chaos` | Different pack for every event — maximum variety |
|
|
295
307
|
|
|
296
308
|
## Tools
|
package/bin/pingthings.js
CHANGED
|
@@ -26,6 +26,9 @@ const commands = {
|
|
|
26
26
|
create: () => import('../src/cli/create.js'),
|
|
27
27
|
theme: () => import('../src/cli/theme.js'),
|
|
28
28
|
'test-events': () => import('../src/cli/test-events.js'),
|
|
29
|
+
'random-pack': () => import('../src/cli/random-pack.js'),
|
|
30
|
+
doctor: () => import('../src/cli/doctor.js'),
|
|
31
|
+
completions: () => import('../src/cli/completions.js'),
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
function showHelp() {
|
|
@@ -47,9 +50,12 @@ Commands:
|
|
|
47
50
|
theme [name] Apply a sound theme (maps events across packs)
|
|
48
51
|
config [key] [val] Show or update configuration
|
|
49
52
|
init Set up Claude Code hooks automatically
|
|
53
|
+
random-pack Switch to a random pack
|
|
50
54
|
create <dir> Create a new pack from a folder of audio files
|
|
51
55
|
install <source> Install a pack from GitHub or URL
|
|
52
56
|
uninstall <pack> Remove a user-installed pack
|
|
57
|
+
doctor Diagnose audio setup and configuration
|
|
58
|
+
completions <shell> Generate shell completions (bash/zsh/fish)
|
|
53
59
|
|
|
54
60
|
Options:
|
|
55
61
|
--help, -h Show this help message
|
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fighting-announcer",
|
|
3
|
+
"description": "Fighting game announcer voice lines",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"license": "CC-BY-4.0",
|
|
6
|
+
"credits": "Alba MacKenna (https://opengameart.org/content/voice-pack%E2%94%82fighting-game-announcer)",
|
|
7
|
+
"category": "arena",
|
|
8
|
+
"sounds": [
|
|
9
|
+
"sounds/begin.wav",
|
|
10
|
+
"sounds/complete.wav",
|
|
11
|
+
"sounds/congratulations.wav",
|
|
12
|
+
"sounds/defeat.wav",
|
|
13
|
+
"sounds/failure.wav",
|
|
14
|
+
"sounds/fight.wav",
|
|
15
|
+
"sounds/final-round.wav",
|
|
16
|
+
"sounds/finish.wav",
|
|
17
|
+
"sounds/game-over.wav",
|
|
18
|
+
"sounds/get-set.wav",
|
|
19
|
+
"sounds/go.wav",
|
|
20
|
+
"sounds/great.wav",
|
|
21
|
+
"sounds/high-score.wav",
|
|
22
|
+
"sounds/new-record.wav",
|
|
23
|
+
"sounds/no-contest.wav",
|
|
24
|
+
"sounds/ready.wav",
|
|
25
|
+
"sounds/sudden-death.wav",
|
|
26
|
+
"sounds/victory.wav",
|
|
27
|
+
"sounds/you-lose.wav",
|
|
28
|
+
"sounds/you-win.wav"
|
|
29
|
+
],
|
|
30
|
+
"events": {
|
|
31
|
+
"done": [
|
|
32
|
+
"sounds/complete.wav",
|
|
33
|
+
"sounds/great.wav",
|
|
34
|
+
"sounds/finish.wav",
|
|
35
|
+
"sounds/new-record.wav"
|
|
36
|
+
],
|
|
37
|
+
"permission": [
|
|
38
|
+
"sounds/fight.wav",
|
|
39
|
+
"sounds/ready.wav",
|
|
40
|
+
"sounds/get-set.wav",
|
|
41
|
+
"sounds/begin.wav",
|
|
42
|
+
"sounds/go.wav"
|
|
43
|
+
],
|
|
44
|
+
"complete": [
|
|
45
|
+
"sounds/victory.wav",
|
|
46
|
+
"sounds/you-win.wav",
|
|
47
|
+
"sounds/congratulations.wav",
|
|
48
|
+
"sounds/high-score.wav"
|
|
49
|
+
],
|
|
50
|
+
"error": [
|
|
51
|
+
"sounds/game-over.wav",
|
|
52
|
+
"sounds/defeat.wav",
|
|
53
|
+
"sounds/you-lose.wav",
|
|
54
|
+
"sounds/failure.wav"
|
|
55
|
+
],
|
|
56
|
+
"blocked": [
|
|
57
|
+
"sounds/sudden-death.wav",
|
|
58
|
+
"sounds/final-round.wav",
|
|
59
|
+
"sounds/no-contest.wav"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "wesnoth-combat",
|
|
3
3
|
"description": "Fantasy combat and magic sounds from Battle for Wesnoth",
|
|
4
4
|
"version": "1.0.0",
|
|
5
|
-
"license": "GPL-2.0",
|
|
5
|
+
"license": "GPL-2.0-or-later",
|
|
6
6
|
"credits": "Battle for Wesnoth contributors (https://github.com/wesnoth/wesnoth) — licensed under GPL v2+",
|
|
7
7
|
"category": "fantasy",
|
|
8
8
|
"sounds": [
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xonotic-announcer",
|
|
3
|
+
"description": "Arena FPS announcer voice lines from Xonotic",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"license": "GPL-2.0-or-later",
|
|
6
|
+
"credits": "Xonotic Project (https://github.com/xonotic/xonotic-data.pk3dir)",
|
|
7
|
+
"category": "arena",
|
|
8
|
+
"sounds": [
|
|
9
|
+
"sounds/1fragleft.wav",
|
|
10
|
+
"sounds/airshot.wav",
|
|
11
|
+
"sounds/amazing.wav",
|
|
12
|
+
"sounds/awesome.wav",
|
|
13
|
+
"sounds/begin.wav",
|
|
14
|
+
"sounds/botlike.wav",
|
|
15
|
+
"sounds/electrobitch.wav",
|
|
16
|
+
"sounds/headshot.wav",
|
|
17
|
+
"sounds/impressive.wav",
|
|
18
|
+
"sounds/lastsecond.wav",
|
|
19
|
+
"sounds/leadgained.wav",
|
|
20
|
+
"sounds/narrowly.wav",
|
|
21
|
+
"sounds/prepareforbattle.wav",
|
|
22
|
+
"sounds/terminated.wav",
|
|
23
|
+
"sounds/yoda.wav"
|
|
24
|
+
],
|
|
25
|
+
"events": {
|
|
26
|
+
"done": [
|
|
27
|
+
"sounds/awesome.wav",
|
|
28
|
+
"sounds/amazing.wav",
|
|
29
|
+
"sounds/impressive.wav",
|
|
30
|
+
"sounds/airshot.wav"
|
|
31
|
+
],
|
|
32
|
+
"permission": [
|
|
33
|
+
"sounds/prepareforbattle.wav",
|
|
34
|
+
"sounds/begin.wav",
|
|
35
|
+
"sounds/1fragleft.wav"
|
|
36
|
+
],
|
|
37
|
+
"complete": [
|
|
38
|
+
"sounds/yoda.wav",
|
|
39
|
+
"sounds/botlike.wav",
|
|
40
|
+
"sounds/leadgained.wav"
|
|
41
|
+
],
|
|
42
|
+
"error": [
|
|
43
|
+
"sounds/terminated.wav",
|
|
44
|
+
"sounds/headshot.wav",
|
|
45
|
+
"sounds/electrobitch.wav"
|
|
46
|
+
],
|
|
47
|
+
"blocked": [
|
|
48
|
+
"sounds/lastsecond.wav",
|
|
49
|
+
"sounds/narrowly.wav"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
function showHelp() {
|
|
2
|
+
console.log(`
|
|
3
|
+
Usage: pingthings completions <shell>
|
|
4
|
+
|
|
5
|
+
Generate shell completion script.
|
|
6
|
+
|
|
7
|
+
Arguments:
|
|
8
|
+
shell bash, zsh, or fish
|
|
9
|
+
|
|
10
|
+
Install:
|
|
11
|
+
pingthings completions bash >> ~/.bashrc
|
|
12
|
+
pingthings completions zsh >> ~/.zshrc
|
|
13
|
+
pingthings completions fish > ~/.config/fish/completions/pingthings.fish
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const COMMANDS = [
|
|
18
|
+
'play', 'list', 'select', 'browse', 'search', 'sounds',
|
|
19
|
+
'use', 'preview', 'test-events', 'theme', 'config',
|
|
20
|
+
'init', 'create', 'install', 'uninstall', 'random-pack', 'doctor', 'completions',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const THEMES = [
|
|
24
|
+
'retro', 'sci-fi', 'arena', 'fantasy', 'ancient',
|
|
25
|
+
'professional', '8bit', 'space', 'chaos', 'reset',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const EVENTS = ['done', 'permission', 'complete', 'error', 'blocked'];
|
|
29
|
+
|
|
30
|
+
function bash() {
|
|
31
|
+
return `# pingthings bash completions
|
|
32
|
+
_pingthings() {
|
|
33
|
+
local cur prev commands
|
|
34
|
+
COMPREPLY=()
|
|
35
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
36
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
37
|
+
commands="${COMMANDS.join(' ')}"
|
|
38
|
+
|
|
39
|
+
case "\${prev}" in
|
|
40
|
+
pingthings)
|
|
41
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
42
|
+
return 0
|
|
43
|
+
;;
|
|
44
|
+
use|preview|sounds|test-events|uninstall)
|
|
45
|
+
local packs=$(pingthings list 2>/dev/null | grep -oE '\\S+-\\S+' | head -20)
|
|
46
|
+
COMPREPLY=( $(compgen -W "\${packs}" -- "\${cur}") )
|
|
47
|
+
return 0
|
|
48
|
+
;;
|
|
49
|
+
theme)
|
|
50
|
+
COMPREPLY=( $(compgen -W "${THEMES.join(' ')}" -- "\${cur}") )
|
|
51
|
+
return 0
|
|
52
|
+
;;
|
|
53
|
+
--event|-e)
|
|
54
|
+
COMPREPLY=( $(compgen -W "${EVENTS.join(' ')}" -- "\${cur}") )
|
|
55
|
+
return 0
|
|
56
|
+
;;
|
|
57
|
+
browse)
|
|
58
|
+
local cats="military arena fantasy sci-fi fps retro ui transport spooky racing"
|
|
59
|
+
COMPREPLY=( $(compgen -W "\${cats}" -- "\${cur}") )
|
|
60
|
+
return 0
|
|
61
|
+
;;
|
|
62
|
+
completions)
|
|
63
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
|
|
64
|
+
return 0
|
|
65
|
+
;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
complete -F _pingthings pingthings`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function zsh() {
|
|
74
|
+
return `# pingthings zsh completions
|
|
75
|
+
_pingthings() {
|
|
76
|
+
local -a commands themes events
|
|
77
|
+
commands=(
|
|
78
|
+
'play:Play a sound from the active pack'
|
|
79
|
+
'list:Show available sound packs'
|
|
80
|
+
'select:Interactive pack selector'
|
|
81
|
+
'browse:Browse packs by category'
|
|
82
|
+
'search:Search packs and sounds'
|
|
83
|
+
'sounds:List individual sounds in a pack'
|
|
84
|
+
'use:Set the active sound pack'
|
|
85
|
+
'preview:Preview a random sound from a pack'
|
|
86
|
+
'test-events:Play all event sounds'
|
|
87
|
+
'theme:Apply a sound theme'
|
|
88
|
+
'config:Show or update configuration'
|
|
89
|
+
'init:Set up Claude Code hooks'
|
|
90
|
+
'create:Create a pack from audio files'
|
|
91
|
+
'install:Install a pack from GitHub or local path'
|
|
92
|
+
'uninstall:Remove a user-installed pack'
|
|
93
|
+
'random-pack:Switch to a random pack'
|
|
94
|
+
'doctor:Diagnose audio setup'
|
|
95
|
+
'completions:Generate shell completions'
|
|
96
|
+
)
|
|
97
|
+
themes=(${THEMES.join(' ')})
|
|
98
|
+
events=(${EVENTS.join(' ')})
|
|
99
|
+
|
|
100
|
+
_arguments '1:command:->cmds' '*::arg:->args'
|
|
101
|
+
|
|
102
|
+
case "$state" in
|
|
103
|
+
cmds)
|
|
104
|
+
_describe -t commands 'pingthings command' commands
|
|
105
|
+
;;
|
|
106
|
+
args)
|
|
107
|
+
case $words[1] in
|
|
108
|
+
use|preview|sounds|test-events|uninstall)
|
|
109
|
+
local packs=($(pingthings list 2>/dev/null | grep -oE '\\S+-\\S+' | head -20))
|
|
110
|
+
_describe -t packs 'pack' packs
|
|
111
|
+
;;
|
|
112
|
+
theme)
|
|
113
|
+
_describe -t themes 'theme' themes
|
|
114
|
+
;;
|
|
115
|
+
browse)
|
|
116
|
+
local cats=(military arena fantasy sci-fi fps retro ui transport spooky racing)
|
|
117
|
+
_describe -t categories 'category' cats
|
|
118
|
+
;;
|
|
119
|
+
completions)
|
|
120
|
+
_describe -t shells 'shell' '(bash zsh fish)'
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
;;
|
|
124
|
+
esac
|
|
125
|
+
}
|
|
126
|
+
compdef _pingthings pingthings`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function fish() {
|
|
130
|
+
const lines = [
|
|
131
|
+
'# pingthings fish completions',
|
|
132
|
+
'complete -c pingthings -f',
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const cmdDescs = {
|
|
136
|
+
play: 'Play a sound', list: 'Show packs', select: 'Interactive picker',
|
|
137
|
+
browse: 'Browse by category', search: 'Search packs', sounds: 'List sounds',
|
|
138
|
+
use: 'Set active pack', preview: 'Preview a pack', 'test-events': 'Test event sounds',
|
|
139
|
+
theme: 'Apply a theme', config: 'Configuration', init: 'Set up hooks',
|
|
140
|
+
create: 'Create a pack', install: 'Install a pack', uninstall: 'Remove a pack',
|
|
141
|
+
'random-pack': 'Random pack', doctor: 'Diagnose setup', completions: 'Shell completions',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
for (const [cmd, desc] of Object.entries(cmdDescs)) {
|
|
145
|
+
lines.push(`complete -c pingthings -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const t of THEMES) {
|
|
149
|
+
lines.push(`complete -c pingthings -n '__fish_seen_subcommand_from theme' -a '${t}'`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const e of EVENTS) {
|
|
153
|
+
lines.push(`complete -c pingthings -n '__fish_seen_subcommand_from play' -l 'event' -a '${e}'`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return lines.join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default function completions(args) {
|
|
160
|
+
const shell = args[0];
|
|
161
|
+
|
|
162
|
+
if (!shell || shell === '--help' || shell === '-h') {
|
|
163
|
+
showHelp();
|
|
164
|
+
if (!shell) process.exit(1);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
switch (shell) {
|
|
169
|
+
case 'bash':
|
|
170
|
+
console.log(bash());
|
|
171
|
+
break;
|
|
172
|
+
case 'zsh':
|
|
173
|
+
console.log(zsh());
|
|
174
|
+
break;
|
|
175
|
+
case 'fish':
|
|
176
|
+
console.log(fish());
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
console.error(`Unknown shell: ${shell}. Use bash, zsh, or fish.`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/cli/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readConfig, writeConfig, VALID_MODES, VALID_EVENTS } from '../config.js';
|
|
2
2
|
|
|
3
|
-
const VALID_KEYS = ['activePack', 'mode', 'specificSound', 'volume', 'eventPacks'];
|
|
3
|
+
const VALID_KEYS = ['activePack', 'mode', 'specificSound', 'volume', 'eventPacks', 'cooldown', 'quietHours'];
|
|
4
4
|
|
|
5
5
|
function showHelp() {
|
|
6
6
|
console.log(`
|
|
@@ -17,13 +17,16 @@ Keys:
|
|
|
17
17
|
mode random, specific, or informational
|
|
18
18
|
specificSound Sound name for specific mode
|
|
19
19
|
volume Playback volume (0-100)
|
|
20
|
+
cooldown Avoid repeating the same sound (true/false)
|
|
21
|
+
quietHours Mute during hours, e.g. "22-7" (10pm-7am)
|
|
20
22
|
eventPacks Per-event pack overrides (use "pingthings config eventPacks.<event> <pack>")
|
|
21
23
|
|
|
22
24
|
Examples:
|
|
23
25
|
pingthings config Show full config
|
|
24
|
-
pingthings config volume Show current volume
|
|
25
26
|
pingthings config volume 50 Set volume to 50%
|
|
26
|
-
pingthings config
|
|
27
|
+
pingthings config cooldown false Disable cooldown
|
|
28
|
+
pingthings config quietHours 22-7 Mute 10pm to 7am
|
|
29
|
+
pingthings config quietHours null Disable quiet hours
|
|
27
30
|
pingthings config eventPacks.error openarena-announcer
|
|
28
31
|
`);
|
|
29
32
|
}
|
|
@@ -109,6 +112,32 @@ export default function config(args) {
|
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
// Validate cooldown
|
|
116
|
+
if (key === 'cooldown') {
|
|
117
|
+
if (value !== 'true' && value !== 'false') {
|
|
118
|
+
console.error('Cooldown must be true or false.');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const cfg = readConfig();
|
|
122
|
+
cfg.cooldown = value === 'true';
|
|
123
|
+
writeConfig(cfg);
|
|
124
|
+
console.log(`cooldown set to: ${cfg.cooldown}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Validate quietHours
|
|
129
|
+
if (key === 'quietHours') {
|
|
130
|
+
if (value !== 'null' && !/^\d{1,2}-\d{1,2}$/.test(value)) {
|
|
131
|
+
console.error('Quiet hours must be in format "HH-HH" (e.g., "22-7") or "null" to disable.');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
const cfg = readConfig();
|
|
135
|
+
cfg.quietHours = value === 'null' ? null : value;
|
|
136
|
+
writeConfig(cfg);
|
|
137
|
+
console.log(`quietHours set to: ${cfg.quietHours ?? '(disabled)'}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
112
141
|
// Two args: set value
|
|
113
142
|
const cfg = readConfig();
|
|
114
143
|
cfg[key] = value === 'null' ? null : value;
|
package/src/cli/create.js
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { platform, homedir } from 'node:os';
|
|
5
|
+
import { readConfig, getConfigDir } from '../config.js';
|
|
6
|
+
import { listPacks } from '../packs.js';
|
|
7
|
+
|
|
8
|
+
function showHelp() {
|
|
9
|
+
console.log(`
|
|
10
|
+
Usage: pingthings doctor
|
|
11
|
+
|
|
12
|
+
Diagnose audio setup, check player availability, verify packs,
|
|
13
|
+
and validate Claude Code hook configuration.
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function check(label, fn) {
|
|
18
|
+
try {
|
|
19
|
+
const result = fn();
|
|
20
|
+
if (result === true) {
|
|
21
|
+
console.log(` OK ${label}`);
|
|
22
|
+
} else if (result === false) {
|
|
23
|
+
console.log(` !! ${label}`);
|
|
24
|
+
} else {
|
|
25
|
+
console.log(` OK ${label}: ${result}`);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.log(` !! ${label}: ${err.message}`);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function commandExists(cmd) {
|
|
35
|
+
try {
|
|
36
|
+
execFileSync('which', [cmd], { stdio: 'pipe' });
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function doctor(args) {
|
|
44
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
45
|
+
showHelp();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log('\npingthings doctor\n');
|
|
50
|
+
|
|
51
|
+
// Platform
|
|
52
|
+
console.log('Platform:');
|
|
53
|
+
check('Operating system', () => {
|
|
54
|
+
const os = platform();
|
|
55
|
+
const names = { darwin: 'macOS', linux: 'Linux', win32: 'Windows' };
|
|
56
|
+
return names[os] || os;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Audio player
|
|
60
|
+
console.log('\nAudio player:');
|
|
61
|
+
let hasPlayer = false;
|
|
62
|
+
if (platform() === 'darwin') {
|
|
63
|
+
hasPlayer = check('afplay available', () => commandExists('afplay'));
|
|
64
|
+
} else if (platform() === 'linux') {
|
|
65
|
+
const pa = check('paplay available (PulseAudio/PipeWire)', () => commandExists('paplay'));
|
|
66
|
+
const al = check('aplay available (ALSA)', () => commandExists('aplay'));
|
|
67
|
+
hasPlayer = pa || al;
|
|
68
|
+
} else if (platform() === 'win32') {
|
|
69
|
+
hasPlayer = check('PowerShell available', () => commandExists('powershell'));
|
|
70
|
+
}
|
|
71
|
+
if (!hasPlayer) {
|
|
72
|
+
console.log('\n No audio player found! Sounds will not play.');
|
|
73
|
+
if (platform() === 'linux') {
|
|
74
|
+
console.log(' Install: sudo apt install pulseaudio-utils (Ubuntu/Debian)');
|
|
75
|
+
console.log(' Or: sudo apt install alsa-utils');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Config
|
|
80
|
+
console.log('\nConfiguration:');
|
|
81
|
+
const config = readConfig();
|
|
82
|
+
check('Config directory', () => getConfigDir());
|
|
83
|
+
check('Active pack', () => config.activePack);
|
|
84
|
+
check('Mode', () => config.mode);
|
|
85
|
+
check('Volume', () => `${config.volume}%`);
|
|
86
|
+
if (config.quietHours) {
|
|
87
|
+
check('Quiet hours', () => config.quietHours);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Packs
|
|
91
|
+
console.log('\nPacks:');
|
|
92
|
+
const packs = listPacks();
|
|
93
|
+
check('Built-in packs', () => {
|
|
94
|
+
const builtIn = packs.filter(p => p.isBuiltIn);
|
|
95
|
+
return `${builtIn.length} packs, ${builtIn.reduce((s, p) => s + p.soundCount, 0)} sounds`;
|
|
96
|
+
});
|
|
97
|
+
check('User packs', () => {
|
|
98
|
+
const user = packs.filter(p => !p.isBuiltIn);
|
|
99
|
+
return user.length > 0 ? `${user.length} packs` : 'none';
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Active pack validation
|
|
103
|
+
const activePack = packs.find(p => p.name === config.activePack);
|
|
104
|
+
if (!activePack) {
|
|
105
|
+
check('Active pack exists', () => { throw new Error(`"${config.activePack}" not found`); });
|
|
106
|
+
} else {
|
|
107
|
+
check('Active pack valid', () => `${activePack.name} (${activePack.soundCount} sounds)`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Claude Code hooks
|
|
111
|
+
console.log('\nClaude Code integration:');
|
|
112
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
113
|
+
if (existsSync(settingsPath)) {
|
|
114
|
+
try {
|
|
115
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
116
|
+
const hooks = settings.hooks || {};
|
|
117
|
+
const hasPingthings = JSON.stringify(hooks).includes('pingthings');
|
|
118
|
+
check('settings.json found', () => true);
|
|
119
|
+
check('pingthings hooks configured', () => hasPingthings);
|
|
120
|
+
if (hasPingthings) {
|
|
121
|
+
const hookTypes = Object.keys(hooks).filter(k =>
|
|
122
|
+
JSON.stringify(hooks[k]).includes('pingthings')
|
|
123
|
+
);
|
|
124
|
+
check('Hook events', () => hookTypes.join(', '));
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
check('settings.json valid', () => false);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
check('settings.json found', () => { throw new Error('Run "pingthings init" to set up'); });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log('');
|
|
134
|
+
}
|
package/src/cli/init.js
CHANGED
|
@@ -113,13 +113,16 @@ export default async function init(args) {
|
|
|
113
113
|
output: process.stdout,
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
116
|
+
await new Promise((resolve) => {
|
|
117
|
+
rl.question('Choose (1 or 2): ', (answer) => {
|
|
118
|
+
const choice = answer.trim();
|
|
119
|
+
if (choice === '2') {
|
|
120
|
+
applyHooks('informational');
|
|
121
|
+
} else {
|
|
122
|
+
applyHooks('basic');
|
|
123
|
+
}
|
|
124
|
+
rl.close();
|
|
125
|
+
resolve();
|
|
126
|
+
});
|
|
124
127
|
});
|
|
125
128
|
}
|
package/src/cli/play.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readConfig, VALID_EVENTS } from '../config.js';
|
|
1
|
+
import { readConfig, VALID_EVENTS, getLastPlayed, setLastPlayed, isQuietHours } from '../config.js';
|
|
2
2
|
import { getPackSounds, getEventSounds, pickRandom, resolvePack } from '../packs.js';
|
|
3
3
|
import { playSound } from '../player.js';
|
|
4
4
|
import { basename } from 'node:path';
|
|
@@ -133,5 +133,26 @@ export default function play(args) {
|
|
|
133
133
|
process.exit(1);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// Quiet hours check
|
|
137
|
+
if (isQuietHours(config)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Cooldown: avoid playing the same sound twice in a row
|
|
142
|
+
if (config.cooldown && !parsed.sound) {
|
|
143
|
+
const lastPlayed = getLastPlayed();
|
|
144
|
+
if (soundFile === lastPlayed) {
|
|
145
|
+
// Pick a different sound from the same pool
|
|
146
|
+
const pool = parsed.event
|
|
147
|
+
? getEventSounds(config.eventPacks?.[parsed.event] || packName, parsed.event)
|
|
148
|
+
: getPackSounds(packName);
|
|
149
|
+
if (pool.length > 1) {
|
|
150
|
+
const alternatives = pool.filter(s => s !== lastPlayed);
|
|
151
|
+
soundFile = pickRandom(alternatives);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
setLastPlayed(soundFile);
|
|
136
157
|
playSound(soundFile, config.volume);
|
|
137
158
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readConfig, writeConfig } from '../config.js';
|
|
2
|
+
import { listPacks, getPackSounds, pickRandom } from '../packs.js';
|
|
3
|
+
import { playSound } from '../player.js';
|
|
4
|
+
import { basename } from 'node:path';
|
|
5
|
+
|
|
6
|
+
function showHelp() {
|
|
7
|
+
console.log(`
|
|
8
|
+
Usage: pingthings random-pack
|
|
9
|
+
|
|
10
|
+
Switch to a random pack for variety. Plays a preview sound from the new pack.
|
|
11
|
+
`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function randomPack(args) {
|
|
15
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
16
|
+
showHelp();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const config = readConfig();
|
|
21
|
+
const packs = listPacks();
|
|
22
|
+
|
|
23
|
+
if (packs.length <= 1) {
|
|
24
|
+
console.log('Only one pack available.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Pick a different pack than the current one
|
|
29
|
+
const others = packs.filter(p => p.name !== config.activePack);
|
|
30
|
+
const chosen = others[Math.floor(Math.random() * others.length)];
|
|
31
|
+
|
|
32
|
+
config.activePack = chosen.name;
|
|
33
|
+
writeConfig(config);
|
|
34
|
+
|
|
35
|
+
const sounds = getPackSounds(chosen.name);
|
|
36
|
+
if (sounds.length > 0) {
|
|
37
|
+
const sample = pickRandom(sounds);
|
|
38
|
+
console.log(`Switched to: ${chosen.name} (${chosen.category})`);
|
|
39
|
+
console.log(`Preview: ${basename(sample)}`);
|
|
40
|
+
playSound(sample, config.volume);
|
|
41
|
+
} else {
|
|
42
|
+
console.log(`Switched to: ${chosen.name}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/cli/select.js
CHANGED
|
@@ -45,37 +45,41 @@ export default async function select(args) {
|
|
|
45
45
|
output: process.stdout,
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
await new Promise((resolve) => {
|
|
49
|
+
const ask = () => {
|
|
50
|
+
rl.question('> ', (answer) => {
|
|
51
|
+
const trimmed = answer.trim().toLowerCase();
|
|
52
|
+
|
|
53
|
+
if (trimmed === 'q' || trimmed === 'quit' || trimmed === '') {
|
|
54
|
+
rl.close();
|
|
55
|
+
resolve();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const num = parseInt(trimmed, 10);
|
|
60
|
+
if (isNaN(num) || num < 1 || num > packs.length) {
|
|
61
|
+
console.log(`Enter 1-${packs.length} or q to quit.`);
|
|
62
|
+
ask();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const chosen = packs[num - 1];
|
|
67
|
+
config.activePack = chosen.name;
|
|
68
|
+
writeConfig(config);
|
|
69
|
+
|
|
70
|
+
// Preview a sound from the chosen pack
|
|
71
|
+
const sounds = getPackSounds(chosen.name);
|
|
72
|
+
if (sounds.length > 0) {
|
|
73
|
+
const sample = sounds[Math.floor(Math.random() * sounds.length)];
|
|
74
|
+
playSound(sample, config.volume);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`\nActive pack set to: ${chosen.name}`);
|
|
53
78
|
rl.close();
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
ask();
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const chosen = packs[num - 1];
|
|
65
|
-
config.activePack = chosen.name;
|
|
66
|
-
writeConfig(config);
|
|
67
|
-
|
|
68
|
-
// Preview a sound from the chosen pack
|
|
69
|
-
const sounds = getPackSounds(chosen.name);
|
|
70
|
-
if (sounds.length > 0) {
|
|
71
|
-
const sample = sounds[Math.floor(Math.random() * sounds.length)];
|
|
72
|
-
playSound(sample, config.volume);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
console.log(`\nActive pack set to: ${chosen.name}`);
|
|
76
|
-
rl.close();
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
ask();
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
ask();
|
|
84
|
+
});
|
|
81
85
|
}
|
package/src/cli/uninstall.js
CHANGED
|
@@ -25,6 +25,11 @@ export default function uninstall(args) {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
if (packName.includes('..') || packName.includes('/') || packName.includes('\\')) {
|
|
29
|
+
console.error('Invalid pack name.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
const userPackDir = join(getConfigDir(), 'packs', packName);
|
|
29
34
|
|
|
30
35
|
if (!existsSync(userPackDir)) {
|
package/src/config.js
CHANGED
|
@@ -8,6 +8,8 @@ const DEFAULTS = {
|
|
|
8
8
|
specificSound: null,
|
|
9
9
|
volume: 100,
|
|
10
10
|
eventPacks: {},
|
|
11
|
+
cooldown: true,
|
|
12
|
+
quietHours: null,
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export function getConfigDir() {
|
|
@@ -45,6 +47,34 @@ export function getDefaults() {
|
|
|
45
47
|
return { ...DEFAULTS };
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
export function getLastPlayed() {
|
|
51
|
+
const path = join(getConfigDir(), '.last-played');
|
|
52
|
+
try {
|
|
53
|
+
return readFileSync(path, 'utf8').trim();
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function setLastPlayed(soundPath) {
|
|
60
|
+
const path = join(getConfigDir(), '.last-played');
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(path, soundPath, 'utf8');
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isQuietHours(config) {
|
|
67
|
+
if (!config.quietHours) return false;
|
|
68
|
+
const [start, end] = config.quietHours.split('-').map(Number);
|
|
69
|
+
if (isNaN(start) || isNaN(end)) return false;
|
|
70
|
+
const hour = new Date().getHours();
|
|
71
|
+
if (start < end) {
|
|
72
|
+
return hour >= start && hour < end;
|
|
73
|
+
}
|
|
74
|
+
// Wraps midnight (e.g., 22-7)
|
|
75
|
+
return hour >= start || hour < end;
|
|
76
|
+
}
|
|
77
|
+
|
|
48
78
|
export const VALID_MODES = ['random', 'specific', 'informational'];
|
|
49
79
|
|
|
50
80
|
export const VALID_EVENTS = ['done', 'permission', 'complete', 'error', 'blocked'];
|
package/src/player.js
CHANGED
|
@@ -2,17 +2,23 @@ import { spawn, execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { platform } from 'node:os';
|
|
4
4
|
|
|
5
|
+
function commandExists(cmd) {
|
|
6
|
+
try {
|
|
7
|
+
execFileSync('which', [cmd], { stdio: 'pipe' });
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
function getPlayerCommand() {
|
|
6
15
|
switch (platform()) {
|
|
7
16
|
case 'darwin':
|
|
8
17
|
return 'afplay';
|
|
9
18
|
case 'linux': {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} catch {
|
|
14
|
-
return 'aplay';
|
|
15
|
-
}
|
|
19
|
+
if (commandExists('paplay')) return 'paplay';
|
|
20
|
+
if (commandExists('aplay')) return 'aplay';
|
|
21
|
+
return null;
|
|
16
22
|
}
|
|
17
23
|
case 'win32':
|
|
18
24
|
return 'powershell';
|
|
@@ -26,20 +32,16 @@ function buildArgs(cmd, filePath, volume) {
|
|
|
26
32
|
|
|
27
33
|
switch (cmd) {
|
|
28
34
|
case 'afplay': {
|
|
29
|
-
// afplay volume: 0.0 to 1.0
|
|
30
35
|
const afplayVol = (vol / 100).toFixed(2);
|
|
31
36
|
return ['-v', afplayVol, filePath];
|
|
32
37
|
}
|
|
33
38
|
case 'paplay': {
|
|
34
|
-
// paplay volume: 0 to 65536 (100% = 65536)
|
|
35
39
|
const paplayVol = Math.round((vol / 100) * 65536).toString();
|
|
36
40
|
return ['--volume', paplayVol, filePath];
|
|
37
41
|
}
|
|
38
42
|
case 'aplay':
|
|
39
|
-
// aplay doesn't support volume natively
|
|
40
43
|
return [filePath];
|
|
41
44
|
case 'powershell': {
|
|
42
|
-
// PowerShell SoundPlayer doesn't support volume, but it plays the file
|
|
43
45
|
const script = `(New-Object System.Media.SoundPlayer '${filePath.replace(/'/g, "''")}').PlaySync()`;
|
|
44
46
|
return ['-NoProfile', '-Command', script];
|
|
45
47
|
}
|
|
@@ -55,7 +57,8 @@ export function playSound(filePath, volume) {
|
|
|
55
57
|
|
|
56
58
|
const cmd = getPlayerCommand();
|
|
57
59
|
if (!cmd) {
|
|
58
|
-
|
|
60
|
+
// No audio player available — skip silently (e.g. CI environments)
|
|
61
|
+
return;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
const args = buildArgs(cmd, filePath, volume);
|
|
@@ -65,6 +68,8 @@ export function playSound(filePath, volume) {
|
|
|
65
68
|
stdio: 'ignore',
|
|
66
69
|
});
|
|
67
70
|
|
|
71
|
+
// Handle spawn errors gracefully (command not found, permission denied)
|
|
72
|
+
child.on('error', () => {});
|
|
68
73
|
child.unref();
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -75,7 +80,7 @@ export function playSoundSync(filePath, volume) {
|
|
|
75
80
|
|
|
76
81
|
const cmd = getPlayerCommand();
|
|
77
82
|
if (!cmd) {
|
|
78
|
-
|
|
83
|
+
return;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
const args = buildArgs(cmd, filePath, volume);
|