pingthings 0.2.1 → 0.4.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.
Files changed (73) hide show
  1. package/README.md +13 -0
  2. package/bin/pingthings.js +8 -0
  3. package/package.json +1 -1
  4. package/packs/0ad-civilizations/manifest.json +1 -0
  5. package/packs/7kaa-soldiers/manifest.json +1 -0
  6. package/packs/freedoom-arsenal/manifest.json +1 -0
  7. package/packs/kenney-interface/manifest.json +58 -0
  8. package/packs/kenney-interface/sounds/back-1.wav +0 -0
  9. package/packs/kenney-interface/sounds/bong.wav +0 -0
  10. package/packs/kenney-interface/sounds/click-1.wav +0 -0
  11. package/packs/kenney-interface/sounds/close-1.wav +0 -0
  12. package/packs/kenney-interface/sounds/confirmation-1.wav +0 -0
  13. package/packs/kenney-interface/sounds/confirmation-2.wav +0 -0
  14. package/packs/kenney-interface/sounds/confirmation-3.wav +0 -0
  15. package/packs/kenney-interface/sounds/error-1.wav +0 -0
  16. package/packs/kenney-interface/sounds/error-2.wav +0 -0
  17. package/packs/kenney-interface/sounds/error-3.wav +0 -0
  18. package/packs/kenney-interface/sounds/glitch-1.wav +0 -0
  19. package/packs/kenney-interface/sounds/maximize-1.wav +0 -0
  20. package/packs/kenney-interface/sounds/maximize-2.wav +0 -0
  21. package/packs/kenney-interface/sounds/open-1.wav +0 -0
  22. package/packs/kenney-interface/sounds/pluck-1.wav +0 -0
  23. package/packs/kenney-interface/sounds/question-1.wav +0 -0
  24. package/packs/kenney-interface/sounds/question-2.wav +0 -0
  25. package/packs/kenney-interface/sounds/question-3.wav +0 -0
  26. package/packs/kenney-scifi/manifest.json +58 -0
  27. package/packs/kenney-scifi/sounds/computer-1.wav +0 -0
  28. package/packs/kenney-scifi/sounds/computer-2.wav +0 -0
  29. package/packs/kenney-scifi/sounds/computer-3.wav +0 -0
  30. package/packs/kenney-scifi/sounds/door-close.wav +0 -0
  31. package/packs/kenney-scifi/sounds/door-open.wav +0 -0
  32. package/packs/kenney-scifi/sounds/engine-1.wav +0 -0
  33. package/packs/kenney-scifi/sounds/explosion-1.wav +0 -0
  34. package/packs/kenney-scifi/sounds/explosion-2.wav +0 -0
  35. package/packs/kenney-scifi/sounds/forcefield-1.wav +0 -0
  36. package/packs/kenney-scifi/sounds/forcefield-2.wav +0 -0
  37. package/packs/kenney-scifi/sounds/impact-1.wav +0 -0
  38. package/packs/kenney-scifi/sounds/impact-2.wav +0 -0
  39. package/packs/kenney-scifi/sounds/laser-large.wav +0 -0
  40. package/packs/kenney-scifi/sounds/laser-retro.wav +0 -0
  41. package/packs/kenney-scifi/sounds/low-explosion.wav +0 -0
  42. package/packs/kenney-scifi/sounds/slime.wav +0 -0
  43. package/packs/kenney-scifi/sounds/thruster-1.wav +0 -0
  44. package/packs/kenney-scifi/sounds/thruster-2.wav +0 -0
  45. package/packs/openarena-announcer/manifest.json +1 -0
  46. package/packs/retro-8bit/manifest.json +58 -0
  47. package/packs/retro-8bit/sounds/button-1.wav +0 -0
  48. package/packs/retro-8bit/sounds/button-2.wav +0 -0
  49. package/packs/retro-8bit/sounds/button-3.wav +0 -0
  50. package/packs/retro-8bit/sounds/coin-1.wav +0 -0
  51. package/packs/retro-8bit/sounds/coin-2.wav +0 -0
  52. package/packs/retro-8bit/sounds/coin-cluster.wav +0 -0
  53. package/packs/retro-8bit/sounds/coin-double.wav +0 -0
  54. package/packs/retro-8bit/sounds/damage-1.wav +0 -0
  55. package/packs/retro-8bit/sounds/error-1.wav +0 -0
  56. package/packs/retro-8bit/sounds/error-2.wav +0 -0
  57. package/packs/retro-8bit/sounds/error-3.wav +0 -0
  58. package/packs/retro-8bit/sounds/fanfare-1.wav +0 -0
  59. package/packs/retro-8bit/sounds/fanfare-2.wav +0 -0
  60. package/packs/retro-8bit/sounds/fanfare-3.wav +0 -0
  61. package/packs/retro-8bit/sounds/negative-1.wav +0 -0
  62. package/packs/retro-8bit/sounds/negative-2.wav +0 -0
  63. package/packs/retro-8bit/sounds/powerup-1.wav +0 -0
  64. package/packs/retro-8bit/sounds/powerup-2.wav +0 -0
  65. package/packs/warzone2100-command/manifest.json +1 -0
  66. package/packs/wesnoth-combat/manifest.json +1 -0
  67. package/src/cli/browse.js +75 -0
  68. package/src/cli/search.js +81 -0
  69. package/src/cli/sounds.js +76 -0
  70. package/src/cli/theme.js +34 -1
  71. package/src/cli/uninstall.js +48 -0
  72. package/src/packs.js +8 -2
  73. package/src/player.js +7 -3
