Skip to content

Commit dd81dfd

Browse files
balloobclaude
andauthored
Refactor CLI to use subcommands for utility functions (#215)
## Summary Refactored the CLI to introduce dedicated subcommands (`audio-devices`, `servers`, `clients`) for utility functions, replacing the previous flag-based approach (`--list-audio-devices`, `--list-servers`, `--list-clients`). The old flags are deprecated but remain functional with warnings for backward compatibility. ## Key Changes - **New subcommand structure**: Added three new top-level subcommands with `list` subcommands: - `sendspin audio-devices list` (replaces `--list-audio-devices`) - `sendspin servers list` (replaces `--list-servers`) - `sendspin clients list` (replaces `--list-clients`) - **Parser updates**: - Added `audio-devices`, `servers`, and `clients` to `EXPLICIT_APPS` frozenset - Created dedicated argument parsers for each new subcommand with appropriate help text - Added routing logic in `main()` to handle the new subcommands - **Backward compatibility**: - Deprecated flags still work but now print a warning message directing users to the new subcommands - Old flags remain in the player argument parser with updated help text indicating deprecation - **Documentation updates**: - Updated help text throughout to reference new subcommand syntax - Updated README.md examples to use new subcommand format - Updated AGENTS.md to reflect new device enumeration approach - Updated systemd installation script to use new subcommand syntax - Added example output showing both old and new syntax in `list_audio_devices()` ## Implementation Details - New subcommands are handled before the main player/daemon/serve logic in `main()` - Each new subcommand validates that a `list` subcommand was provided; otherwise shows help - Deprecation warnings are printed to stdout when old flags are used - All three new subcommands follow the same pattern for consistency and extensibility https://claude.ai/code/session_018Mj2pQj7Lqu5YTGnB97E94 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ad439cc commit dd81dfd

4 files changed

Lines changed: 89 additions & 14 deletions

File tree

‎AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Project documentation, installation instructions, usage guide (including command
4848
Main entry point and orchestrator. Responsibilities:
4949
- **Argument parsing**: Commands (`serve`, `daemon`), flags (`--url`, `--name`, `--id`, `--audio-device`, `--static-delay-ms`)
5050
- **Command routing**: Routes to appropriate mode (TUI app, daemon, or server)
51-
- **Device enumeration**: Lists audio devices and servers via `--list-audio-devices` and `--list-servers`
51+
- **Device enumeration**: Lists audio devices and servers via `audio-devices list` and `servers list` subcommands
5252
- **Backward compatibility**: Handles deprecated `--headless` flag with warning
5353

5454
### `sendspin/tui/` (TUI Mode Package)
@@ -96,7 +96,7 @@ Daemon mode for headless operation (via `sendspin daemon` or deprecated `--headl
9696
### `sendspin/audio_devices.py`
9797
Audio device resolution and ALSA device listing. Responsibilities:
9898
- **Device resolution**: Resolves `--audio-device` argument by index, name prefix, or raw ALSA device name (via `resolve_audio_device()`)
99-
- **ALSA device listing**: Enumerates ALSA PCM devices via `aplay -L` for display in `--list-audio-devices`
99+
- **ALSA device listing**: Enumerates ALSA PCM devices via `aplay -L` for display in `sendspin audio-devices list`
100100
- **ALSA device fallback**: Opens ALSA plugin devices (dmix, plug) not enumerated by PortAudio, using safe defaults if PortAudio can't query device info
101101

102102
### `sendspin/audio.py`

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ sendspin --url ws://192.168.1.100:8080/sendspin
190190

191191
**List available servers on the network:**
192192
```bash
193-
sendspin --list-servers
193+
sendspin servers list
194194
```
195195

196196
### Client Identification
@@ -211,7 +211,7 @@ By default, the player uses your system's default audio output device. You can l
211211

212212
**List available audio devices:**
213213
```bash
214-
sendspin --list-audio-devices
214+
sendspin audio-devices list
215215
```
216216

217217
This displays all audio output devices with their IDs, channel configurations, and sample rates. The default device is marked.
@@ -231,7 +231,7 @@ sendspin --audio-device "MacBook"
231231
sendspin --audio-device dmixer
232232
```
233233

234-
This is useful for ALSA plugin devices (dmix, plug, etc.) that don't appear in `--list-audio-devices`. For example, in a dual mono setup where two daemons share a single sound card via dmix, each daemon can target a different ALSA device that routes to a specific channel:
234+
This is useful for ALSA plugin devices (dmix, plug, etc.) that may not appear in the numbered PortAudio device list (though they may be shown in the ALSA devices section on Linux). For example, in a dual mono setup where two daemons share a single sound card via dmix, each daemon can target a different ALSA device that routes to a specific channel:
235235

236236
```bash
237237
# Room 1: left channel via dmix

‎scripts/systemd/install-systemd.sh‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,9 @@ else
239239
fi
240240

241241
if [ -n "$DAEMON_DBUS" ]; then
242-
sudo -u "$DAEMON_USER" env XDG_RUNTIME_DIR="$DAEMON_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$DAEMON_DBUS" "$SENDSPIN_BIN" --list-audio-devices 2>&1 | head -n -2
242+
sudo -u "$DAEMON_USER" env XDG_RUNTIME_DIR="$DAEMON_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$DAEMON_DBUS" "$SENDSPIN_BIN" audio-devices list 2>&1 | grep -v -e "^To select an audio device" -e "^ sendspin"
243243
else
244-
sudo -u "$DAEMON_USER" "$SENDSPIN_BIN" --list-audio-devices 2>&1 | head -n -2 || echo -e "${D}(Audio devices will be detected when daemon starts)${N}"
244+
sudo -u "$DAEMON_USER" "$SENDSPIN_BIN" audio-devices list 2>&1 | grep -v -e "^To select an audio device" -e "^ sendspin" || echo -e "${D}(Audio devices will be detected when daemon starts)${N}"
245245
fi
246246

247247
DEVICE=$(prompt_input "Audio device" "default")

‎sendspin/cli.py‎

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
• Other systems: https://www.portaudio.com/"""
4242

4343
PLAYER_APP_SENTINEL = "player"
44-
EXPLICIT_APPS = frozenset({PLAYER_APP_SENTINEL, "daemon", "serve"})
44+
EXPLICIT_APPS = frozenset(
45+
{PLAYER_APP_SENTINEL, "daemon", "serve", "audio-devices", "servers", "clients"}
46+
)
4547
TOP_LEVEL_ACTIONS = frozenset({"-h", "--help", "--version"})
4648

4749

@@ -92,6 +94,7 @@ def list_audio_devices() -> None:
9294
)
9395
if devices:
9496
print(f"\nTo select an audio device:\n sendspin --audio-device {devices[0].index}")
97+
print(f" sendspin daemon --audio-device {devices[0].index}")
9598

9699
if sys.platform.startswith("linux"):
97100
alsa_devices = list_alsa_devices()
@@ -143,7 +146,7 @@ def _add_player_runtime_options(target: ArgumentTarget, *, suppress_defaults: bo
143146
help=(
144147
"Audio output device by index (e.g., 0, 1, 2), name prefix (e.g., 'MacBook'), "
145148
"or raw ALSA device name (e.g., 'dmixer', 'olohuone') for plugin devices like dmix. "
146-
"Use --list-audio-devices to see enumerated devices."
149+
"Use 'sendspin audio-devices list' to see enumerated devices."
147150
),
148151
)
149152
target.add_argument(
@@ -206,19 +209,19 @@ def _add_player_actions(target: ArgumentTarget, *, suppress_defaults: bool = Fal
206209
"--list-audio-devices",
207210
action="store_true",
208211
default=argparse.SUPPRESS if suppress_defaults else False,
209-
help="List available audio output devices and exit",
212+
help="(deprecated: use 'sendspin audio-devices list') List audio devices and exit",
210213
)
211214
target.add_argument(
212215
"--list-servers",
213216
action="store_true",
214217
default=argparse.SUPPRESS if suppress_defaults else False,
215-
help="Discover and list available Sendspin servers on the network",
218+
help="(deprecated: use 'sendspin servers list') List Sendspin servers and exit",
216219
)
217220
target.add_argument(
218221
"--list-clients",
219222
action="store_true",
220223
default=argparse.SUPPRESS if suppress_defaults else False,
221-
help="Discover and list available Sendspin clients on the network",
224+
help="(deprecated: use 'sendspin clients list') List Sendspin clients and exit",
222225
)
223226
target.add_argument(
224227
"--headless",
@@ -357,7 +360,7 @@ def _build_parser() -> argparse.ArgumentParser:
357360
default=None,
358361
help=(
359362
"Audio output device by index (e.g., 0, 1, 2) or name prefix (e.g., 'MacBook'). "
360-
"Use --list-audio-devices to see available devices."
363+
"Use 'sendspin audio-devices list' to see available devices."
361364
),
362365
)
363366
daemon_parser.add_argument(
@@ -418,6 +421,57 @@ def _build_parser() -> argparse.ArgumentParser:
418421
help="Product name reported in the client hello (defaults to auto-detected OS/platform name)",
419422
)
420423

424+
# audio-devices subcommand
425+
audio_devices_parser = subparsers.add_parser(
426+
"audio-devices",
427+
help="Audio device utilities",
428+
description="Audio device utilities.",
429+
)
430+
audio_devices_sub = audio_devices_parser.add_subparsers(
431+
dest="audio_devices_command",
432+
title="Commands",
433+
required=True,
434+
)
435+
audio_devices_sub.add_parser(
436+
"list",
437+
help="List available audio output devices",
438+
description="List all available audio output devices and exit.",
439+
)
440+
441+
# servers subcommand
442+
servers_parser = subparsers.add_parser(
443+
"servers",
444+
help="Server discovery utilities",
445+
description="Server discovery utilities.",
446+
)
447+
servers_sub = servers_parser.add_subparsers(
448+
dest="servers_command",
449+
title="Commands",
450+
required=True,
451+
)
452+
servers_sub.add_parser(
453+
"list",
454+
help="Discover and list available Sendspin servers on the network",
455+
description="Discover and list available Sendspin servers on the network.",
456+
)
457+
458+
# clients subcommand
459+
clients_parser = subparsers.add_parser(
460+
"clients",
461+
help="Client discovery utilities",
462+
description="Client discovery utilities.",
463+
)
464+
clients_sub = clients_parser.add_subparsers(
465+
dest="clients_command",
466+
title="Commands",
467+
required=True,
468+
)
469+
clients_sub.add_parser(
470+
"list",
471+
help="Discover and list available Sendspin clients on the network",
472+
description="Discover and list available Sendspin clients on the network.",
473+
)
474+
421475
return parser
422476

423477

@@ -613,17 +667,38 @@ def main() -> int:
613667
traceback.print_exc()
614668
return 1
615669

670+
# Handle utility subcommands
671+
if args.command == "audio-devices":
672+
if args.audio_devices_command == "list":
673+
list_audio_devices()
674+
return 0
675+
676+
if args.command == "servers":
677+
if args.servers_command == "list":
678+
asyncio.run(list_servers())
679+
return 0
680+
681+
if args.command == "clients":
682+
if args.clients_command == "list":
683+
asyncio.run(list_clients())
684+
return 0
685+
616686
if args.command == PLAYER_APP_SENTINEL:
617-
# Handle player-only actions before starting async runtime.
687+
# Deprecated flags - route to new subcommands with a warning.
618688
if args.list_audio_devices:
689+
print(
690+
"Warning: --list-audio-devices is deprecated. Use 'sendspin audio-devices list'.\n"
691+
)
619692
list_audio_devices()
620693
return 0
621694

622695
if args.list_servers:
696+
print("Warning: --list-servers is deprecated. Use 'sendspin servers list'.\n")
623697
asyncio.run(list_servers())
624698
return 0
625699

626700
if args.list_clients:
701+
print("Warning: --list-clients is deprecated. Use 'sendspin clients list'.\n")
627702
asyncio.run(list_clients())
628703
return 0
629704

0 commit comments

Comments
 (0)