pingthings 0.1.0 → 0.2.1
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/LICENSE +22 -3
- package/README.md +89 -4
- package/bin/pingthings.js +16 -4
- package/package.json +1 -1
- package/src/cli/config.js +78 -3
- package/src/cli/create.js +109 -0
- package/src/cli/init.js +125 -0
- package/src/cli/install.js +120 -7
- package/src/cli/list.js +15 -1
- package/src/cli/play.js +13 -3
- package/src/cli/preview.js +18 -5
- package/src/cli/select.js +81 -0
- package/src/cli/test-events.js +51 -0
- package/src/cli/theme.js +132 -0
- package/src/cli/use.js +18 -4
- package/src/config.js +2 -0
- package/src/packs.js +1 -0
- package/src/player.js +55 -6
package/LICENSE
CHANGED
|
@@ -6,6 +6,25 @@
|
|
|
6
6
|
Everyone is permitted to copy and distribute verbatim copies
|
|
7
7
|
of this license document, but changing it is not allowed.
|
|
8
8
|
|
|
9
|
-
Audio assets are from
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
This project is licensed under GPL v2. Audio assets are sourced from
|
|
10
|
+
multiple open source projects under compatible licenses:
|
|
11
|
+
|
|
12
|
+
7kaa-soldiers: Seven Kingdoms: Ancient Adversaries
|
|
13
|
+
Copyright 1997,1998 Enlight Software Ltd. — GPL v2
|
|
14
|
+
|
|
15
|
+
wesnoth-combat: Battle for Wesnoth
|
|
16
|
+
Copyright Wesnoth contributors — GPL v2+
|
|
17
|
+
|
|
18
|
+
openarena-announcer: OpenArena
|
|
19
|
+
Copyright OpenArena contributors — GPL v2
|
|
20
|
+
|
|
21
|
+
freedoom-arsenal: Freedoom
|
|
22
|
+
Copyright Freedoom contributors — BSD-3-Clause
|
|
23
|
+
|
|
24
|
+
warzone2100-command: Warzone 2100
|
|
25
|
+
Copyright Warzone 2100 Project — GPL v2
|
|
26
|
+
|
|
27
|
+
0ad-civilizations: 0 A.D.
|
|
28
|
+
Copyright Wildfire Games — CC-BY-SA 3.0
|
|
29
|
+
Distributed as part of this aggregate work alongside GPL v2 code.
|
|
30
|
+
See packs/0ad-civilizations/manifest.json for full attribution.
|
package/README.md
CHANGED
|
@@ -126,10 +126,15 @@ For different sounds based on what Claude is doing, set up multiple hooks:
|
|
|
126
126
|
|---------|-------------|
|
|
127
127
|
| `pingthings play [sound] [--event type]` | Play a sound (random, specific, or event-based) |
|
|
128
128
|
| `pingthings list` | Show available sound packs |
|
|
129
|
+
| `pingthings select` | Interactive pack selector |
|
|
129
130
|
| `pingthings use <pack>` | Set the active sound pack |
|
|
130
131
|
| `pingthings preview <pack>` | Preview a random sound from a pack |
|
|
132
|
+
| `pingthings test-events [pack]` | Play all event sounds to hear each one |
|
|
133
|
+
| `pingthings theme [name]` | Apply a sound theme |
|
|
131
134
|
| `pingthings config [key] [val]` | Show or update configuration |
|
|
132
|
-
| `pingthings
|
|
135
|
+
| `pingthings init` | Set up Claude Code hooks automatically |
|
|
136
|
+
| `pingthings create <dir>` | Create a pack from audio files |
|
|
137
|
+
| `pingthings install <source>` | Install a pack from GitHub or local path |
|
|
133
138
|
|
|
134
139
|
## Configuration
|
|
135
140
|
|
|
@@ -139,13 +144,17 @@ Config lives at `~/.config/pingthings/config.json`:
|
|
|
139
144
|
{
|
|
140
145
|
"activePack": "7kaa-soldiers",
|
|
141
146
|
"mode": "random",
|
|
142
|
-
"specificSound": null
|
|
147
|
+
"specificSound": null,
|
|
148
|
+
"volume": 100,
|
|
149
|
+
"eventPacks": {}
|
|
143
150
|
}
|
|
144
151
|
```
|
|
145
152
|
|
|
146
153
|
- **activePack** — which sound pack to use
|
|
147
154
|
- **mode** — `"random"` (default), `"specific"`, or `"informational"`
|
|
148
155
|
- **specificSound** — sound name to always play when mode is `"specific"`
|
|
156
|
+
- **volume** — playback volume, 0-100 (default: 100)
|
|
157
|
+
- **eventPacks** — per-event pack overrides (e.g. `{"error": "freedoom-arsenal"}`)
|
|
149
158
|
|
|
150
159
|
Set values via CLI:
|
|
151
160
|
|
|
@@ -252,11 +261,87 @@ To support informational mode, add an `events` field mapping event types to soun
|
|
|
252
261
|
}
|
|
253
262
|
```
|
|
254
263
|
|
|
264
|
+
## Themes
|
|
265
|
+
|
|
266
|
+
Apply a pre-built theme that maps events to packs with one command:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
pingthings theme # list available themes
|
|
270
|
+
pingthings theme sci-fi # apply a theme
|
|
271
|
+
pingthings theme reset # back to defaults
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
| Theme | Description |
|
|
275
|
+
|-------|-------------|
|
|
276
|
+
| `retro` | Classic retro gaming — Freedoom weapons + 7kaa soldiers |
|
|
277
|
+
| `sci-fi` | Sci-fi command center — Warzone 2100 commander voice |
|
|
278
|
+
| `arena` | Arena announcer with FPS weapons for errors |
|
|
279
|
+
| `fantasy` | Medieval fantasy — Wesnoth + 0 A.D. civilizations |
|
|
280
|
+
| `ancient` | Ancient world — 7kaa soldiers + 0 A.D. voices |
|
|
281
|
+
| `chaos` | Different pack for every event — maximum variety |
|
|
282
|
+
|
|
283
|
+
## Tools
|
|
284
|
+
|
|
285
|
+
### Create a pack
|
|
286
|
+
```bash
|
|
287
|
+
pingthings create ./my-sounds my-pack
|
|
288
|
+
```
|
|
289
|
+
Scaffolds a new pack from a folder of audio files with an auto-generated manifest.
|
|
290
|
+
|
|
291
|
+
### Install from GitHub
|
|
292
|
+
```bash
|
|
293
|
+
pingthings install user/repo
|
|
294
|
+
pingthings install https://github.com/user/pack-name
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Auto-setup Claude Code hooks
|
|
298
|
+
```bash
|
|
299
|
+
pingthings init # interactive wizard
|
|
300
|
+
pingthings init --basic # random sounds, no prompts
|
|
301
|
+
pingthings init --informational # event-based sounds, no prompts
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Use with other tools
|
|
305
|
+
|
|
306
|
+
pingthings works anywhere you can run a shell command.
|
|
307
|
+
|
|
308
|
+
### Git hooks
|
|
309
|
+
```bash
|
|
310
|
+
# .git/hooks/post-commit
|
|
311
|
+
#!/bin/sh
|
|
312
|
+
pingthings play --event done
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### CI notifications (GitHub Actions)
|
|
316
|
+
```yaml
|
|
317
|
+
- name: Notify on failure
|
|
318
|
+
if: failure()
|
|
319
|
+
run: npx pingthings play --event error
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Shell aliases
|
|
323
|
+
```bash
|
|
324
|
+
# Add to ~/.zshrc or ~/.bashrc
|
|
325
|
+
alias done='pingthings play --event complete'
|
|
326
|
+
alias oops='pingthings play --event error'
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Pomodoro timer
|
|
330
|
+
```bash
|
|
331
|
+
sleep 1500 && pingthings play --event complete # 25 minute focus
|
|
332
|
+
```
|
|
333
|
+
|
|
255
334
|
## Requirements
|
|
256
335
|
|
|
257
336
|
- Node.js >= 18
|
|
258
|
-
- macOS (`afplay`)
|
|
337
|
+
- macOS (`afplay`), Linux (`paplay` / `aplay`), or Windows (PowerShell)
|
|
259
338
|
|
|
260
339
|
## License
|
|
261
340
|
|
|
262
|
-
GPL v2 — includes audio from
|
|
341
|
+
GPL v2 — includes audio from open source games:
|
|
342
|
+
- [Seven Kingdoms: Ancient Adversaries](https://github.com/the3dfxdude/7kaa) (GPL v2)
|
|
343
|
+
- [Battle for Wesnoth](https://github.com/wesnoth/wesnoth) (GPL v2+)
|
|
344
|
+
- [OpenArena](http://openarena.ws) (GPL v2)
|
|
345
|
+
- [Freedoom](https://github.com/freedoom/freedoom) (BSD-3-Clause)
|
|
346
|
+
- [Warzone 2100](https://github.com/Warzone2100/warzone2100) (GPL v2)
|
|
347
|
+
- [0 A.D.](https://github.com/0ad/0ad) (CC-BY-SA 3.0)
|
package/bin/pingthings.js
CHANGED
|
@@ -14,9 +14,14 @@ const commands = {
|
|
|
14
14
|
play: () => import('../src/cli/play.js'),
|
|
15
15
|
list: () => import('../src/cli/list.js'),
|
|
16
16
|
use: () => import('../src/cli/use.js'),
|
|
17
|
+
select: () => import('../src/cli/select.js'),
|
|
17
18
|
preview: () => import('../src/cli/preview.js'),
|
|
18
19
|
config: () => import('../src/cli/config.js'),
|
|
19
20
|
install: () => import('../src/cli/install.js'),
|
|
21
|
+
init: () => import('../src/cli/init.js'),
|
|
22
|
+
create: () => import('../src/cli/create.js'),
|
|
23
|
+
theme: () => import('../src/cli/theme.js'),
|
|
24
|
+
'test-events': () => import('../src/cli/test-events.js'),
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
function showHelp() {
|
|
@@ -28,10 +33,15 @@ Usage: pingthings <command> [options]
|
|
|
28
33
|
Commands:
|
|
29
34
|
play [sound] Play a sound from the active pack (random by default)
|
|
30
35
|
list Show available sound packs
|
|
36
|
+
select Interactive pack selector
|
|
31
37
|
use <pack> Set the active sound pack
|
|
32
38
|
preview <pack> Preview a random sound from a pack
|
|
39
|
+
test-events [pack] Play all event sounds to hear what each one sounds like
|
|
40
|
+
theme [name] Apply a sound theme (maps events across packs)
|
|
33
41
|
config [key] [val] Show or update configuration
|
|
34
|
-
|
|
42
|
+
init Set up Claude Code hooks automatically
|
|
43
|
+
create <dir> Create a new pack from a folder of audio files
|
|
44
|
+
install <source> Install a pack from GitHub or URL
|
|
35
45
|
|
|
36
46
|
Options:
|
|
37
47
|
--help, -h Show this help message
|
|
@@ -47,9 +57,11 @@ Examples:
|
|
|
47
57
|
pingthings play 00083-READY Play a specific sound
|
|
48
58
|
pingthings play --event done Play a "task done" sound
|
|
49
59
|
pingthings play -e error Play an "error" sound
|
|
50
|
-
pingthings
|
|
51
|
-
pingthings
|
|
52
|
-
pingthings
|
|
60
|
+
pingthings select Choose a pack interactively
|
|
61
|
+
pingthings test-events Hear all event sounds
|
|
62
|
+
pingthings theme retro Apply the retro theme
|
|
63
|
+
pingthings init Set up Claude Code hooks
|
|
64
|
+
pingthings config volume 50 Set volume to 50%
|
|
53
65
|
`);
|
|
54
66
|
}
|
|
55
67
|
|
package/package.json
CHANGED
package/src/cli/config.js
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
|
-
import { readConfig, writeConfig, VALID_MODES } from '../config.js';
|
|
1
|
+
import { readConfig, writeConfig, VALID_MODES, VALID_EVENTS } from '../config.js';
|
|
2
2
|
|
|
3
|
-
const VALID_KEYS = ['activePack', 'mode', 'specificSound'];
|
|
3
|
+
const VALID_KEYS = ['activePack', 'mode', 'specificSound', 'volume', 'eventPacks'];
|
|
4
|
+
|
|
5
|
+
function showHelp() {
|
|
6
|
+
console.log(`
|
|
7
|
+
Usage: pingthings config [key] [value]
|
|
8
|
+
|
|
9
|
+
Show or update configuration.
|
|
10
|
+
|
|
11
|
+
With no arguments, shows the full config.
|
|
12
|
+
With one argument, shows that key's value.
|
|
13
|
+
With two arguments, sets the key to the value.
|
|
14
|
+
|
|
15
|
+
Keys:
|
|
16
|
+
activePack Which sound pack to use
|
|
17
|
+
mode random, specific, or informational
|
|
18
|
+
specificSound Sound name for specific mode
|
|
19
|
+
volume Playback volume (0-100)
|
|
20
|
+
eventPacks Per-event pack overrides (use "pingthings config eventPacks.<event> <pack>")
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
pingthings config Show full config
|
|
24
|
+
pingthings config volume Show current volume
|
|
25
|
+
pingthings config volume 50 Set volume to 50%
|
|
26
|
+
pingthings config mode informational Enable event-based sounds
|
|
27
|
+
pingthings config eventPacks.error openarena-announcer
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
4
30
|
|
|
5
31
|
export default function config(args) {
|
|
32
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
33
|
+
showHelp();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
6
37
|
const [key, ...rest] = args;
|
|
7
38
|
const value = rest.join(' ') || undefined;
|
|
8
39
|
|
|
@@ -13,6 +44,31 @@ export default function config(args) {
|
|
|
13
44
|
return;
|
|
14
45
|
}
|
|
15
46
|
|
|
47
|
+
// Handle eventPacks.event syntax
|
|
48
|
+
if (key.startsWith('eventPacks.')) {
|
|
49
|
+
const event = key.split('.')[1];
|
|
50
|
+
if (!VALID_EVENTS.includes(event)) {
|
|
51
|
+
console.error(`Unknown event: ${event}`);
|
|
52
|
+
console.error(`Valid events: ${VALID_EVENTS.join(', ')}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
if (value === undefined) {
|
|
56
|
+
const cfg = readConfig();
|
|
57
|
+
console.log(cfg.eventPacks?.[event] ?? '(not set — uses active pack)');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const cfg = readConfig();
|
|
61
|
+
if (!cfg.eventPacks) cfg.eventPacks = {};
|
|
62
|
+
if (value === 'null' || value === 'reset') {
|
|
63
|
+
delete cfg.eventPacks[event];
|
|
64
|
+
} else {
|
|
65
|
+
cfg.eventPacks[event] = value;
|
|
66
|
+
}
|
|
67
|
+
writeConfig(cfg);
|
|
68
|
+
console.log(`${event} pack set to: ${value === 'null' || value === 'reset' ? '(active pack)' : value}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
16
72
|
// Validate key
|
|
17
73
|
if (!VALID_KEYS.includes(key)) {
|
|
18
74
|
console.error(`Unknown config key: ${key}`);
|
|
@@ -23,7 +79,12 @@ export default function config(args) {
|
|
|
23
79
|
// One arg: show value
|
|
24
80
|
if (value === undefined) {
|
|
25
81
|
const cfg = readConfig();
|
|
26
|
-
|
|
82
|
+
const val = cfg[key];
|
|
83
|
+
if (typeof val === 'object' && val !== null) {
|
|
84
|
+
console.log(JSON.stringify(val, null, 2));
|
|
85
|
+
} else {
|
|
86
|
+
console.log(val ?? '(not set)');
|
|
87
|
+
}
|
|
27
88
|
return;
|
|
28
89
|
}
|
|
29
90
|
|
|
@@ -34,6 +95,20 @@ export default function config(args) {
|
|
|
34
95
|
process.exit(1);
|
|
35
96
|
}
|
|
36
97
|
|
|
98
|
+
// Validate volume
|
|
99
|
+
if (key === 'volume') {
|
|
100
|
+
const vol = parseInt(value, 10);
|
|
101
|
+
if (isNaN(vol) || vol < 0 || vol > 100) {
|
|
102
|
+
console.error('Volume must be a number between 0 and 100.');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const cfg = readConfig();
|
|
106
|
+
cfg.volume = vol;
|
|
107
|
+
writeConfig(cfg);
|
|
108
|
+
console.log(`volume set to: ${vol}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
37
112
|
// Two args: set value
|
|
38
113
|
const cfg = readConfig();
|
|
39
114
|
cfg[key] = value === 'null' ? null : value;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, readdirSync, mkdirSync, copyFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, basename, extname } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { getConfigDir } from '../config.js';
|
|
5
|
+
|
|
6
|
+
const AUDIO_EXTENSIONS = new Set(['.wav', '.mp3', '.ogg', '.flac']);
|
|
7
|
+
|
|
8
|
+
function showHelp() {
|
|
9
|
+
console.log(`
|
|
10
|
+
Usage: pingthings create <source-dir> [pack-name]
|
|
11
|
+
|
|
12
|
+
Create a new sound pack from a folder of audio files.
|
|
13
|
+
|
|
14
|
+
Arguments:
|
|
15
|
+
source-dir Directory containing audio files (.wav, .mp3, .ogg, .flac)
|
|
16
|
+
pack-name Name for the pack (defaults to directory name)
|
|
17
|
+
|
|
18
|
+
The pack will be installed to ~/.config/pingthings/packs/<pack-name>/
|
|
19
|
+
with an auto-generated manifest.json.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
pingthings create ./my-sounds
|
|
23
|
+
pingthings create ./my-sounds custom-pack
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default async function create(args) {
|
|
28
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
29
|
+
showHelp();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sourceDir = args[0];
|
|
34
|
+
if (!sourceDir) {
|
|
35
|
+
showHelp();
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!existsSync(sourceDir)) {
|
|
40
|
+
console.error(`Directory not found: ${sourceDir}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Find audio files
|
|
45
|
+
const audioFiles = readdirSync(sourceDir)
|
|
46
|
+
.filter(f => AUDIO_EXTENSIONS.has(extname(f).toLowerCase()))
|
|
47
|
+
.sort();
|
|
48
|
+
|
|
49
|
+
if (audioFiles.length === 0) {
|
|
50
|
+
console.error(`No audio files found in ${sourceDir}`);
|
|
51
|
+
console.error('Supported formats: .wav, .mp3, .ogg, .flac');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const packName = args[1] || basename(sourceDir);
|
|
56
|
+
const packsDir = join(getConfigDir(), 'packs');
|
|
57
|
+
const packDir = join(packsDir, packName);
|
|
58
|
+
const soundsDir = join(packDir, 'sounds');
|
|
59
|
+
|
|
60
|
+
if (existsSync(packDir)) {
|
|
61
|
+
console.error(`Pack already exists: ${packDir}`);
|
|
62
|
+
console.error('Delete it first or choose a different name.');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create pack directory structure
|
|
67
|
+
mkdirSync(soundsDir, { recursive: true });
|
|
68
|
+
|
|
69
|
+
// Copy audio files
|
|
70
|
+
console.log(`\nCreating pack "${packName}" with ${audioFiles.length} sounds...\n`);
|
|
71
|
+
|
|
72
|
+
const soundPaths = [];
|
|
73
|
+
for (const file of audioFiles) {
|
|
74
|
+
const dest = join(soundsDir, file);
|
|
75
|
+
copyFileSync(join(sourceDir, file), dest);
|
|
76
|
+
soundPaths.push(`sounds/${file}`);
|
|
77
|
+
console.log(` + ${file}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Generate manifest
|
|
81
|
+
const manifest = {
|
|
82
|
+
name: packName,
|
|
83
|
+
description: '',
|
|
84
|
+
version: '1.0.0',
|
|
85
|
+
license: '',
|
|
86
|
+
credits: '',
|
|
87
|
+
sounds: soundPaths,
|
|
88
|
+
events: {
|
|
89
|
+
done: [],
|
|
90
|
+
permission: [],
|
|
91
|
+
complete: [],
|
|
92
|
+
error: [],
|
|
93
|
+
blocked: [],
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
writeFileSync(
|
|
98
|
+
join(packDir, 'manifest.json'),
|
|
99
|
+
JSON.stringify(manifest, null, 2) + '\n',
|
|
100
|
+
'utf8'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
console.log(`\nPack created at: ${packDir}`);
|
|
104
|
+
console.log(`Manifest: ${join(packDir, 'manifest.json')}`);
|
|
105
|
+
console.log('\nNext steps:');
|
|
106
|
+
console.log(' 1. Edit manifest.json to add description, license, and credits');
|
|
107
|
+
console.log(' 2. Map sounds to events in the "events" field');
|
|
108
|
+
console.log(` 3. Run: pingthings use ${packName}`);
|
|
109
|
+
}
|
package/src/cli/init.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
function showHelp() {
|
|
7
|
+
console.log(`
|
|
8
|
+
Usage: pingthings init
|
|
9
|
+
|
|
10
|
+
Interactive setup wizard that configures Claude Code hooks
|
|
11
|
+
in ~/.claude/settings.json automatically.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--basic Set up basic random sounds (no prompts)
|
|
15
|
+
--informational Set up event-based sounds (no prompts)
|
|
16
|
+
--help, -h Show this help message
|
|
17
|
+
`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const BASIC_HOOKS = {
|
|
21
|
+
Notification: [{
|
|
22
|
+
matcher: '',
|
|
23
|
+
hooks: [{ type: 'command', command: 'pingthings play' }],
|
|
24
|
+
}],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const INFORMATIONAL_HOOKS = {
|
|
28
|
+
Notification: [{
|
|
29
|
+
matcher: '',
|
|
30
|
+
hooks: [{ type: 'command', command: 'pingthings play --event permission' }],
|
|
31
|
+
}],
|
|
32
|
+
Stop: [{
|
|
33
|
+
matcher: '',
|
|
34
|
+
hooks: [{ type: 'command', command: 'pingthings play --event complete' }],
|
|
35
|
+
}],
|
|
36
|
+
PostToolUseFailure: [{
|
|
37
|
+
matcher: '',
|
|
38
|
+
hooks: [{ type: 'command', command: 'pingthings play --event error' }],
|
|
39
|
+
}],
|
|
40
|
+
StopFailure: [{
|
|
41
|
+
matcher: '',
|
|
42
|
+
hooks: [{ type: 'command', command: 'pingthings play --event blocked' }],
|
|
43
|
+
}],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function getSettingsPath() {
|
|
47
|
+
return join(homedir(), '.claude', 'settings.json');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readSettings() {
|
|
51
|
+
const path = getSettingsPath();
|
|
52
|
+
if (!existsSync(path)) return {};
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
55
|
+
} catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeSettings(settings) {
|
|
61
|
+
const path = getSettingsPath();
|
|
62
|
+
const tmpPath = path + '.tmp';
|
|
63
|
+
writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
64
|
+
renameSync(tmpPath, path);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function applyHooks(mode) {
|
|
68
|
+
const settings = readSettings();
|
|
69
|
+
const hooks = mode === 'informational' ? INFORMATIONAL_HOOKS : BASIC_HOOKS;
|
|
70
|
+
settings.hooks = { ...settings.hooks, ...hooks };
|
|
71
|
+
writeSettings(settings);
|
|
72
|
+
|
|
73
|
+
console.log(`\nClaude Code hooks configured (${mode} mode).`);
|
|
74
|
+
console.log(`Settings written to: ${getSettingsPath()}`);
|
|
75
|
+
|
|
76
|
+
if (mode === 'informational') {
|
|
77
|
+
console.log('\nHook mapping:');
|
|
78
|
+
console.log(' Notification → permission sound');
|
|
79
|
+
console.log(' Stop → complete sound');
|
|
80
|
+
console.log(' PostToolUseFailure → error sound');
|
|
81
|
+
console.log(' StopFailure → blocked sound');
|
|
82
|
+
} else {
|
|
83
|
+
console.log('\nA random sound will play on every Claude Code notification.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('\nRestart Claude Code for hooks to take effect.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default async function init(args) {
|
|
90
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
91
|
+
showHelp();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (args.includes('--basic')) {
|
|
96
|
+
applyHooks('basic');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (args.includes('--informational')) {
|
|
101
|
+
applyHooks('informational');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log('\npingthings — Claude Code hook setup\n');
|
|
106
|
+
console.log('How would you like sounds to work?\n');
|
|
107
|
+
console.log(' 1. Basic — random sound on every notification');
|
|
108
|
+
console.log(' 2. Informational — different sounds for different events');
|
|
109
|
+
console.log(' (done, permission, error, blocked)\n');
|
|
110
|
+
|
|
111
|
+
const rl = createInterface({
|
|
112
|
+
input: process.stdin,
|
|
113
|
+
output: process.stdout,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
rl.question('Choose (1 or 2): ', (answer) => {
|
|
117
|
+
const choice = answer.trim();
|
|
118
|
+
if (choice === '2') {
|
|
119
|
+
applyHooks('informational');
|
|
120
|
+
} else {
|
|
121
|
+
applyHooks('basic');
|
|
122
|
+
}
|
|
123
|
+
rl.close();
|
|
124
|
+
});
|
|
125
|
+
}
|
package/src/cli/install.js
CHANGED
|
@@ -1,8 +1,121 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
console.log(
|
|
1
|
+
import { existsSync, mkdirSync, cpSync } from 'node:fs';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { getConfigDir } from '../config.js';
|
|
5
|
+
|
|
6
|
+
function showHelp() {
|
|
7
|
+
console.log(`
|
|
8
|
+
Usage: pingthings install <source>
|
|
9
|
+
|
|
10
|
+
Install a sound pack from a GitHub repository or local path.
|
|
11
|
+
|
|
12
|
+
Arguments:
|
|
13
|
+
source GitHub URL, GitHub shorthand (user/repo), or local path
|
|
14
|
+
|
|
15
|
+
The pack will be installed to ~/.config/pingthings/packs/
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
pingthings install user/repo
|
|
19
|
+
pingthings install https://github.com/user/pingthings-pack-name
|
|
20
|
+
pingthings install ./path/to/local/pack
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function installFromGit(source) {
|
|
25
|
+
const packsDir = join(getConfigDir(), 'packs');
|
|
26
|
+
mkdirSync(packsDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
// Convert shorthand to full URL
|
|
29
|
+
let url = source;
|
|
30
|
+
if (!source.startsWith('http') && !source.startsWith('git@') && source.includes('/')) {
|
|
31
|
+
url = `https://github.com/${source}.git`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const repoName = basename(url, '.git');
|
|
35
|
+
const destDir = join(packsDir, repoName);
|
|
36
|
+
|
|
37
|
+
if (existsSync(destDir)) {
|
|
38
|
+
console.error(`Pack already exists: ${repoName}`);
|
|
39
|
+
console.error(`Delete ${destDir} first to reinstall.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`Cloning ${url}...`);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
execFileSync('git', ['clone', '--depth', '1', url, destDir], {
|
|
47
|
+
stdio: 'inherit',
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
console.error('Failed to clone repository. Check the URL and try again.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for manifest
|
|
55
|
+
if (!existsSync(join(destDir, 'manifest.json'))) {
|
|
56
|
+
console.log('\nNote: No manifest.json found. The pack may need one to work properly.');
|
|
57
|
+
console.log('Run "pingthings create" to generate a manifest from audio files.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`\nInstalled: ${repoName}`);
|
|
61
|
+
console.log(`Location: ${destDir}`);
|
|
62
|
+
console.log(`\nTo use: pingthings use ${repoName}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function installFromLocal(source) {
|
|
66
|
+
if (!existsSync(source)) {
|
|
67
|
+
console.error(`Path not found: ${source}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!existsSync(join(source, 'manifest.json'))) {
|
|
72
|
+
console.error('No manifest.json found in the source directory.');
|
|
73
|
+
console.error('Run "pingthings create <dir>" to create a pack from audio files.');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const packName = basename(source);
|
|
78
|
+
if (!packName) {
|
|
79
|
+
console.error('Could not determine pack name from path.');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const packsDir = join(getConfigDir(), 'packs');
|
|
83
|
+
const destDir = join(packsDir, packName);
|
|
84
|
+
|
|
85
|
+
if (existsSync(destDir)) {
|
|
86
|
+
console.error(`Pack already exists: ${packName}`);
|
|
87
|
+
console.error(`Delete ${destDir} first to reinstall.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
mkdirSync(packsDir, { recursive: true });
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
cpSync(source, destDir, { recursive: true });
|
|
95
|
+
} catch {
|
|
96
|
+
console.error('Failed to copy pack.');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`Installed: ${packName}`);
|
|
101
|
+
console.log(`Location: ${destDir}`);
|
|
102
|
+
console.log(`\nTo use: pingthings use ${packName}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default function install(args) {
|
|
106
|
+
const source = args[0];
|
|
107
|
+
|
|
108
|
+
if (!source || source === '--help' || source === '-h') {
|
|
109
|
+
showHelp();
|
|
110
|
+
if (!source) process.exit(1);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Determine if it's a git URL, shorthand, or local path
|
|
115
|
+
if (source.startsWith('http') || source.startsWith('git@') ||
|
|
116
|
+
(source.includes('/') && !source.startsWith('.') && !source.startsWith('/'))) {
|
|
117
|
+
installFromGit(source);
|
|
118
|
+
} else {
|
|
119
|
+
installFromLocal(source);
|
|
120
|
+
}
|
|
8
121
|
}
|
package/src/cli/list.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { readConfig } from '../config.js';
|
|
2
2
|
import { listPacks } from '../packs.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
function showHelp() {
|
|
5
|
+
console.log(`
|
|
6
|
+
Usage: pingthings list
|
|
7
|
+
|
|
8
|
+
Show all available sound packs with their sound count and source.
|
|
9
|
+
The active pack is marked with *.
|
|
10
|
+
`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function list(args) {
|
|
14
|
+
if (args?.includes('--help') || args?.includes('-h')) {
|
|
15
|
+
showHelp();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
const config = readConfig();
|
|
6
20
|
const packs = listPacks();
|
|
7
21
|
|
package/src/cli/play.js
CHANGED
|
@@ -78,10 +78,15 @@ export default function play(args) {
|
|
|
78
78
|
|
|
79
79
|
// If --event flag is provided, use event mapping
|
|
80
80
|
if (parsed.event) {
|
|
81
|
-
|
|
81
|
+
// Pack mixing: check if there's a per-event pack override
|
|
82
|
+
const eventPackName = config.eventPacks?.[parsed.event] || packName;
|
|
83
|
+
const eventPack = resolvePack(eventPackName);
|
|
84
|
+
const resolvedPack = eventPack ? eventPackName : packName;
|
|
85
|
+
|
|
86
|
+
const eventSounds = getEventSounds(resolvedPack, parsed.event);
|
|
82
87
|
if (eventSounds.length === 0) {
|
|
83
88
|
// Fall back to random if pack has no mapping for this event
|
|
84
|
-
const allSounds = getPackSounds(
|
|
89
|
+
const allSounds = getPackSounds(resolvedPack);
|
|
85
90
|
soundFile = pickRandom(allSounds);
|
|
86
91
|
} else {
|
|
87
92
|
soundFile = pickRandom(eventSounds);
|
|
@@ -123,5 +128,10 @@ export default function play(args) {
|
|
|
123
128
|
soundFile = pickRandom(sounds);
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
if (!soundFile) {
|
|
132
|
+
console.error(`No sounds available in pack: ${packName}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
playSound(soundFile, config.volume);
|
|
127
137
|
}
|
package/src/cli/preview.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
|
+
import { readConfig } from '../config.js';
|
|
1
2
|
import { getPackSounds, pickRandom, resolvePack } from '../packs.js';
|
|
2
3
|
import { playSound } from '../player.js';
|
|
3
4
|
import { basename } from 'node:path';
|
|
4
5
|
|
|
6
|
+
function showHelp() {
|
|
7
|
+
console.log(`
|
|
8
|
+
Usage: pingthings preview <pack>
|
|
9
|
+
|
|
10
|
+
Play a random sound from a pack to hear what it sounds like.
|
|
11
|
+
|
|
12
|
+
Arguments:
|
|
13
|
+
pack Name of the pack to preview
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
export default function preview(args) {
|
|
6
18
|
const packName = args[0];
|
|
7
19
|
|
|
8
|
-
if (!packName) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
if (!packName || packName === '--help' || packName === '-h') {
|
|
21
|
+
showHelp();
|
|
22
|
+
if (!packName) process.exit(1);
|
|
23
|
+
return;
|
|
12
24
|
}
|
|
13
25
|
|
|
14
26
|
const pack = resolvePack(packName);
|
|
@@ -24,7 +36,8 @@ export default function preview(args) {
|
|
|
24
36
|
process.exit(1);
|
|
25
37
|
}
|
|
26
38
|
|
|
39
|
+
const config = readConfig();
|
|
27
40
|
const sound = pickRandom(sounds);
|
|
28
41
|
console.log(`Playing: ${basename(sound)} from "${packName}"`);
|
|
29
|
-
playSound(sound);
|
|
42
|
+
playSound(sound, config.volume);
|
|
30
43
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { readConfig, writeConfig } from '../config.js';
|
|
3
|
+
import { listPacks, getPackSounds } from '../packs.js';
|
|
4
|
+
import { playSound } from '../player.js';
|
|
5
|
+
import { basename } from 'node:path';
|
|
6
|
+
|
|
7
|
+
function showHelp() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Usage: pingthings select
|
|
10
|
+
|
|
11
|
+
Interactive menu for choosing your active sound pack.
|
|
12
|
+
Displays a numbered list — type a number to preview and select.
|
|
13
|
+
`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default async function select(args) {
|
|
17
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
18
|
+
showHelp();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const config = readConfig();
|
|
23
|
+
const packs = listPacks();
|
|
24
|
+
|
|
25
|
+
if (packs.length === 0) {
|
|
26
|
+
console.error('No sound packs found.');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('\nChoose a sound pack:\n');
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < packs.length; i++) {
|
|
33
|
+
const active = packs[i].name === config.activePack ? ' *' : ' ';
|
|
34
|
+
console.log(`${active} ${i + 1}. ${packs[i].name} (${packs[i].soundCount} sounds)`);
|
|
35
|
+
if (packs[i].description) {
|
|
36
|
+
console.log(` ${packs[i].description}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log('\n * = current pack');
|
|
41
|
+
console.log(' Enter a number to preview & select, or q to quit.\n');
|
|
42
|
+
|
|
43
|
+
const rl = createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const ask = () => {
|
|
49
|
+
rl.question('> ', (answer) => {
|
|
50
|
+
const trimmed = answer.trim().toLowerCase();
|
|
51
|
+
|
|
52
|
+
if (trimmed === 'q' || trimmed === 'quit' || trimmed === '') {
|
|
53
|
+
rl.close();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const num = parseInt(trimmed, 10);
|
|
58
|
+
if (isNaN(num) || num < 1 || num > packs.length) {
|
|
59
|
+
console.log(`Enter 1-${packs.length} or q to quit.`);
|
|
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();
|
|
81
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readConfig, VALID_EVENTS } from '../config.js';
|
|
2
|
+
import { getEventSounds, getPackSounds, pickRandom, resolvePack } from '../packs.js';
|
|
3
|
+
import { playSoundSync } from '../player.js';
|
|
4
|
+
import { basename } from 'node:path';
|
|
5
|
+
|
|
6
|
+
function showHelp() {
|
|
7
|
+
console.log(`
|
|
8
|
+
Usage: pingthings test-events [pack]
|
|
9
|
+
|
|
10
|
+
Play one sound for each event type from a pack so you can hear
|
|
11
|
+
what each event sounds like. Defaults to the active pack.
|
|
12
|
+
|
|
13
|
+
Arguments:
|
|
14
|
+
pack Pack to test (optional, defaults to active pack)
|
|
15
|
+
|
|
16
|
+
Events played: done, permission, complete, error, blocked
|
|
17
|
+
`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default async function testEvents(args) {
|
|
21
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
22
|
+
showHelp();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config = readConfig();
|
|
27
|
+
const packName = args[0] || config.activePack;
|
|
28
|
+
const pack = resolvePack(packName);
|
|
29
|
+
|
|
30
|
+
if (!pack) {
|
|
31
|
+
console.error(`Pack not found: ${packName}`);
|
|
32
|
+
console.error('Run "pingthings list" to see available packs.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`\nTesting events for: ${packName}\n`);
|
|
37
|
+
|
|
38
|
+
for (const event of VALID_EVENTS) {
|
|
39
|
+
const sounds = getEventSounds(packName, event);
|
|
40
|
+
if (sounds.length === 0) {
|
|
41
|
+
console.log(` ${event.padEnd(12)} (no sounds mapped)`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sound = pickRandom(sounds);
|
|
46
|
+
console.log(` ${event.padEnd(12)} ${basename(sound)}`);
|
|
47
|
+
playSoundSync(sound, config.volume);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('\nDone.\n');
|
|
51
|
+
}
|
package/src/cli/theme.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { readConfig, writeConfig } from '../config.js';
|
|
2
|
+
import { resolvePack } from '../packs.js';
|
|
3
|
+
|
|
4
|
+
const THEMES = {
|
|
5
|
+
'retro': {
|
|
6
|
+
description: 'Classic retro gaming — Freedoom weapons and 7kaa soldiers',
|
|
7
|
+
activePack: '7kaa-soldiers',
|
|
8
|
+
eventPacks: {
|
|
9
|
+
done: '7kaa-soldiers',
|
|
10
|
+
permission: '7kaa-soldiers',
|
|
11
|
+
complete: 'freedoom-arsenal',
|
|
12
|
+
error: 'freedoom-arsenal',
|
|
13
|
+
blocked: 'freedoom-arsenal',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
'sci-fi': {
|
|
17
|
+
description: 'Sci-fi command center — Warzone 2100 commander voice',
|
|
18
|
+
activePack: 'warzone2100-command',
|
|
19
|
+
eventPacks: {
|
|
20
|
+
done: 'warzone2100-command',
|
|
21
|
+
permission: 'warzone2100-command',
|
|
22
|
+
complete: 'warzone2100-command',
|
|
23
|
+
error: 'warzone2100-command',
|
|
24
|
+
blocked: 'warzone2100-command',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
'arena': {
|
|
28
|
+
description: 'Arena announcer with FPS weapons for errors',
|
|
29
|
+
activePack: 'openarena-announcer',
|
|
30
|
+
eventPacks: {
|
|
31
|
+
done: 'openarena-announcer',
|
|
32
|
+
permission: 'openarena-announcer',
|
|
33
|
+
complete: 'openarena-announcer',
|
|
34
|
+
error: 'freedoom-arsenal',
|
|
35
|
+
blocked: 'freedoom-arsenal',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
'fantasy': {
|
|
39
|
+
description: 'Medieval fantasy — Wesnoth combat and 0 A.D. civilizations',
|
|
40
|
+
activePack: 'wesnoth-combat',
|
|
41
|
+
eventPacks: {
|
|
42
|
+
done: 'wesnoth-combat',
|
|
43
|
+
permission: '0ad-civilizations',
|
|
44
|
+
complete: 'wesnoth-combat',
|
|
45
|
+
error: 'wesnoth-combat',
|
|
46
|
+
blocked: '0ad-civilizations',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
'ancient': {
|
|
50
|
+
description: 'Ancient world — 7kaa soldiers and 0 A.D. civilizations',
|
|
51
|
+
activePack: '7kaa-soldiers',
|
|
52
|
+
eventPacks: {
|
|
53
|
+
done: '7kaa-soldiers',
|
|
54
|
+
permission: '0ad-civilizations',
|
|
55
|
+
complete: '0ad-civilizations',
|
|
56
|
+
error: '7kaa-soldiers',
|
|
57
|
+
blocked: '0ad-civilizations',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
'chaos': {
|
|
61
|
+
description: 'Random pack for every event — maximum variety',
|
|
62
|
+
activePack: '7kaa-soldiers',
|
|
63
|
+
eventPacks: {
|
|
64
|
+
done: '7kaa-soldiers',
|
|
65
|
+
permission: 'openarena-announcer',
|
|
66
|
+
complete: 'warzone2100-command',
|
|
67
|
+
error: 'freedoom-arsenal',
|
|
68
|
+
blocked: 'wesnoth-combat',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function showHelp() {
|
|
74
|
+
console.log(`
|
|
75
|
+
Usage: pingthings theme [name]
|
|
76
|
+
|
|
77
|
+
Apply a sound theme that maps events to packs.
|
|
78
|
+
|
|
79
|
+
With no arguments, lists available themes.
|
|
80
|
+
|
|
81
|
+
Available themes:`);
|
|
82
|
+
|
|
83
|
+
for (const [name, theme] of Object.entries(THEMES)) {
|
|
84
|
+
console.log(` ${name.padEnd(12)} ${theme.description}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`
|
|
88
|
+
Examples:
|
|
89
|
+
pingthings theme List themes
|
|
90
|
+
pingthings theme sci-fi Apply the sci-fi theme
|
|
91
|
+
pingthings theme reset Reset to defaults
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default function theme(args) {
|
|
96
|
+
const name = args[0];
|
|
97
|
+
|
|
98
|
+
if (!name || name === '--help' || name === '-h') {
|
|
99
|
+
showHelp();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (name === 'reset') {
|
|
104
|
+
const config = readConfig();
|
|
105
|
+
config.eventPacks = {};
|
|
106
|
+
config.mode = 'random';
|
|
107
|
+
writeConfig(config);
|
|
108
|
+
console.log('Theme reset. Using default random mode.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const selected = THEMES[name];
|
|
113
|
+
if (!selected) {
|
|
114
|
+
console.error(`Unknown theme: ${name}`);
|
|
115
|
+
console.error(`Available themes: ${Object.keys(THEMES).join(', ')}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const config = readConfig();
|
|
120
|
+
config.activePack = selected.activePack;
|
|
121
|
+
config.mode = 'informational';
|
|
122
|
+
config.eventPacks = { ...selected.eventPacks };
|
|
123
|
+
writeConfig(config);
|
|
124
|
+
|
|
125
|
+
console.log(`\nTheme applied: ${name}`);
|
|
126
|
+
console.log(`${selected.description}\n`);
|
|
127
|
+
console.log('Event mapping:');
|
|
128
|
+
for (const [event, pack] of Object.entries(selected.eventPacks)) {
|
|
129
|
+
console.log(` ${event.padEnd(12)} → ${pack}`);
|
|
130
|
+
}
|
|
131
|
+
console.log(`\nMode set to: informational`);
|
|
132
|
+
}
|
package/src/cli/use.js
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { readConfig, writeConfig } from '../config.js';
|
|
2
2
|
import { resolvePack } from '../packs.js';
|
|
3
3
|
|
|
4
|
+
function showHelp() {
|
|
5
|
+
console.log(`
|
|
6
|
+
Usage: pingthings use <pack>
|
|
7
|
+
|
|
8
|
+
Set the active sound pack.
|
|
9
|
+
|
|
10
|
+
Arguments:
|
|
11
|
+
pack Name of the pack to activate
|
|
12
|
+
|
|
13
|
+
Run "pingthings list" to see available packs.
|
|
14
|
+
Run "pingthings select" for an interactive picker.
|
|
15
|
+
`);
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
export default function use(args) {
|
|
5
19
|
const packName = args[0];
|
|
6
20
|
|
|
7
|
-
if (!packName) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
21
|
+
if (!packName || packName === '--help' || packName === '-h') {
|
|
22
|
+
showHelp();
|
|
23
|
+
if (!packName) process.exit(1);
|
|
24
|
+
return;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
const pack = resolvePack(packName);
|
package/src/config.js
CHANGED
package/src/packs.js
CHANGED
package/src/player.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { platform } from 'node:os';
|
|
4
4
|
|
|
@@ -7,33 +7,82 @@ function getPlayerCommand() {
|
|
|
7
7
|
case 'darwin':
|
|
8
8
|
return 'afplay';
|
|
9
9
|
case 'linux': {
|
|
10
|
-
// Prefer PulseAudio/PipeWire, fall back to ALSA
|
|
11
10
|
try {
|
|
12
|
-
|
|
11
|
+
execFileSync('which', ['paplay'], { stdio: 'pipe' });
|
|
13
12
|
return 'paplay';
|
|
14
13
|
} catch {
|
|
15
14
|
return 'aplay';
|
|
16
15
|
}
|
|
17
16
|
}
|
|
17
|
+
case 'win32':
|
|
18
|
+
return 'powershell';
|
|
18
19
|
default:
|
|
19
20
|
return null;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
function buildArgs(cmd, filePath, volume) {
|
|
25
|
+
const vol = Math.max(0, Math.min(100, volume ?? 100));
|
|
26
|
+
|
|
27
|
+
switch (cmd) {
|
|
28
|
+
case 'afplay': {
|
|
29
|
+
// afplay volume: 0.0 to 1.0
|
|
30
|
+
const afplayVol = (vol / 100).toFixed(2);
|
|
31
|
+
return ['-v', afplayVol, filePath];
|
|
32
|
+
}
|
|
33
|
+
case 'paplay': {
|
|
34
|
+
// paplay volume: 0 to 65536 (100% = 65536)
|
|
35
|
+
const paplayVol = Math.round((vol / 100) * 65536).toString();
|
|
36
|
+
return ['--volume', paplayVol, filePath];
|
|
37
|
+
}
|
|
38
|
+
case 'aplay':
|
|
39
|
+
// aplay doesn't support volume natively
|
|
40
|
+
return [filePath];
|
|
41
|
+
case 'powershell': {
|
|
42
|
+
// PowerShell SoundPlayer doesn't support volume, but it plays the file
|
|
43
|
+
const script = `(New-Object System.Media.SoundPlayer '${filePath.replace(/'/g, "''")}').PlaySync()`;
|
|
44
|
+
return ['-NoProfile', '-Command', script];
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
return [filePath];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function playSound(filePath, volume) {
|
|
24
52
|
if (!existsSync(filePath)) {
|
|
25
53
|
throw new Error(`Sound file not found: ${filePath}`);
|
|
26
54
|
}
|
|
27
55
|
|
|
28
56
|
const cmd = getPlayerCommand();
|
|
29
57
|
if (!cmd) {
|
|
30
|
-
throw new Error(`Unsupported platform: ${platform()}. Supported: macOS, Linux.`);
|
|
58
|
+
throw new Error(`Unsupported platform: ${platform()}. Supported: macOS, Linux, Windows.`);
|
|
31
59
|
}
|
|
32
60
|
|
|
33
|
-
const
|
|
61
|
+
const args = buildArgs(cmd, filePath, volume);
|
|
62
|
+
|
|
63
|
+
const child = spawn(cmd, args, {
|
|
34
64
|
detached: true,
|
|
35
65
|
stdio: 'ignore',
|
|
36
66
|
});
|
|
37
67
|
|
|
38
68
|
child.unref();
|
|
39
69
|
}
|
|
70
|
+
|
|
71
|
+
export function playSoundSync(filePath, volume) {
|
|
72
|
+
if (!existsSync(filePath)) {
|
|
73
|
+
throw new Error(`Sound file not found: ${filePath}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cmd = getPlayerCommand();
|
|
77
|
+
if (!cmd) {
|
|
78
|
+
throw new Error(`Unsupported platform: ${platform()}. Supported: macOS, Linux, Windows.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const args = buildArgs(cmd, filePath, volume);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
execFileSync(cmd, args, { stdio: 'ignore', timeout: 10000 });
|
|
85
|
+
} catch {
|
|
86
|
+
// Timeout or error — don't crash
|
|
87
|
+
}
|
|
88
|
+
}
|