package/README.md CHANGED
@@ -134,7 +134,11 @@ For different sounds based on what Claude is doing, set up multiple hooks:
134
134
  | `pingthings config [key] [val]` | Show or update configuration |
135
135
  | `pingthings init` | Set up Claude Code hooks automatically |
136
136
  | `pingthings create <dir>` | Create a pack from audio files |
137
+ | `pingthings browse [category]` | Browse packs by category |
138
+ | `pingthings search <term>` | Search packs and sounds |
139
+ | `pingthings sounds [pack]` | List individual sounds in a pack |
137
140
  | `pingthings install <source>` | Install a pack from GitHub or local path |
141
+ | `pingthings uninstall <pack>` | Remove a user-installed pack |
138
142
 
139
143
  ## Configuration
140
144
 
@@ -218,6 +222,15 @@ Sci-fi military commander voice lines from **Warzone 2100** — 21 sounds includ
218
222
  ### 0ad-civilizations
219
223
  Ancient civilization voice lines and alerts from **0 A.D.** — 28 sounds with Greek, Latin, and Persian voice acknowledgements ("my lord", "as you wish", "by your order") plus alarm sounds (victory, defeat, attack). License: CC-BY-SA 3.0.
220
224
 
225
+ ### retro-8bit
226
+ Classic 8-bit chiptune notification sounds — 18 sounds including coins, fanfares, power-ups, error buzzes, and button clicks. Perfect for that retro gaming feel. License: CC0.
227
+
228
+ ### kenney-interface
229
+ Clean modern UI notification sounds by **Kenney** — 18 sounds including confirmations, questions, errors, glitches, and bongs. Professional and minimal. License: CC0.
230
+
231
+ ### kenney-scifi
232
+ Futuristic sci-fi notification sounds by **Kenney** — 18 sounds including computer noises, force fields, lasers, explosions, and thrusters. License: CC0.
233
+
221
234
  ## Custom packs
222
235
 
223
236
  Place packs in `~/.config/pingthings/packs/<pack-name>/`:
