diff --git a/src/vban_cli/app.py b/src/vban_cli/app.py index c437c23..f69a1c4 100644 --- a/src/vban_cli/app.py +++ b/src/vban_cli/app.py @@ -3,16 +3,21 @@ from typing import Annotated import vban_cmd from cyclopts import App, Argument, Parameter, config +from rich.traceback import install as install_rich_traceback from . import __version__ as version from . import bus, command, console, recorder, strip from .context import Context +from .error import VbanCLIConnectionError app = App( config=config.Env( 'VBAN_CLI_', ), # Environment variable prefix for configuration parameters version=version, + console=console.out, + error_console=console.err, + exit_on_error=True, ) app.command(strip.app.meta, name='strip') app.command(bus.app.meta, name='bus') @@ -20,6 +25,8 @@ app.command(command.app, name='command') app.command(recorder.app, name='recorder') app.register_install_completion_command() +install_rich_traceback(console=console.err) + @Parameter(name='*') @dataclass @@ -43,14 +50,17 @@ def launcher( if command.__name__ == 'sendtext': disable_rt_listeners = True - with vban_cmd.api( - vban_config.kind, - ip=vban_config.host, - port=vban_config.port, - streamname=vban_config.streamname, - disable_rt_listeners=disable_rt_listeners, - ) as client: - return command(*bound.args, **bound.kwargs, ctx=Context(client=client)) + try: + with vban_cmd.api( + vban_config.kind, + ip=vban_config.host, + port=vban_config.port, + streamname=vban_config.streamname, + disable_rt_listeners=disable_rt_listeners, + ) as client: + return command(*bound.args, **bound.kwargs, ctx=Context(client=client)) + except vban_cmd.error.VBANCMDConnectionError as e: + raise VbanCLIConnectionError(str(e)) from e @app.command(name='sendtext') @@ -62,12 +72,8 @@ def sendtext( ): """Send a text command to the current Voicemeeter/Matrix instance.""" if resp := ctx.client.sendtext(text): - console.out.print(resp) + app.console.print(resp) def run(): - try: - app.meta() - except Exception as e: - console.err.print(f'Error: {e}') - return e.code + app.meta() diff --git a/src/vban_cli/bus.py b/src/vban_cli/bus.py index da9b0e6..09a24ee 100644 --- a/src/vban_cli/bus.py +++ b/src/vban_cli/bus.py @@ -2,7 +2,6 @@ from typing import Annotated, Literal, Optional from cyclopts import App, Argument, Parameter -from . import console from .context import Context from .help import BusHelpFormatter @@ -46,7 +45,7 @@ def mono( If provided, sets the mono state to this value. If not provided, the current mono state is printed. """ if new_value is None: - console.out.print(['off', 'mono', 'stereoreverse'][ctx.client.bus[index].mono]) + app.console.print(['off', 'mono', 'stereoreverse'][ctx.client.bus[index].mono]) return ctx.client.bus[index].mono = ['off', 'mono', 'stereoreverse'].index(new_value) @@ -66,7 +65,7 @@ def mute( If provided, sets the mute state to this value. If not provided, the current mute state is printed. """ if new_value is None: - console.out.print(ctx.client.bus[index].mute) + app.console.print(ctx.client.bus[index].mute) return ctx.client.bus[index].mute = new_value @@ -104,6 +103,6 @@ def mode( If provided, sets the bus mode to this value. If not provided, the current bus mode is printed. """ if type_ is None: - console.out.print(ctx.client.bus[index].mode.get()) + app.console.print(ctx.client.bus[index].mode.get()) return setattr(ctx.client.bus[index].mode, type_, True) diff --git a/src/vban_cli/command.py b/src/vban_cli/command.py index 888be79..5dbfd64 100644 --- a/src/vban_cli/command.py +++ b/src/vban_cli/command.py @@ -2,7 +2,6 @@ from typing import Annotated from cyclopts import App, Parameter -from . import console from .context import Context from .help import BaseHelpFormatter @@ -16,7 +15,7 @@ def show( ): """Bring the Voicemeeter GUI to the foreground.""" ctx.client.command.show() - console.out.print('Voicemeeter GUI should now be in the foreground.') + app.console.print('Voicemeeter GUI should now be in the foreground.') @app.command(name='hide') @@ -26,7 +25,7 @@ def hide( ): """Send the Voicemeeter GUI to the background.""" ctx.client.command.hide() - console.out.print('Voicemeeter GUI should now be in the background.') + app.console.print('Voicemeeter GUI should now be in the background.') @app.command(name='shutdown') @@ -36,7 +35,7 @@ def shutdown( ): """Shut down Voicemeeter.""" ctx.client.command.shutdown() - console.out.print('Voicemeeter should now be shut down.') + app.console.print('Voicemeeter should now be shut down.') @app.command(name='restart') @@ -46,4 +45,4 @@ def restart( ): """Restart the Voicemeeter engine.""" ctx.client.command.restart() - console.out.print('Voicemeeter engine should now be restarting.') + app.console.print('Voicemeeter engine should now be restarting.') diff --git a/src/vban_cli/comp.py b/src/vban_cli/comp.py index ce3b295..758217c 100644 --- a/src/vban_cli/comp.py +++ b/src/vban_cli/comp.py @@ -39,7 +39,7 @@ def knob( """ if new_knob is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(ctx.client.strip[index].comp.knob) + # app.console.print(ctx.client.strip[index].comp.knob) return ctx.client.strip[index].comp.knob = new_knob @@ -60,6 +60,6 @@ def input_gain( """ if new_gain is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(ctx.client.strip[index].comp.gainin) + # app.console.print(ctx.client.strip[index].comp.gainin) return ctx.client.strip[index].comp.gainin = new_gain diff --git a/src/vban_cli/denoiser.py b/src/vban_cli/denoiser.py index 7b2b918..ac22ca9 100644 --- a/src/vban_cli/denoiser.py +++ b/src/vban_cli/denoiser.py @@ -39,6 +39,6 @@ def knob( """ if new_knob is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(ctx.client.strip[index].denoiser.knob) + # app.console.print(ctx.client.strip[index].denoiser.knob) return ctx.client.strip[index].denoiser.knob = new_knob diff --git a/src/vban_cli/eq.py b/src/vban_cli/eq.py index 389b581..92c2c3a 100644 --- a/src/vban_cli/eq.py +++ b/src/vban_cli/eq.py @@ -48,7 +48,7 @@ def on( """ if new_state is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(target.on) + # app.console.print(target.on) return target.on = new_state @@ -86,7 +86,7 @@ def cell_on( """ if new_state is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(target.on) + # app.console.print(target.on) return target.on = new_state @@ -106,7 +106,7 @@ def cell_freq( """ if new_freq is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(target.f) + # app.console.print(target.f) return target.f = new_freq @@ -126,7 +126,7 @@ def cell_gain( """ if new_gain is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(target.gain) + # app.console.print(target.gain) return target.gain = new_gain @@ -146,7 +146,7 @@ def cell_q( """ if new_q is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(target.q) + # app.console.print(target.q) return target.q = new_q @@ -166,6 +166,6 @@ def cell_type( """ if new_type is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(target.type) + # app.console.print(target.type) return target.type = new_type diff --git a/src/vban_cli/error.py b/src/vban_cli/error.py new file mode 100644 index 0000000..d3132aa --- /dev/null +++ b/src/vban_cli/error.py @@ -0,0 +1,14 @@ +class VbanCLIError(Exception): + """Base class for exceptions in this module.""" + + def __init__(self, message: str, code: int = 1): + super().__init__(message) + self.code = code + + +class VbanCLIConnectionError(VbanCLIError): + """Exception raised for connection errors.""" + + +class VbanCLIValidationError(VbanCLIError): + """Exception raised for validation errors.""" diff --git a/src/vban_cli/gainlayer.py b/src/vban_cli/gainlayer.py index 3830080..f084d3d 100644 --- a/src/vban_cli/gainlayer.py +++ b/src/vban_cli/gainlayer.py @@ -2,7 +2,6 @@ from typing import Annotated from cyclopts import App, Argument, Parameter -from . import console from .context import Context from .help import GainlayerHelpFormatter @@ -43,6 +42,6 @@ def level( If provided, sets the level to this value. If not provided, the current level is printed. """ if new_level is None: - console.out.print(ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain) + app.console.print(ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain) return ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain = new_level diff --git a/src/vban_cli/gate.py b/src/vban_cli/gate.py index fbea466..8a674e2 100644 --- a/src/vban_cli/gate.py +++ b/src/vban_cli/gate.py @@ -39,7 +39,7 @@ def knob( """ if new_knob is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(ctx.client.strip[index].gate.knob) + # app.console.print(ctx.client.strip[index].gate.knob) return ctx.client.strip[index].gate.knob = new_knob @@ -60,6 +60,6 @@ def threshold( """ if new_threshold is None: # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. - # console.out.print(ctx.client.strip[index].gate.threshold) + # app.console.print(ctx.client.strip[index].gate.threshold) return ctx.client.strip[index].gate.threshold = new_threshold diff --git a/src/vban_cli/recorder.py b/src/vban_cli/recorder.py index 1cfb744..e5b5e2f 100644 --- a/src/vban_cli/recorder.py +++ b/src/vban_cli/recorder.py @@ -3,7 +3,7 @@ from typing import Annotated from cyclopts import App, Parameter, validators -from . import console, validation +from . import validation from .context import Context from .help import BaseHelpFormatter @@ -17,7 +17,7 @@ def play( ): """Start the recorder playback.""" ctx.client.recorder.play() - console.out.print('Recorder playback started.') + app.console.print('Recorder playback started.') @app.command(name='pause') @@ -27,7 +27,7 @@ def pause( ): """Pause the recorder playback.""" ctx.client.recorder.stop() - console.out.print('Recorder playback paused.') + app.console.print('Recorder playback paused.') @app.command(name='stop') @@ -40,7 +40,7 @@ def stop( ctx.client.recorder.goto('00:00:00') # We have no way of knowing if the recorder was playing or recording, so we print a generic message. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 4. - console.out.print('Recorder stopped.') + app.console.print('Recorder stopped.') @app.command(name='replay') @@ -50,7 +50,7 @@ def replay( ): """Replay the recorder playback.""" ctx.client.recorder.replay() - console.out.print('Recorder playback replay started.') + app.console.print('Recorder playback replay started.') @app.command(name='record') @@ -60,7 +60,7 @@ def record( ): """Start recording.""" ctx.client.recorder.record() - console.out.print('Recorder recording started.') + app.console.print('Recorder recording started.') @app.command(name='pause-recording') @@ -70,7 +70,7 @@ def pause_recording( ): """Pause the recorder recording.""" ctx.client.recorder.pause() - console.out.print('Recorder recording paused.') + app.console.print('Recorder recording paused.') @app.command(name='ff') @@ -80,7 +80,7 @@ def ff( ): """Fast forward the recorder playback.""" ctx.client.recorder.ff() - console.out.print('Recorder playback fast forwarded.') + app.console.print('Recorder playback fast forwarded.') @app.command(name='rew') @@ -90,7 +90,7 @@ def rew( ): """Rewind the recorder playback.""" ctx.client.recorder.rew() - console.out.print('Recorder playback rewound.') + app.console.print('Recorder playback rewound.') @app.command(name='load') @@ -110,7 +110,7 @@ def load( note: This command may only work if vban-cli is running on localhost and may not work if vban-cli is running on a remote server.""" ctx.client.recorder.load(file_path) - console.out.print(f'Loaded file: {file_path}') + app.console.print(f'Loaded file: {file_path}') @app.command(name='goto') @@ -128,4 +128,4 @@ def goto( ): """Go to a specific timestamp in the recorder playback.""" ctx.client.recorder.goto(time_string) - console.out.print(f'Went to timestamp {time_string} in recorder playback.') + app.console.print(f'Went to timestamp {time_string} in recorder playback.') diff --git a/src/vban_cli/strip.py b/src/vban_cli/strip.py index d476ae5..8f5011b 100644 --- a/src/vban_cli/strip.py +++ b/src/vban_cli/strip.py @@ -2,7 +2,7 @@ from typing import Annotated, Optional from cyclopts import App, Argument, Parameter -from . import comp, console, denoiser, eq, gainlayer, gate +from . import comp, denoiser, eq, gainlayer, gate from .context import Context from .help import StripHelpFormatter @@ -47,7 +47,7 @@ def mono( If provided, sets the mono state to this value. If not provided, the current mono state is printed. """ if new_state is None: - console.out.print(ctx.client.strip[index].mono) + app.console.print(ctx.client.strip[index].mono) return ctx.client.strip[index].mono = new_state @@ -67,7 +67,7 @@ def solo( If provided, sets the solo state to this value. If not provided, the current solo state is printed. """ if new_state is None: - console.out.print(ctx.client.strip[index].solo) + app.console.print(ctx.client.strip[index].solo) return ctx.client.strip[index].solo = new_state @@ -87,7 +87,7 @@ def mute( If provided, sets the mute state to this value. If not provided, the current mute state is printed. """ if new_state is None: - console.out.print(ctx.client.strip[index].mute) + app.console.print(ctx.client.strip[index].mute) return ctx.client.strip[index].mute = new_state @@ -107,7 +107,7 @@ def gain( If provided, sets the gain to this value. If not provided, the current gain is printed. """ if new_value is None: - console.out.print(ctx.client.strip[index].gain) + app.console.print(ctx.client.strip[index].gain) return ctx.client.strip[index].gain = new_value @@ -127,7 +127,7 @@ def a1( If provided, sets the A1 state to this value. If not provided, the current A1 state is printed. """ if new_value is None: - console.out.print(ctx.client.strip[index].A1) + app.console.print(ctx.client.strip[index].A1) return ctx.client.strip[index].A1 = new_value @@ -147,7 +147,7 @@ def a2( If provided, sets the A2 state to this value. If not provided, the current A2 state is printed. """ if new_value is None: - console.out.print(ctx.client.strip[index].A2) + app.console.print(ctx.client.strip[index].A2) return ctx.client.strip[index].A2 = new_value @@ -167,7 +167,7 @@ def a3( If provided, sets the A3 state to this value. If not provided, the current A3 state is printed. """ if new_value is None: - console.out.print(ctx.client.strip[index].A3) + app.console.print(ctx.client.strip[index].A3) return ctx.client.strip[index].A3 = new_value @@ -187,7 +187,7 @@ def a4( If provided, sets the A4 state to this value. If not provided, the current A4 state is printed. """ if new_value is None: - console.out.print(ctx.client.strip[index].A4) + app.console.print(ctx.client.strip[index].A4) return ctx.client.strip[index].A4 = new_value @@ -207,7 +207,7 @@ def a5( If provided, sets the A5 state to this value. If not provided, the current A5 state is printed. """ if new_value is None: - console.out.print(ctx.client.strip[index].A5) + app.console.print(ctx.client.strip[index].A5) return ctx.client.strip[index].A5 = new_value @@ -227,7 +227,7 @@ def b1( If provided, sets the B1 state to this value. If not provided, the current B1 state is printed. """ if new_value is None: - console.out.print(ctx.client.strip[index].B1) + app.console.print(ctx.client.strip[index].B1) return ctx.client.strip[index].B1 = new_value @@ -247,7 +247,7 @@ def b2( If provided, sets the B2 state to this value. If not provided, the current B2 state is printed. """ if new_value is None: - console.out.print(ctx.client.strip[index].B2) + app.console.print(ctx.client.strip[index].B2) return ctx.client.strip[index].B2 = new_value @@ -267,6 +267,6 @@ def b3( If provided, sets the B3 state to this value. If not provided, the current B3 state is printed. """ if new_value is None: - console.out.print(ctx.client.strip[index].B3) + app.console.print(ctx.client.strip[index].B3) return ctx.client.strip[index].B3 = new_value diff --git a/src/vban_cli/validation.py b/src/vban_cli/validation.py index 451692f..69653af 100644 --- a/src/vban_cli/validation.py +++ b/src/vban_cli/validation.py @@ -1,9 +1,11 @@ import re +from .error import VbanCLIValidationError + def is_valid_time_string(type_, value: str) -> str: """Validate if the given string is a valid time format (HH:MM:SS).""" pattern = r'^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$' if not re.match(pattern, value): - raise ValueError('Invalid time format. Expected HH:MM:SS.') + raise VbanCLIValidationError('Invalid time format. Expected HH:MM:SS.') return value