package/bin/pingthings.js CHANGED
@@ -15,9 +15,13 @@ const commands = {
15
15
  list: () => import('../src/cli/list.js'),
16
16
  use: () => import('../src/cli/use.js'),
17
17
  select: () => import('../src/cli/select.js'),
18
+ browse: () => import('../src/cli/browse.js'),
19
+ search: () => import('../src/cli/search.js'),
20
+ sounds: () => import('../src/cli/sounds.js'),
18
21
  preview: () => import('../src/cli/preview.js'),
19
22
  config: () => import('../src/cli/config.js'),
20
23
  install: () => import('../src/cli/install.js'),
24
+ uninstall: () => import('../src/cli/uninstall.js'),
21
25
  init: () => import('../src/cli/init.js'),
22
26
  create: () => import('../src/cli/create.js'),
23
27
  theme: () => import('../src/cli/theme.js'),
@@ -34,6 +38,9 @@ Commands:
34
38
  play [sound] Play a sound from the active pack (random by default)
35
39
  list Show available sound packs
36
40
  select Interactive pack selector
41
+ browse [category] Browse packs by category
42
+ search <term> Search packs and sounds
43
+ sounds [pack] List individual sounds in a pack
37
44
  use <pack> Set the active sound pack
38
45
  preview <pack> Preview a random sound from a pack
39
46
  test-events [pack] Play all event sounds to hear what each one sounds like
@@ -42,6 +49,7 @@ Commands:
42
49
  init Set up Claude Code hooks automatically
43
50
  create <dir> Create a new pack from a folder of audio files
44
51
  install <source> Install a pack from GitHub or URL
52
+ uninstall <pack> Remove a user-installed pack
45
53
 
46
54
  Options:
47
55
  --help, -h Show this help message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pingthings",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Notification sounds for Claude Code and other CLI tools",
5
5
  "type": "module",
6
6
  "license": "GPL-2.0",
@@ -4,6 +4,7 @@
4
4
  "version": "1.0.0",
5
5
  "license": "CC-BY-SA-3.0",
6
6
  "credits": "Wildfire Games / 0 A.D. (https://github.com/0ad/0ad) — audio licensed under CC-BY-SA 3.0",
7
+ "category": "military",
7
8
  "sounds": [
8
9
  "sounds/alarm-alert.wav",
9
10
  "sounds/alarm-attack.wav",
@@ -4,6 +4,7 @@
4
4
  "version": "1.0.0",
5
5
  "license": "GPL-2.0",
6
6
  "credits": "Enlight Software Ltd. / Seven Kingdoms: Ancient Adversaries (https://github.com/the3dfxdude/7kaa)",
7
+ "category": "military",
7
8
  "sounds": [
8
9
  "sounds/00001-CH_A2.wav",
9
10
  "sounds/00006-NO_V2.wav",
@@ -4,6 +4,7 @@
4
4
  "version": "1.0.0",
5
5
  "license": "BSD-3-Clause",
6
6
  "credits": "Freedoom contributors (https://github.com/freedoom/freedoom)",
7
+ "category": "fps",
7
8
  "sounds": [
8
9
  "sounds/dsbarexp.wav",
9
10
  "sounds/dsbfg.wav",
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "kenney-interface",
3
+ "description": "Clean modern UI notification sounds by Kenney",
4
+ "version": "1.0.0",
5
+ "license": "CC0-1.0",
6
+ "credits": "Kenney (https://kenney.nl)",
7
+ "category": "ui",
8
+ "sounds": [
9
+ "sounds/back-1.wav",
10
+ "sounds/bong.wav",
11
+ "sounds/click-1.wav",
12
+ "sounds/close-1.wav",
13
+ "sounds/confirmation-1.wav",
14
+ "sounds/confirmation-2.wav",
15
+ "sounds/confirmation-3.wav",
16
+ "sounds/error-1.wav",
17
+ "sounds/error-2.wav",
18
+ "sounds/error-3.wav",
19
+ "sounds/glitch-1.wav",
20
+ "sounds/maximize-1.wav",
21
+ "sounds/maximize-2.wav",
22
+ "sounds/open-1.wav",
23
+ "sounds/pluck-1.wav",
24
+ "sounds/question-1.wav",
25
+ "sounds/question-2.wav",
26
+ "sounds/question-3.wav"
27
+ ],
28
+ "events": {
29
+ "done": [
30
+ "sounds/confirmation-1.wav",
31
+ "sounds/confirmation-2.wav",
32
+ "sounds/confirmation-3.wav",
33
+ "sounds/click-1.wav"
34
+ ],
35
+ "permission": [
36
+ "sounds/question-1.wav",
37
+ "sounds/question-2.wav",
38
+ "sounds/question-3.wav",
39
+ "sounds/pluck-1.wav"
40
+ ],
41
+ "complete": [
42
+ "sounds/maximize-1.wav",
43
+ "sounds/maximize-2.wav",
44
+ "sounds/bong.wav",
45
+ "sounds/open-1.wav"
46
+ ],
47
+ "error": [
48
+ "sounds/error-1.wav",
49
+ "sounds/error-2.wav",
50
+ "sounds/error-3.wav",
51
+ "sounds/glitch-1.wav"
52
+ ],
53
+ "blocked": [
54
+ "sounds/close-1.wav",
55
+ "sounds/back-1.wav"
56
+ ]
57
+ }
58
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "kenney-scifi",
3
+ "description": "Futuristic sci-fi notification sounds by Kenney",
4
+ "version": "1.0.0",
5
+ "license": "CC0-1.0",
6
+ "credits": "Kenney (https://kenney.nl)",
7
+ "category": "sci-fi",
8
+ "sounds": [
9
+ "sounds/computer-1.wav",
10
+ "sounds/computer-2.wav",
11
+ "sounds/computer-3.wav",
12
+ "sounds/door-close.wav",
13
+ "sounds/door-open.wav",
14
+ "sounds/engine-1.wav",
15
+ "sounds/explosion-1.wav",
16
+ "sounds/explosion-2.wav",
17
+ "sounds/forcefield-1.wav",
18
+ "sounds/forcefield-2.wav",
19
+ "sounds/impact-1.wav",
20
+ "sounds/impact-2.wav",
21
+ "sounds/laser-large.wav",
22
+ "sounds/laser-retro.wav",
23
+ "sounds/low-explosion.wav",
24
+ "sounds/slime.wav",
25
+ "sounds/thruster-1.wav",
26
+ "sounds/thruster-2.wav"
27
+ ],
28
+ "events": {
29
+ "done": [
30
+ "sounds/computer-1.wav",
31
+ "sounds/computer-2.wav",
32
+ "sounds/computer-3.wav",
33
+ "sounds/door-open.wav"
34
+ ],
35
+ "permission": [
36
+ "sounds/forcefield-1.wav",
37
+ "sounds/forcefield-2.wav",
38
+ "sounds/laser-retro.wav"
39
+ ],
40
+ "complete": [
41
+ "sounds/thruster-1.wav",
42
+ "sounds/thruster-2.wav",
43
+ "sounds/engine-1.wav"
44
+ ],
45
+ "error": [
46
+ "sounds/explosion-1.wav",
47
+ "sounds/explosion-2.wav",
48
+ "sounds/low-explosion.wav",
49
+ "sounds/impact-1.wav",
50
+ "sounds/impact-2.wav"
51
+ ],
52
+ "blocked": [
53
+ "sounds/door-close.wav",
54
+ "sounds/laser-large.wav",
55
+ "sounds/slime.wav"
56
+ ]
57
+ }
58
+ }
@@ -4,6 +4,7 @@
4
4
  "version": "1.0.0",
5
5
  "license": "GPL-2.0",
6
6
  "credits": "OpenArena contributors (http://openarena.ws)",
7
+ "category": "arena",
7
8
  "sounds": [
8
9
  "sounds/accuracy.wav",
9
10
  "sounds/assist.wav",
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "retro-8bit",
3
+ "description": "Classic 8-bit chiptune notification sounds",
4
+ "version": "1.0.0",
5
+ "license": "CC0-1.0",
6
+ "credits": "Juhani Junkala / SubspaceAudio (https://opengameart.org/content/512-sound-effects-8-bit-style)",
7
+ "category": "retro",
8
+ "sounds": [
9
+ "sounds/button-1.wav",
10
+ "sounds/button-2.wav",
11
+ "sounds/button-3.wav",
12
+ "sounds/coin-1.wav",
13
+ "sounds/coin-2.wav",
14
+ "sounds/coin-cluster.wav",
15
+ "sounds/coin-double.wav",
16
+ "sounds/damage-1.wav",
17
+ "sounds/error-1.wav",
18
+ "sounds/error-2.wav",
19
+ "sounds/error-3.wav",
20
+ "sounds/fanfare-1.wav",
21
+ "sounds/fanfare-2.wav",
22
+ "sounds/fanfare-3.wav",
23
+ "sounds/negative-1.wav",
24
+ "sounds/negative-2.wav",
25
+ "sounds/powerup-1.wav",
26
+ "sounds/powerup-2.wav"
27
+ ],
28
+ "events": {
29
+ "done": [
30
+ "sounds/coin-1.wav",
31
+ "sounds/coin-2.wav",
32
+ "sounds/coin-double.wav",
33
+ "sounds/coin-cluster.wav"
34
+ ],
35
+ "permission": [
36
+ "sounds/button-1.wav",
37
+ "sounds/button-2.wav",
38
+ "sounds/button-3.wav"
39
+ ],
40
+ "complete": [
41
+ "sounds/fanfare-1.wav",
42
+ "sounds/fanfare-2.wav",
43
+ "sounds/fanfare-3.wav",
44
+ "sounds/powerup-1.wav",
45
+ "sounds/powerup-2.wav"
46
+ ],
47
+ "error": [
48
+ "sounds/error-1.wav",
49
+ "sounds/error-2.wav",
50
+ "sounds/error-3.wav",
51
+ "sounds/damage-1.wav"
52
+ ],
53
+ "blocked": [
54
+ "sounds/negative-1.wav",
55
+ "sounds/negative-2.wav"
56
+ ]
57
+ }
58
+ }
@@ -4,6 +4,7 @@
4
4
  "version": "1.0.0",
5
5
  "license": "GPL-2.0",
6
6
  "credits": "Warzone 2100 Project (https://github.com/Warzone2100/warzone2100)",
7
+ "category": "military",
7
8
  "sounds": [
8
9
  "sounds/construction-completed.wav",
9
10
  "sounds/design-completed.wav",
@@ -4,6 +4,7 @@
4
4
  "version": "1.0.0",
5
5
  "license": "GPL-2.0",
6
6
  "credits": "Battle for Wesnoth contributors (https://github.com/wesnoth/wesnoth) — licensed under GPL v2+",
7
+ "category": "fantasy",
7
8
  "sounds": [
8
9
  "sounds/axe.wav",
9
10
  "sounds/bow.wav",
@@ -0,0 +1,75 @@
1
+ import { readConfig } from '../config.js';
2
+ import { listPacks } from '../packs.js';
3
+
4
+ function showHelp() {
5
+ console.log(`
6
+ Usage: pingthings browse [category]
7
+
8
+ Browse packs by category.
9
+
10
+ With no arguments, shows all categories with pack counts.
11
+ With a category, shows packs in that category.
12
+
13
+ Categories: military, arena, fantasy, sci-fi, fps, retro, ui, transport, spooky, racing
14
+
15
+ Examples:
16
+ pingthings browse List all categories
17
+ pingthings browse military Show military packs
18
+ pingthings browse sci-fi Show sci-fi packs
19
+ `);
20
+ }
21
+
22
+ export default function browse(args) {
23
+ if (args.includes('--help') || args.includes('-h')) {
24
+ showHelp();
25
+ return;
26
+ }
27
+
28
+ const config = readConfig();
29
+ const packs = listPacks();
30
+ const category = args[0];
31
+
32
+ if (!category) {
33
+ // Show all categories with counts
34
+ const categories = {};
35
+ for (const pack of packs) {
36
+ const cat = pack.category || 'other';
37
+ if (!categories[cat]) categories[cat] = [];
38
+ categories[cat].push(pack);
39
+ }
40
+
41
+ console.log('\nCategories:\n');
42
+ for (const [cat, catPacks] of Object.entries(categories).sort()) {
43
+ const totalSounds = catPacks.reduce((sum, p) => sum + p.soundCount, 0);
44
+ console.log(` ${cat.padEnd(14)} ${catPacks.length} pack${catPacks.length === 1 ? '' : 's'}, ${totalSounds} sounds`);
45
+ for (const p of catPacks) {
46
+ const active = p.name === config.activePack ? ' *' : ' ';
47
+ console.log(` ${active} └ ${p.name}`);
48
+ }
49
+ }
50
+ console.log('\n * = active pack');
51
+ console.log(' Run "pingthings browse <category>" to see details.\n');
52
+ return;
53
+ }
54
+
55
+ // Show packs in a specific category
56
+ const filtered = packs.filter(p => (p.category || 'other') === category);
57
+
58
+ if (filtered.length === 0) {
59
+ console.error(`No packs found in category: ${category}`);
60
+ const allCats = [...new Set(packs.map(p => p.category || 'other'))].sort();
61
+ console.error(`Available categories: ${allCats.join(', ')}`);
62
+ process.exit(1);
63
+ }
64
+
65
+ console.log(`\n${category} packs:\n`);
66
+ for (const pack of filtered) {
67
+ const active = pack.name === config.activePack ? ' *' : ' ';
68
+ console.log(`${active} ${pack.name} (${pack.soundCount} sounds)`);
69
+ if (pack.description) {
70
+ console.log(` ${pack.description}`);
71
+ }
72
+ console.log(` License: ${pack.license}`);
73
+ }
74
+ console.log('\n * = active pack\n');
75
+ }
@@ -0,0 +1,81 @@
1
+ import { readConfig } from '../config.js';
2
+ import { listPacks, getPackSounds } from '../packs.js';
3
+ import { basename } from 'node:path';
4
+
5
+ function showHelp() {
6
+ console.log(`
7
+ Usage: pingthings search <term>
8
+
9
+ Search across all packs by name, description, category, or sound filename.
10
+
11
+ Arguments:
12
+ term Text to search for (case-insensitive)
13
+
14
+ Examples:
15
+ pingthings search sword Find packs/sounds with "sword"
16
+ pingthings search military Find military category packs
17
+ pingthings search explosion Find explosion sounds
18
+ pingthings search coin Find coin/pickup sounds
19
+ `);
20
+ }
21
+
22
+ export default function search(args) {
23
+ const term = args[0];
24
+
25
+ if (!term || term === '--help' || term === '-h') {
26
+ showHelp();
27
+ if (!term) process.exit(1);
28
+ return;
29
+ }
30
+
31
+ const query = term.toLowerCase();
32
+ const config = readConfig();
33
+ const packs = listPacks();
34
+ const results = { packs: [], sounds: [] };
35
+
36
+ for (const pack of packs) {
37
+ // Check pack-level matches
38
+ const packMatch =
39
+ pack.name.toLowerCase().includes(query) ||
40
+ pack.description.toLowerCase().includes(query) ||
41
+ (pack.category || '').toLowerCase().includes(query);
42
+
43
+ if (packMatch) {
44
+ results.packs.push(pack);
45
+ }
46
+
47
+ // Check individual sounds
48
+ const sounds = getPackSounds(pack.name);
49
+ for (const sound of sounds) {
50
+ const name = basename(sound).toLowerCase();
51
+ if (name.includes(query)) {
52
+ results.sounds.push({ pack: pack.name, sound: basename(sound), path: sound });
53
+ }
54
+ }
55
+ }
56
+
57
+ if (results.packs.length === 0 && results.sounds.length === 0) {
58
+ console.log(`No results for "${term}".`);
59
+ return;
60
+ }
61
+
62
+ if (results.packs.length > 0) {
63
+ console.log(`\nPacks matching "${term}":\n`);
64
+ for (const pack of results.packs) {
65
+ const active = pack.name === config.activePack ? ' *' : ' ';
66
+ console.log(`${active} ${pack.name} (${pack.category}, ${pack.soundCount} sounds)`);
67
+ if (pack.description) {
68
+ console.log(` ${pack.description}`);
69
+ }
70
+ }
71
+ }
72
+
73
+ if (results.sounds.length > 0) {
74
+ console.log(`\nSounds matching "${term}":\n`);
75
+ for (const s of results.sounds) {
76
+ console.log(` ${s.pack} / ${s.sound}`);
77
+ }
78
+ }
79
+
80
+ console.log('');
81
+ }
@@ -0,0 +1,76 @@
1
+ import { readConfig } from '../config.js';
2
+ import { getPackSounds, resolvePack, getPackEvents, getEventSounds } from '../packs.js';
3
+ import { basename } from 'node:path';
4
+
5
+ function showHelp() {
6
+ console.log(`
7
+ Usage: pingthings sounds [pack]
8
+
9
+ List all individual sounds in a pack.
10
+
11
+ Arguments:
12
+ pack Pack to list sounds from (defaults to active pack)
13
+
14
+ Options:
15
+ --events Show sounds grouped by event mapping
16
+
17
+ Examples:
18
+ pingthings sounds List sounds in active pack
19
+ pingthings sounds openarena-announcer List sounds in a specific pack
20
+ pingthings sounds --events Show event mappings
21
+ `);
22
+ }
23
+
24
+ export default function sounds(args) {
25
+ if (args.includes('--help') || args.includes('-h')) {
26
+ showHelp();
27
+ return;
28
+ }
29
+
30
+ const showEvents = args.includes('--events');
31
+ const packName = args.find(a => !a.startsWith('-')) || readConfig().activePack;
32
+
33
+ const pack = resolvePack(packName);
34
+ if (!pack) {
35
+ console.error(`Pack not found: ${packName}`);
36
+ console.error('Run "pingthings list" to see available packs.');
37
+ process.exit(1);
38
+ }
39
+
40
+ const allSounds = getPackSounds(packName);
41
+
42
+ if (showEvents) {
43
+ const events = getPackEvents(packName);
44
+ console.log(`\n${packName} — sounds by event:\n`);
45
+
46
+ for (const event of events) {
47
+ const eventSounds = getEventSounds(packName, event);
48
+ console.log(` ${event}:`);
49
+ for (const s of eventSounds) {
50
+ console.log(` ${basename(s)}`);
51
+ }
52
+ }
53
+
54
+ // Show unmapped sounds
55
+ const mappedPaths = new Set();
56
+ for (const event of events) {
57
+ for (const s of getEventSounds(packName, event)) {
58
+ mappedPaths.add(s);
59
+ }
60
+ }
61
+ const unmapped = allSounds.filter(s => !mappedPaths.has(s));
62
+ if (unmapped.length > 0) {
63
+ console.log(' (unmapped):');
64
+ for (const s of unmapped) {
65
+ console.log(` ${basename(s)}`);
66
+ }
67
+ }
68
+ } else {
69
+ console.log(`\n${packName} — ${allSounds.length} sounds:\n`);
70
+ for (const s of allSounds) {
71
+ console.log(` ${basename(s)}`);
72
+ }
73
+ }
74
+
75
+ console.log('');
76
+ }
package/src/cli/theme.js CHANGED
@@ -57,11 +57,44 @@ const THEMES = {
57
57
  blocked: '0ad-civilizations',
58
58
  },
59
59
  },
60
+ 'professional': {
61
+ description: 'Clean and minimal — Kenney UI sounds for everything',
62
+ activePack: 'kenney-interface',
63
+ eventPacks: {
64
+ done: 'kenney-interface',
65
+ permission: 'kenney-interface',
66
+ complete: 'kenney-interface',
67
+ error: 'kenney-interface',
68
+ blocked: 'kenney-interface',
69
+ },
70
+ },
71
+ '8bit': {
72
+ description: 'Pure retro — 8-bit chiptune for everything',
73
+ activePack: 'retro-8bit',
74
+ eventPacks: {
75
+ done: 'retro-8bit',
76
+ permission: 'retro-8bit',
77
+ complete: 'retro-8bit',
78
+ error: 'retro-8bit',
79
+ blocked: 'retro-8bit',
80
+ },
81
+ },
82
+ 'space': {
83
+ description: 'Space station — Warzone 2100 + Kenney sci-fi',
84
+ activePack: 'kenney-scifi',
85
+ eventPacks: {
86
+ done: 'kenney-scifi',
87
+ permission: 'warzone2100-command',
88
+ complete: 'warzone2100-command',
89
+ error: 'kenney-scifi',
90
+ blocked: 'kenney-scifi',
91
+ },
92
+ },
60
93
  'chaos': {
61
94
  description: 'Random pack for every event — maximum variety',
62
95
  activePack: '7kaa-soldiers',
63
96
  eventPacks: {
64
- done: '7kaa-soldiers',
97
+ done: 'retro-8bit',
65
98
  permission: 'openarena-announcer',
66
99
  complete: 'warzone2100-command',
67
100
  error: 'freedoom-arsenal',
@@ -0,0 +1,48 @@
1
+ import { existsSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getConfigDir, readConfig, writeConfig } from '../config.js';
4
+
5
+ function showHelp() {
6
+ console.log(`
7
+ Usage: pingthings uninstall <pack>
8
+
9
+ Remove a user-installed sound pack.
10
+
11
+ Built-in packs cannot be uninstalled. Only packs in
12
+ ~/.config/pingthings/packs/ can be removed.
13
+
14
+ Arguments:
15
+ pack Name of the pack to remove
16
+ `);
17
+ }
18
+
19
+ export default function uninstall(args) {
20
+ const packName = args[0];
21
+
22
+ if (!packName || packName === '--help' || packName === '-h') {
23
+ showHelp();
24
+ if (!packName) process.exit(1);
25
+ return;
26
+ }
27
+
28
+ const userPackDir = join(getConfigDir(), 'packs', packName);
29
+
30
+ if (!existsSync(userPackDir)) {
31
+ console.error(`Pack not found in user packs: ${packName}`);
32
+ console.error('Only user-installed packs can be uninstalled.');
33
+ console.error('Run "pingthings list" to see all packs.');
34
+ process.exit(1);
35
+ }
36
+
37
+ rmSync(userPackDir, { recursive: true });
38
+
39
+ // If this was the active pack, reset to default
40
+ const config = readConfig();
41
+ if (config.activePack === packName) {
42
+ config.activePack = '7kaa-soldiers';
43
+ writeConfig(config);
44
+ console.log(`Active pack reset to: 7kaa-soldiers`);
45
+ }
46
+
47
+ console.log(`Uninstalled: ${packName}`);
48
+ }
package/src/packs.js CHANGED
@@ -16,8 +16,13 @@ function readManifest(packDir) {
16
16
  const manifestPath = join(packDir, 'manifest.json');
17
17
  if (!existsSync(manifestPath)) return null;
18
18
  try {
19
- return JSON.parse(readFileSync(manifestPath, 'utf8'));
20
- } catch {
19
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
20
+ if (!manifest.name) {
21
+ console.error(`Warning: manifest.json in ${packDir} is missing "name" field`);
22
+ }
23
+ return manifest;
24
+ } catch (err) {
25
+ console.error(`Warning: Failed to parse ${manifestPath}: ${err.message}`);
21
26
  return null;
22
27
  }
23
28
  }
@@ -52,6 +57,7 @@ function scanDirectory(dir, isBuiltIn) {
52
57
  description: manifest?.description || '',
53
58
  license: manifest?.license || 'Unknown',
54
59
  credits: manifest?.credits || '',
60
+ category: manifest?.category || 'other',
55
61
  location: packDir,
56
62
  soundCount: sounds.length,
57
63
  isBuiltIn,
package/src/player.js CHANGED
@@ -81,8 +81,12 @@ export function playSoundSync(filePath, volume) {
81
81
  const args = buildArgs(cmd, filePath, volume);
82
82
 
83
83
  try {
84
- execFileSync(cmd, args, { stdio: 'ignore', timeout: 10000 });
85
- } catch {
86
- // Timeout or error — don't crash
84
+ execFileSync(cmd, args, { stdio: ['ignore', 'ignore', 'pipe'], timeout: 5000 });
85
+ } catch (err) {
86
+ if (err.killed) {
87
+ console.error(`Warning: Sound playback timed out for ${filePath}`);
88
+ } else if (err.stderr?.length) {
89
+ console.error(`Warning: Playback error: ${err.stderr.toString().trim()}`);
90
+ }
87
91
  }
88
92
  }