Compare commits

..

No commits in common. "main" and "v0.23.0" have entirely different histories.

33 changed files with 388 additions and 773 deletions

View File

@ -1,20 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"

View File

@ -19,10 +19,10 @@ jobs:
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v4
with: with:
python-version: '3.11' python-version: '3.11'
cache: 'pip' cache: 'pip'

View File

@ -13,7 +13,7 @@ jobs:
ruff: ruff:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3 - uses: astral-sh/ruff-action@v3
with: with:
args: 'format --check --diff' args: 'format --check --diff'

View File

@ -5,29 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [0.24.8] - 2026-02-07 # [0.23.0] - 2026-01-09
### Changed
- --debug flag removed and replaced with --loglevel. See [Flags](https://github.com/onyx-and-iris/obsws-cli/tree/main?tab=readme-ov-file#flags). This gives the user more control over the level of logging. The default level has been set to WARNING.
### Fixed
- shell completion now works, see [Shell Completion](https://github.com/onyx-and-iris/obsws-cli/tree/main?tab=readme-ov-file#shell-completion). Unfortunately, command aliases in the help output are no longer present as it was breaking shell completion. However, the aliases do still work. See [issue #3](https://github.com/onyx-and-iris/obsws-cli/issues/3)
# [0.24.6] - 2026-01-26
### Changed
- environment variables should now be prefixed with 'OBSWS_CLI_', see [Environment Variables](https://github.com/onyx-and-iris/obsws-cli?tab=readme-ov-file#environment-variables)
# [0.24.0] - 2026-01-09
### Added ### Added
- new subcommands added to input, see [Input](https://github.com/onyx-and-iris/obsws-cli?tab=readme-ov-file#input) - new subcommands added to input, see [Input](https://github.com/onyx-and-iris/obsws-cli?tab=readme-ov-file#input)
- settings command group, see [Settings](https://github.com/onyx-and-iris/obsws-cli?tab=readme-ov-file#settings) - settings command group, see [Settings](https://github.com/onyx-and-iris/obsws-cli?tab=readme-ov-file#settings)
- media command group, see [Media](https://github.com/onyx-and-iris/obsws-cli?tab=readme-ov-file#media)
# [0.20.0] - 2025-07-14 # [0.20.0] - 2025-07-14

View File

@ -1,6 +1,6 @@
# obsws-cli # obsws-cli
[![Hatch project](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pypa/hatch/master/docs/assets/badge/v0.json)](https://github.com/pypa/hatch) [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@ -16,7 +16,6 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Style](#style) - [Style](#style)
- [Commands](#root-typer) - [Commands](#root-typer)
- [Shell Completion](#shell-completion)
- [License](#license) - [License](#license)
## Requirements ## Requirements
@ -49,8 +48,6 @@ The CLI should now be discoverable as `obsws-cli`
- --password/-p: Websocket password - --password/-p: Websocket password
- --timeout/-T: Websocket timeout - --timeout/-T: Websocket timeout
- --version/-v: Print the obsws-cli version - --version/-v: Print the obsws-cli version
- --loglevel/-l: Set the application's logging level
- One of *NOTSET, DEBUG, INFO, WARN, WARNING, ERROR, CRITICAL, FATAL*
Pass `--host`, `--port` and `--password` as flags on the root command, for example: Pass `--host`, `--port` and `--password` as flags on the root command, for example:
@ -65,10 +62,9 @@ Store and load environment variables from:
- `user home directory / .config / obsws-cli / obsws.env` - `user home directory / .config / obsws-cli / obsws.env`
```env ```env
OBSWS_CLI_HOST=localhost OBS_HOST=localhost
OBSWS_CLI_PORT=4455 OBS_PORT=4455
OBSWS_CLI_PASSWORD=<websocket password> OBS_PASSWORD=<websocket password>
OBSWS_CLI_LOGLEVEL=DEBUG
``` ```
Flags can be used to override environment variables. Flags can be used to override environment variables.
@ -100,8 +96,8 @@ obsws-cli --style="cyan" --no-border sceneitem list
Or with environment variables: Or with environment variables:
```env ```env
OBSWS_CLI_STYLE=cyan OBS_STYLE=cyan
OBSWS_CLI_STYLE_NO_BORDER=true OBS_STYLE_NO_BORDER=true
``` ```
## Root Typer ## Root Typer
@ -780,52 +776,6 @@ obsws-cli settings video
obsws-cli settings video --base-width=1920 --base-height=1080 obsws-cli settings video --base-width=1920 --base-height=1080
``` ```
#### Media
- cursor: Get/set the cursor position of a media input.
- args: InputName
*optional*
- TimeString
```console
obsws-cli media cursor "Media"
obsws-cli media cursor "Media" "00:08:30"
```
- play: Plays a media input.
```console
obsws-cli media play "Media"
```
- pause: Pauses a media input.
```console
obsws-cli media pause "Media"
```
- stop: Stops a media input.
```console
obsws-cli media stop "Media"
```
- restart: Restarts a media input.
```console
obsws-cli media restart "Media"
```
## Shell Completion
```console
obsws-cli --install-completion
```
Currently supported shells: *bash* *zsh* *fish* *powershell*
## License ## License

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2025-present onyx-and-iris <code@onyxandiris.online> # SPDX-FileCopyrightText: 2025-present onyx-and-iris <code@onyxandiris.online>
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
__version__ = '0.24.8' __version__ = '0.23.0'

View File

@ -1,5 +1,7 @@
"""module defining a custom group class for handling command name aliases.""" """module defining a custom group class for handling command name aliases."""
import re
import typer import typer
@ -22,8 +24,6 @@ class RootTyperAliasGroup(typer.core.TyperGroup):
cmd_name = 'hotkey' cmd_name = 'hotkey'
case 'i': case 'i':
cmd_name = 'input' cmd_name = 'input'
case 'm':
cmd_name = 'media'
case 'prf': case 'prf':
cmd_name = 'profile' cmd_name = 'profile'
case 'prj': case 'prj':
@ -40,8 +40,6 @@ class RootTyperAliasGroup(typer.core.TyperGroup):
cmd_name = 'sceneitem' cmd_name = 'sceneitem'
case 'ss': case 'ss':
cmd_name = 'screenshot' cmd_name = 'screenshot'
case 'set':
cmd_name = 'settings'
case 'st': case 'st':
cmd_name = 'stream' cmd_name = 'stream'
case 'sm': case 'sm':
@ -51,3 +49,25 @@ class RootTyperAliasGroup(typer.core.TyperGroup):
case 'vc': case 'vc':
cmd_name = 'virtualcam' cmd_name = 'virtualcam'
return super().get_command(ctx, cmd_name) return super().get_command(ctx, cmd_name)
class SubTyperAliasGroup(typer.core.TyperGroup):
"""A custom group class to handle command name aliases for sub typers."""
_CMD_SPLIT_P = re.compile(r' ?[,|] ?')
def __init__(self, *args, **kwargs):
"""Initialize the AliasGroup."""
super().__init__(*args, **kwargs)
self.no_args_is_help = True
def get_command(self, ctx, cmd_name):
"""Get a command by name."""
cmd_name = self._group_cmd_name(cmd_name)
return super().get_command(ctx, cmd_name)
def _group_cmd_name(self, default_name):
for cmd in self.commands.values():
if cmd.name and default_name in self._CMD_SPLIT_P.split(cmd.name):
return cmd.name
return default_name

View File

@ -2,7 +2,6 @@
import importlib import importlib
import logging import logging
import pkgutil
from typing import Annotated from typing import Annotated
import obsws_python as obsws import obsws_python as obsws
@ -10,15 +9,31 @@ import typer
from obsws_cli.__about__ import __version__ as version from obsws_cli.__about__ import __version__ as version
from . import commands, console, envconfig, styles from . import config, console, styles
from .alias import RootTyperAliasGroup from .alias import RootTyperAliasGroup
app = typer.Typer(cls=RootTyperAliasGroup) app = typer.Typer(cls=RootTyperAliasGroup)
for importer, modname, ispkg in pkgutil.iter_modules( for sub_typer in (
commands.__path__, commands.__name__ + '.' 'filter',
'group',
'hotkey',
'input',
'profile',
'projector',
'record',
'replaybuffer',
'scene',
'scenecollection',
'sceneitem',
'screenshot',
'stream',
'studiomode',
'text',
'virtualcam',
'settings',
): ):
subtyper = importlib.import_module(modname) module = importlib.import_module(f'.{sub_typer}', package=__package__)
app.add_typer(subtyper.app, name=modname.split('.')[-1]) app.add_typer(module.app, name=sub_typer)
def version_callback(value: bool): def version_callback(value: bool):
@ -28,21 +43,11 @@ def version_callback(value: bool):
raise typer.Exit() raise typer.Exit()
def setup_logging(loglevel: str): def setup_logging(debug: bool):
"""Set up logging for the application.""" """Set up logging for the application."""
level_map = logging.getLevelNamesMapping() log_level = logging.DEBUG if debug else logging.CRITICAL
try:
level_int = level_map[loglevel.upper()]
except KeyError:
possible_levels = ', '.join(
sorted(level_map.keys(), key=lambda k: level_map[k])
)
raise typer.BadParameter(
f'Invalid log level: {loglevel}. Valid options are: {possible_levels}'
) from None
logging.basicConfig( logging.basicConfig(
level=level_int, level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
) )
@ -64,62 +69,62 @@ def main(
typer.Option( typer.Option(
'--host', '--host',
'-H', '-H',
envvar='OBSWS_CLI_HOST', envvar='OBS_HOST',
help='WebSocket host', help='WebSocket host',
show_default='localhost', show_default='localhost',
), ),
] = envconfig.get('host'), ] = config.get('host'),
port: Annotated[ port: Annotated[
int, int,
typer.Option( typer.Option(
'--port', '--port',
'-P', '-P',
envvar='OBSWS_CLI_PORT', envvar='OBS_PORT',
help='WebSocket port', help='WebSocket port',
show_default=4455, show_default=4455,
), ),
] = envconfig.get('port'), ] = config.get('port'),
password: Annotated[ password: Annotated[
str, str,
typer.Option( typer.Option(
'--password', '--password',
'-p', '-p',
envvar='OBSWS_CLI_PASSWORD', envvar='OBS_PASSWORD',
help='WebSocket password', help='WebSocket password',
show_default=False, show_default=False,
), ),
] = envconfig.get('password'), ] = config.get('password'),
timeout: Annotated[ timeout: Annotated[
int, int,
typer.Option( typer.Option(
'--timeout', '--timeout',
'-T', '-T',
envvar='OBSWS_CLI_TIMEOUT', envvar='OBS_TIMEOUT',
help='WebSocket timeout', help='WebSocket timeout',
show_default=5, show_default=5,
), ),
] = envconfig.get('timeout'), ] = config.get('timeout'),
style: Annotated[ style: Annotated[
str, str,
typer.Option( typer.Option(
'--style', '--style',
'-s', '-s',
envvar='OBSWS_CLI_STYLE', envvar='OBS_STYLE',
help='Set the style for the CLI output', help='Set the style for the CLI output',
show_default='disabled', show_default='disabled',
callback=validate_style, callback=validate_style,
), ),
] = envconfig.get('style'), ] = config.get('style'),
no_border: Annotated[ no_border: Annotated[
bool, bool,
typer.Option( typer.Option(
'--no-border', '--no-border',
'-b', '-b',
envvar='OBSWS_CLI_STYLE_NO_BORDER', envvar='OBS_STYLE_NO_BORDER',
help='Disable table border styling in the CLI output', help='Disable table border styling in the CLI output',
show_default=False, show_default=False,
), ),
] = envconfig.get('style_no_border'), ] = config.get('style_no_border'),
version: Annotated[ version: Annotated[
bool, bool,
typer.Option( typer.Option(
@ -131,18 +136,19 @@ def main(
callback=version_callback, callback=version_callback,
), ),
] = False, ] = False,
loglevel: Annotated[ debug: Annotated[
str, bool,
typer.Option( typer.Option(
'--loglevel', '--debug',
'-l', '-d',
envvar='OBSWS_CLI_LOGLEVEL', envvar='OBS_DEBUG',
is_eager=True, is_eager=True,
help='Set the logging level', help='Enable debug logging',
show_default=False, show_default=False,
callback=setup_logging, callback=setup_logging,
hidden=True,
), ),
] = envconfig.get('loglevel'), ] = config.get('debug'),
): ):
"""obsws_cli is a command line interface for the OBS WebSocket API.""" """obsws_cli is a command line interface for the OBS WebSocket API."""
ctx.ensure_object(dict) ctx.ensure_object(dict)

View File

@ -1,105 +0,0 @@
"""module containing commands for media inputs."""
from typing import Annotated, Optional
import typer
from obsws_cli import console, util, validate
app = typer.Typer()
@app.callback()
def main():
"""Commands for media inputs."""
@app.command('cursor')
@app.command('cur', hidden=True)
def cursor(
ctx: typer.Context,
input_name: Annotated[
str, typer.Argument(..., help='The name of the media input.')
],
timecode: Annotated[
Optional[str],
typer.Argument(
...,
help='The timecode to set the cursor to (format: HH:MM:SS).',
callback=validate.timecode_format,
),
] = None,
):
"""Get/set the cursor position of a media input."""
if timecode is None:
resp = ctx.obj['obsws'].get_media_input_status(input_name)
console.out.print(
f'Cursor for {console.highlight(ctx, input_name)} is at {util.milliseconds_to_timecode(resp.media_cursor)}.'
)
return
cursor_position = util.timecode_to_milliseconds(timecode)
ctx.obj['obsws'].set_media_input_cursor(input_name, cursor_position)
console.out.print(
f'Cursor for {console.highlight(ctx, input_name)} set to {timecode}.'
)
@app.command('play')
@app.command('p', hidden=True)
def play(
ctx: typer.Context,
input_name: Annotated[
str, typer.Argument(..., help='The name of the media input.')
],
):
"""Get/set the playing status of a media input."""
ctx.obj['obsws'].trigger_media_input_action(
input_name, 'OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY'
)
console.out.print(f'Playing media input {console.highlight(ctx, input_name)}.')
@app.command('pause')
@app.command('pa', hidden=True)
def pause(
ctx: typer.Context,
input_name: Annotated[
str, typer.Argument(..., help='The name of the media input.')
],
):
"""Pause a media input."""
ctx.obj['obsws'].trigger_media_input_action(
input_name, 'OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE'
)
console.out.print(f'Paused media input {console.highlight(ctx, input_name)}.')
@app.command('stop')
@app.command('s', hidden=True)
def stop(
ctx: typer.Context,
input_name: Annotated[
str, typer.Argument(..., help='The name of the media input.')
],
):
"""Stop a media input."""
ctx.obj['obsws'].trigger_media_input_action(
input_name, 'OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP'
)
console.out.print(f'Stopped media input {console.highlight(ctx, input_name)}.')
@app.command('restart')
@app.command('r', hidden=True)
def restart(
ctx: typer.Context,
input_name: Annotated[
str, typer.Argument(..., help='The name of the media input.')
],
):
"""Restart a media input."""
ctx.obj['obsws'].trigger_media_input_action(
input_name, 'OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART'
)
console.out.print(f'Restarted media input {console.highlight(ctx, input_name)}.')

80
obsws_cli/config.py Normal file
View File

@ -0,0 +1,80 @@
"""module for settings management for obsws-cli."""
from collections import UserDict
from pathlib import Path
from dotenv import dotenv_values
ConfigValue = str | int
class Config(UserDict):
"""A class to manage config for obsws-cli.
This class extends UserDict to provide a dictionary-like interface for config.
It loads config from environment variables and .env files.
The config values are expected to be in uppercase and should start with 'OBS_'.
Example:
-------
config = Config()
host = config['OBS_HOST']
config['OBS_PORT'] = 4455
"""
PREFIX = 'OBS_'
def __init__(self, *args, **kwargs):
"""Initialize the Settings object."""
kwargs.update(
{
**dotenv_values('.env'),
**dotenv_values(Path.home() / '.config' / 'obsws-cli' / 'obsws.env'),
}
)
super().__init__(*args, **kwargs)
def __getitem__(self, key: str) -> ConfigValue:
"""Get a setting value by key."""
key = key.upper()
if not key.startswith(Config.PREFIX):
key = f'{Config.PREFIX}{key}'
return self.data[key]
def __setitem__(self, key: str, value: ConfigValue):
"""Set a setting value by key."""
key = key.upper()
if not key.startswith(Config.PREFIX):
key = f'{Config.PREFIX}{key}'
self.data[key] = value
_config = Config(
OBS_HOST='localhost',
OBS_PORT=4455,
OBS_PASSWORD='',
OBS_TIMEOUT=5,
OBS_DEBUG=False,
OBS_STYLE='disabled',
OBS_STYLE_NO_BORDER=False,
)
def get(key: str) -> ConfigValue:
"""Get a setting value by key.
Args:
----
key (str): The key of the config to retrieve.
Returns:
-------
The value of the config.
Raises:
------
KeyError: If the key does not exist in the config.
"""
return _config[key]

View File

@ -1,146 +0,0 @@
"""module for settings management for obsws-cli."""
from collections import UserDict
from pathlib import Path
from typing import Any, Union
from dotenv import dotenv_values
ConfigValue = Union[str, int, bool]
class EnvConfig(UserDict):
"""A class to manage .env config for obsws-cli.
This class extends UserDict to provide a dictionary-like interface for config.
It loads config from .env files in the following priority:
1. Local .env file
2. User config file (~/.config/obsws-cli/obsws.env)
3. Default values
Note, typer handles reading from environment variables automatically. They take precedence
over values set in this class.
The config values are expected to be in uppercase and should start with 'OBSWS_CLI_'.
Example:
-------
config = EnvConfig()
host = config['OBSWS_CLI_HOST']
config['OBSWS_CLI_PORT'] = 4455
# Or with defaults
timeout = config.get('OBSWS_CLI_TIMEOUT', 30)
# Keys will be normalised to uppercase with prefix
debug = config.get('debug', False) # Equivalent to 'OBSWS_CLI_DEBUG'
"""
PREFIX = 'OBSWS_CLI_'
def __init__(self, *args, **kwargs):
"""Initialize the Config object with hierarchical loading."""
kwargs.update(
{
**dotenv_values(Path.home() / '.config' / 'obsws-cli' / 'obsws.env'),
**dotenv_values('.env'),
}
)
super().__init__(*args, **self._convert_types(kwargs))
def _convert_types(self, config_data: dict[str, Any]) -> dict[str, ConfigValue]:
"""Convert string values to appropriate types."""
converted = {}
for key, value in config_data.items():
if isinstance(value, str):
if value.lower() in ('true', 'false'):
converted[key] = value.lower() == 'true'
elif value.isdigit():
converted[key] = int(value)
else:
converted[key] = value
else:
converted[key] = value
return converted
def __getitem__(self, key: str) -> ConfigValue:
"""Get a setting value by key."""
normalised_key = self._normalise_key(key)
try:
return self.data[normalised_key]
except KeyError as e:
raise KeyError(
f"Config key '{key}' not found. Available keys: {list(self.data.keys())}"
) from e
def __setitem__(self, key: str, value: ConfigValue):
"""Set a setting value by key."""
normalised_key = self._normalise_key(key)
self.data[normalised_key] = value
def _normalise_key(self, key: str) -> str:
"""Normalise a key to uppercase with OBS_ prefix."""
key = key.upper()
if not key.startswith(self.PREFIX):
key = f'{self.PREFIX}{key}'
return key
def get(self, key: str, default=None) -> ConfigValue:
"""Get a config value with optional default.
Args:
----
key (str): The key to retrieve
default: Default value if key is not found
Returns:
-------
The config value or default
"""
normalised_key = self._normalise_key(key)
if not self.has_key(normalised_key):
return default
return self[normalised_key]
def has_key(self, key: str) -> bool:
"""Check if a config key exists.
Args:
----
key (str): The key to check
Returns:
-------
bool: True if key exists, False otherwise
"""
normalised_key = self._normalise_key(key)
return normalised_key in self.data
_envconfig = EnvConfig(
OBSWS_CLI_HOST='localhost',
OBSWS_CLI_PORT=4455,
OBSWS_CLI_PASSWORD='',
OBSWS_CLI_TIMEOUT=5,
OBSWS_CLI_LOGLEVEL='WARNING',
OBSWS_CLI_STYLE='disabled',
OBSWS_CLI_STYLE_NO_BORDER=False,
)
def get(key: str) -> ConfigValue:
"""Get a setting value by key from the global config instance.
Args:
----
key (str): The key of the config to retrieve.
default: Default value if key is not found.
Returns:
-------
The value of the config or default value.
"""
return _envconfig.get(key)

View File

@ -7,9 +7,10 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util from . import console, util
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -17,8 +18,7 @@ def main():
"""Control filters in OBS scenes.""" """Control filters in OBS scenes."""
@app.command('list') @app.command('list | ls')
@app.command('ls', hidden=True)
def list_( def list_(
ctx: typer.Context, ctx: typer.Context,
source_name: Annotated[ source_name: Annotated[
@ -90,8 +90,7 @@ def _get_filter_enabled(ctx: typer.Context, source_name: str, filter_name: str):
return resp.filter_enabled return resp.filter_enabled
@app.command('enable') @app.command('enable | on')
@app.command('on', hidden=True)
def enable( def enable(
ctx: typer.Context, ctx: typer.Context,
source_name: Annotated[ source_name: Annotated[
@ -120,8 +119,7 @@ def enable(
) )
@app.command('disable') @app.command('disable | off')
@app.command('off', hidden=True)
def disable( def disable(
ctx: typer.Context, ctx: typer.Context,
source_name: Annotated[ source_name: Annotated[
@ -150,8 +148,7 @@ def disable(
) )
@app.command('toggle') @app.command('toggle | tg')
@app.command('tg', hidden=True)
def toggle( def toggle(
ctx: typer.Context, ctx: typer.Context,
source_name: Annotated[ source_name: Annotated[
@ -184,8 +181,7 @@ def toggle(
) )
@app.command('status') @app.command('status | ss')
@app.command('ss', hidden=True)
def status( def status(
ctx: typer.Context, ctx: typer.Context,
source_name: Annotated[ source_name: Annotated[

View File

@ -6,10 +6,11 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util, validate from . import console, util, validate
from obsws_cli.protocols import DataclassProtocol from .alias import SubTyperAliasGroup
from .protocols import DataclassProtocol
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -17,8 +18,7 @@ def main():
"""Control groups in OBS scenes.""" """Control groups in OBS scenes."""
@app.command('list') @app.command('list | ls')
@app.command('ls', hidden=True)
def list_( def list_(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
@ -26,14 +26,17 @@ def list_(
typer.Argument( typer.Argument(
show_default='The current scene', show_default='The current scene',
help='Scene name to list groups for', help='Scene name to list groups for',
callback=validate.scene_in_scenes,
), ),
] = None, ] = None,
): ):
"""List groups in a scene.""" """List groups in a scene."""
if scene_name is None: if not scene_name:
scene_name = ctx.obj['obsws'].get_current_program_scene().scene_name scene_name = ctx.obj['obsws'].get_current_program_scene().scene_name
if not validate.scene_in_scenes(ctx, scene_name):
console.err.print(f"Scene '{scene_name}' not found.")
raise typer.Exit(1)
resp = ctx.obj['obsws'].get_scene_item_list(scene_name) resp = ctx.obj['obsws'].get_scene_item_list(scene_name)
groups = [ groups = [
(item.get('sceneItemId'), item.get('sourceName'), item.get('sceneItemEnabled')) (item.get('sceneItemId'), item.get('sourceName'), item.get('sceneItemEnabled'))
@ -84,24 +87,22 @@ def _get_group(group_name: str, resp: DataclassProtocol) -> dict | None:
return group return group
@app.command('show') @app.command('show | sh')
@app.command('sh', hidden=True)
def show( def show(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
str, str,
typer.Argument( typer.Argument(..., show_default=False, help='Scene name the group is in'),
...,
show_default=False,
help='Scene name the group is in',
callback=validate.scene_in_scenes,
),
], ],
group_name: Annotated[ group_name: Annotated[
str, typer.Argument(..., show_default=False, help='Group name to show') str, typer.Argument(..., show_default=False, help='Group name to show')
], ],
): ):
"""Show a group in a scene.""" """Show a group in a scene."""
if not validate.scene_in_scenes(ctx, scene_name):
console.err.print(f"Scene '{scene_name}' not found.")
raise typer.Exit(1)
resp = ctx.obj['obsws'].get_scene_item_list(scene_name) resp = ctx.obj['obsws'].get_scene_item_list(scene_name)
if (group := _get_group(group_name, resp)) is None: if (group := _get_group(group_name, resp)) is None:
console.err.print( console.err.print(
@ -118,24 +119,21 @@ def show(
console.out.print(f'Group {console.highlight(ctx, group_name)} is now visible.') console.out.print(f'Group {console.highlight(ctx, group_name)} is now visible.')
@app.command('hide') @app.command('hide | h')
@app.command('h', hidden=True)
def hide( def hide(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
str, str, typer.Argument(..., show_default=False, help='Scene name the group is in')
typer.Argument(
...,
show_default=False,
help='Scene name the group is in',
callback=validate.scene_in_scenes,
),
], ],
group_name: Annotated[ group_name: Annotated[
str, typer.Argument(..., show_default=False, help='Group name to hide') str, typer.Argument(..., show_default=False, help='Group name to hide')
], ],
): ):
"""Hide a group in a scene.""" """Hide a group in a scene."""
if not validate.scene_in_scenes(ctx, scene_name):
console.err.print(f'Scene [yellow]{scene_name}[/yellow] not found.')
raise typer.Exit(1)
resp = ctx.obj['obsws'].get_scene_item_list(scene_name) resp = ctx.obj['obsws'].get_scene_item_list(scene_name)
if (group := _get_group(group_name, resp)) is None: if (group := _get_group(group_name, resp)) is None:
console.err.print( console.err.print(
@ -152,24 +150,21 @@ def hide(
console.out.print(f'Group {console.highlight(ctx, group_name)} is now hidden.') console.out.print(f'Group {console.highlight(ctx, group_name)} is now hidden.')
@app.command('toggle') @app.command('toggle | tg')
@app.command('tg', hidden=True)
def toggle( def toggle(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
str, str, typer.Argument(..., show_default=False, help='Scene name the group is in')
typer.Argument(
...,
show_default=False,
help='Scene name the group is in',
callback=validate.scene_in_scenes,
),
], ],
group_name: Annotated[ group_name: Annotated[
str, typer.Argument(..., show_default=False, help='Group name to toggle') str, typer.Argument(..., show_default=False, help='Group name to toggle')
], ],
): ):
"""Toggle a group in a scene.""" """Toggle a group in a scene."""
if not validate.scene_in_scenes(ctx, scene_name):
console.err.print(f'Scene [yellow]{scene_name}[/yellow] not found.')
raise typer.Exit(1)
resp = ctx.obj['obsws'].get_scene_item_list(scene_name) resp = ctx.obj['obsws'].get_scene_item_list(scene_name)
if (group := _get_group(group_name, resp)) is None: if (group := _get_group(group_name, resp)) is None:
console.err.print( console.err.print(
@ -190,24 +185,21 @@ def toggle(
console.out.print(f'Group {console.highlight(ctx, group_name)} is now hidden.') console.out.print(f'Group {console.highlight(ctx, group_name)} is now hidden.')
@app.command('status') @app.command('status | ss')
@app.command('ss', hidden=True)
def status( def status(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
str, str, typer.Argument(..., show_default=False, help='Scene name the group is in')
typer.Argument(
...,
show_default=False,
help='Scene name the group is in',
callback=validate.scene_in_scenes,
),
], ],
group_name: Annotated[ group_name: Annotated[
str, typer.Argument(..., show_default=False, help='Group name to check status') str, typer.Argument(..., show_default=False, help='Group name to check status')
], ],
): ):
"""Get the status of a group in a scene.""" """Get the status of a group in a scene."""
if not validate.scene_in_scenes(ctx, scene_name):
console.err.print(f'Scene [yellow]{scene_name}[/yellow] not found.')
raise typer.Exit(1)
resp = ctx.obj['obsws'].get_scene_item_list(scene_name) resp = ctx.obj['obsws'].get_scene_item_list(scene_name)
if (group := _get_group(group_name, resp)) is None: if (group := _get_group(group_name, resp)) is None:
console.err.print( console.err.print(

View File

@ -6,9 +6,10 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console from . import console
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -16,8 +17,7 @@ def main():
"""Control hotkeys in OBS.""" """Control hotkeys in OBS."""
@app.command('list') @app.command('list | ls')
@app.command('ls', hidden=True)
def list_( def list_(
ctx: typer.Context, ctx: typer.Context,
): ):
@ -45,8 +45,7 @@ def list_(
console.out.print(table) console.out.print(table)
@app.command('trigger') @app.command('trigger | tr')
@app.command('tr', hidden=True)
def trigger( def trigger(
ctx: typer.Context, ctx: typer.Context,
hotkey: Annotated[ hotkey: Annotated[
@ -57,8 +56,7 @@ def trigger(
ctx.obj['obsws'].trigger_hotkey_by_name(hotkey) ctx.obj['obsws'].trigger_hotkey_by_name(hotkey)
@app.command('trigger-sequence') @app.command('trigger-sequence | trs')
@app.command('trs', hidden=True)
def trigger_sequence( def trigger_sequence(
ctx: typer.Context, ctx: typer.Context,
key_id: Annotated[ key_id: Annotated[

View File

@ -7,9 +7,10 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util, validate from . import console, util, validate
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -17,8 +18,7 @@ def main():
"""Control inputs in OBS.""" """Control inputs in OBS."""
@app.command('create') @app.command('create | add')
@app.command('cr', hidden=True)
def create( def create(
ctx: typer.Context, ctx: typer.Context,
input_name: Annotated[ input_name: Annotated[
@ -62,8 +62,7 @@ def create(
) )
@app.command('remove') @app.command('remove | rm')
@app.command('rm', hidden=True)
def remove( def remove(
ctx: typer.Context, ctx: typer.Context,
input_name: Annotated[ input_name: Annotated[
@ -82,8 +81,7 @@ def remove(
console.out.print(f'Input {console.highlight(ctx, input_name)} removed.') console.out.print(f'Input {console.highlight(ctx, input_name)} removed.')
@app.command('list') @app.command('list | ls')
@app.command('ls', hidden=True)
def list_( def list_(
ctx: typer.Context, ctx: typer.Context,
input: Annotated[bool, typer.Option(help='Filter by input type.')] = False, input: Annotated[bool, typer.Option(help='Filter by input type.')] = False,
@ -170,8 +168,7 @@ def list_(
console.out.print(table) console.out.print(table)
@app.command('list-kinds') @app.command('list-kinds | ls-k')
@app.command('ls-k', hidden=True)
def list_kinds( def list_kinds(
ctx: typer.Context, ctx: typer.Context,
): ):
@ -198,8 +195,7 @@ def list_kinds(
console.out.print(table) console.out.print(table)
@app.command('mute') @app.command('mute | m')
@app.command('m', hidden=True)
def mute( def mute(
ctx: typer.Context, ctx: typer.Context,
input_name: Annotated[ input_name: Annotated[
@ -221,8 +217,7 @@ def mute(
console.out.print(f'Input {console.highlight(ctx, input_name)} muted.') console.out.print(f'Input {console.highlight(ctx, input_name)} muted.')
@app.command('unmute') @app.command('unmute | um')
@app.command('um', hidden=True)
def unmute( def unmute(
ctx: typer.Context, ctx: typer.Context,
input_name: Annotated[ input_name: Annotated[
@ -244,8 +239,7 @@ def unmute(
console.out.print(f'Input {console.highlight(ctx, input_name)} unmuted.') console.out.print(f'Input {console.highlight(ctx, input_name)} unmuted.')
@app.command('toggle') @app.command('toggle | tg')
@app.command('tg', hidden=True)
def toggle( def toggle(
ctx: typer.Context, ctx: typer.Context,
input_name: Annotated[ input_name: Annotated[
@ -277,8 +271,7 @@ def toggle(
) )
@app.command('volume') @app.command('volume | vol')
@app.command('vol', hidden=True)
def volume( def volume(
ctx: typer.Context, ctx: typer.Context,
input_name: Annotated[ input_name: Annotated[
@ -312,8 +305,7 @@ def volume(
) )
@app.command('show') @app.command('show | s')
@app.command('s', hidden=True)
def show( def show(
ctx: typer.Context, ctx: typer.Context,
input_name: Annotated[ input_name: Annotated[
@ -406,8 +398,7 @@ def show(
console.out.print(table) console.out.print(table)
@app.command('update') @app.command('update | upd')
@app.command('upd', hidden=True)
def update( def update(
ctx: typer.Context, ctx: typer.Context,
input_name: Annotated[ input_name: Annotated[

View File

@ -6,9 +6,10 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util, validate from . import console, util, validate
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -16,8 +17,7 @@ def main():
"""Control profiles in OBS.""" """Control profiles in OBS."""
@app.command('list') @app.command('list | ls')
@app.command('ls', hidden=True)
def list_(ctx: typer.Context): def list_(ctx: typer.Context):
"""List profiles.""" """List profiles."""
resp = ctx.obj['obsws'].get_profile_list() resp = ctx.obj['obsws'].get_profile_list()
@ -47,8 +47,7 @@ def list_(ctx: typer.Context):
console.out.print(table) console.out.print(table)
@app.command('current') @app.command('current | get')
@app.command('get', hidden=True)
def current(ctx: typer.Context): def current(ctx: typer.Context):
"""Get the current profile.""" """Get the current profile."""
resp = ctx.obj['obsws'].get_profile_list() resp = ctx.obj['obsws'].get_profile_list()
@ -57,21 +56,21 @@ def current(ctx: typer.Context):
) )
@app.command('switch') @app.command('switch | set')
@app.command('set', hidden=True)
def switch( def switch(
ctx: typer.Context, ctx: typer.Context,
profile_name: Annotated[ profile_name: Annotated[
str, str,
typer.Argument( typer.Argument(
..., ..., show_default=False, help='Name of the profile to switch to'
show_default=False,
help='Name of the profile to switch to',
callback=validate.profile_exists,
), ),
], ],
): ):
"""Switch to a profile.""" """Switch to a profile."""
if not validate.profile_exists(ctx, profile_name):
console.err.print(f'Profile [yellow]{profile_name}[/yellow] not found.')
raise typer.Exit(1)
resp = ctx.obj['obsws'].get_profile_list() resp = ctx.obj['obsws'].get_profile_list()
if resp.current_profile_name == profile_name: if resp.current_profile_name == profile_name:
console.err.print( console.err.print(
@ -83,39 +82,35 @@ def switch(
console.out.print(f'Switched to profile {console.highlight(ctx, profile_name)}.') console.out.print(f'Switched to profile {console.highlight(ctx, profile_name)}.')
@app.command('create') @app.command('create | new')
@app.command('new', hidden=True)
def create( def create(
ctx: typer.Context, ctx: typer.Context,
profile_name: Annotated[ profile_name: Annotated[
str, str,
typer.Argument( typer.Argument(..., show_default=False, help='Name of the profile to create.'),
...,
show_default=False,
help='Name of the profile to create.',
callback=validate.profile_not_exists,
),
], ],
): ):
"""Create a new profile.""" """Create a new profile."""
if validate.profile_exists(ctx, profile_name):
console.err.print(f'Profile [yellow]{profile_name}[/yellow] already exists.')
raise typer.Exit(1)
ctx.obj['obsws'].create_profile(profile_name) ctx.obj['obsws'].create_profile(profile_name)
console.out.print(f'Created profile {console.highlight(ctx, profile_name)}.') console.out.print(f'Created profile {console.highlight(ctx, profile_name)}.')
@app.command('remove') @app.command('remove | rm')
@app.command('rm', hidden=True)
def remove( def remove(
ctx: typer.Context, ctx: typer.Context,
profile_name: Annotated[ profile_name: Annotated[
str, str,
typer.Argument( typer.Argument(..., show_default=False, help='Name of the profile to remove.'),
...,
show_default=False,
help='Name of the profile to remove.',
callback=validate.profile_exists,
),
], ],
): ):
"""Remove a profile.""" """Remove a profile."""
if not validate.profile_exists(ctx, profile_name):
console.err.print(f'Profile [yellow]{profile_name}[/yellow] not found.')
raise typer.Exit(1)
ctx.obj['obsws'].remove_profile(profile_name) ctx.obj['obsws'].remove_profile(profile_name)
console.out.print(f'Removed profile {console.highlight(ctx, profile_name)}.') console.out.print(f'Removed profile {console.highlight(ctx, profile_name)}.')

View File

@ -6,9 +6,10 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console from . import console
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -16,8 +17,7 @@ def main():
"""Control projectors in OBS.""" """Control projectors in OBS."""
@app.command('list-monitors') @app.command('list-monitors | ls-m')
@app.command('ls-m', hidden=True)
def list_monitors(ctx: typer.Context): def list_monitors(ctx: typer.Context):
"""List available monitors.""" """List available monitors."""
resp = ctx.obj['obsws'].get_monitor_list() resp = ctx.obj['obsws'].get_monitor_list()
@ -48,8 +48,7 @@ def list_monitors(ctx: typer.Context):
console.out.print(table) console.out.print(table)
@app.command('open') @app.command('open | o')
@app.command('o', hidden=True)
def open( def open(
ctx: typer.Context, ctx: typer.Context,
monitor_index: Annotated[ monitor_index: Annotated[

View File

@ -5,9 +5,10 @@ from typing import Annotated, Optional
import typer import typer
from obsws_cli import console from . import console
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -21,8 +22,7 @@ def _get_recording_status(ctx: typer.Context) -> tuple:
return resp.output_active, resp.output_paused return resp.output_active, resp.output_paused
@app.command('start') @app.command('start | s')
@app.command('s', hidden=True)
def start(ctx: typer.Context): def start(ctx: typer.Context):
"""Start recording.""" """Start recording."""
active, paused = _get_recording_status(ctx) active, paused = _get_recording_status(ctx)
@ -38,8 +38,7 @@ def start(ctx: typer.Context):
console.out.print('Recording started successfully.') console.out.print('Recording started successfully.')
@app.command('stop') @app.command('stop | st')
@app.command('st', hidden=True)
def stop(ctx: typer.Context): def stop(ctx: typer.Context):
"""Stop recording.""" """Stop recording."""
active, _ = _get_recording_status(ctx) active, _ = _get_recording_status(ctx)
@ -53,8 +52,7 @@ def stop(ctx: typer.Context):
) )
@app.command('toggle') @app.command('toggle | tg')
@app.command('tg', hidden=True)
def toggle(ctx: typer.Context): def toggle(ctx: typer.Context):
"""Toggle recording.""" """Toggle recording."""
resp = ctx.obj['obsws'].toggle_record() resp = ctx.obj['obsws'].toggle_record()
@ -64,8 +62,7 @@ def toggle(ctx: typer.Context):
console.out.print('Recording stopped successfully.') console.out.print('Recording stopped successfully.')
@app.command('status') @app.command('status | ss')
@app.command('ss', hidden=True)
def status(ctx: typer.Context): def status(ctx: typer.Context):
"""Get recording status.""" """Get recording status."""
active, paused = _get_recording_status(ctx) active, paused = _get_recording_status(ctx)
@ -78,8 +75,7 @@ def status(ctx: typer.Context):
console.out.print('Recording is not in progress.') console.out.print('Recording is not in progress.')
@app.command('resume') @app.command('resume | r')
@app.command('r', hidden=True)
def resume(ctx: typer.Context): def resume(ctx: typer.Context):
"""Resume recording.""" """Resume recording."""
active, paused = _get_recording_status(ctx) active, paused = _get_recording_status(ctx)
@ -94,8 +90,7 @@ def resume(ctx: typer.Context):
console.out.print('Recording resumed successfully.') console.out.print('Recording resumed successfully.')
@app.command('pause') @app.command('pause | p')
@app.command('p', hidden=True)
def pause(ctx: typer.Context): def pause(ctx: typer.Context):
"""Pause recording.""" """Pause recording."""
active, paused = _get_recording_status(ctx) active, paused = _get_recording_status(ctx)
@ -110,8 +105,7 @@ def pause(ctx: typer.Context):
console.out.print('Recording paused successfully.') console.out.print('Recording paused successfully.')
@app.command('directory') @app.command('directory | d')
@app.command('d', hidden=True)
def directory( def directory(
ctx: typer.Context, ctx: typer.Context,
record_directory: Annotated[ record_directory: Annotated[
@ -138,8 +132,7 @@ def directory(
) )
@app.command('split') @app.command('split | sp')
@app.command('sp', hidden=True)
def split(ctx: typer.Context): def split(ctx: typer.Context):
"""Split the current recording.""" """Split the current recording."""
active, paused = _get_recording_status(ctx) active, paused = _get_recording_status(ctx)
@ -154,8 +147,7 @@ def split(ctx: typer.Context):
console.out.print('Recording split successfully.') console.out.print('Recording split successfully.')
@app.command('chapter') @app.command('chapter | ch')
@app.command('ch', hidden=True)
def chapter( def chapter(
ctx: typer.Context, ctx: typer.Context,
chapter_name: Annotated[ chapter_name: Annotated[

View File

@ -2,9 +2,10 @@
import typer import typer
from obsws_cli import console from . import console
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -12,8 +13,7 @@ def main():
"""Control profiles in OBS.""" """Control profiles in OBS."""
@app.command('start') @app.command('start | s')
@app.command('s', hidden=True)
def start(ctx: typer.Context): def start(ctx: typer.Context):
"""Start the replay buffer.""" """Start the replay buffer."""
resp = ctx.obj['obsws'].get_replay_buffer_status() resp = ctx.obj['obsws'].get_replay_buffer_status()
@ -25,8 +25,7 @@ def start(ctx: typer.Context):
console.out.print('Replay buffer started.') console.out.print('Replay buffer started.')
@app.command('stop') @app.command('stop | st')
@app.command('st', hidden=True)
def stop(ctx: typer.Context): def stop(ctx: typer.Context):
"""Stop the replay buffer.""" """Stop the replay buffer."""
resp = ctx.obj['obsws'].get_replay_buffer_status() resp = ctx.obj['obsws'].get_replay_buffer_status()
@ -38,8 +37,7 @@ def stop(ctx: typer.Context):
console.out.print('Replay buffer stopped.') console.out.print('Replay buffer stopped.')
@app.command('toggle') @app.command('toggle | tg')
@app.command('tg', hidden=True)
def toggle(ctx: typer.Context): def toggle(ctx: typer.Context):
"""Toggle the replay buffer.""" """Toggle the replay buffer."""
resp = ctx.obj['obsws'].toggle_replay_buffer() resp = ctx.obj['obsws'].toggle_replay_buffer()
@ -49,8 +47,7 @@ def toggle(ctx: typer.Context):
console.out.print('Replay buffer is not active.') console.out.print('Replay buffer is not active.')
@app.command('status') @app.command('status | ss')
@app.command('ss', hidden=True)
def status(ctx: typer.Context): def status(ctx: typer.Context):
"""Get the status of the replay buffer.""" """Get the status of the replay buffer."""
resp = ctx.obj['obsws'].get_replay_buffer_status() resp = ctx.obj['obsws'].get_replay_buffer_status()
@ -60,8 +57,7 @@ def status(ctx: typer.Context):
console.out.print('Replay buffer is not active.') console.out.print('Replay buffer is not active.')
@app.command('save') @app.command('save | sv')
@app.command('sv', hidden=True)
def save(ctx: typer.Context): def save(ctx: typer.Context):
"""Save the replay buffer.""" """Save the replay buffer."""
ctx.obj['obsws'].save_replay_buffer() ctx.obj['obsws'].save_replay_buffer()

View File

@ -6,9 +6,10 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util, validate from . import console, util, validate
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -16,8 +17,7 @@ def main():
"""Control OBS scenes.""" """Control OBS scenes."""
@app.command('list') @app.command('list | ls')
@app.command('ls', hidden=True)
def list_( def list_(
ctx: typer.Context, ctx: typer.Context,
uuid: Annotated[bool, typer.Option(help='Show UUIDs of scenes')] = False, uuid: Annotated[bool, typer.Option(help='Show UUIDs of scenes')] = False,
@ -66,19 +66,18 @@ def list_(
console.out.print(table) console.out.print(table)
@app.command('current') @app.command('current | get')
@app.command('get', hidden=True)
def current( def current(
ctx: typer.Context, ctx: typer.Context,
preview: Annotated[ preview: Annotated[
bool, bool, typer.Option(help='Get the preview scene instead of the program scene')
typer.Option(
help='Get the preview scene instead of the program scene',
callback=validate.studio_mode_enabled,
),
] = False, ] = False,
): ):
"""Get the current program scene or preview scene.""" """Get the current program scene or preview scene."""
if preview and not validate.studio_mode_enabled(ctx):
console.err.print('Studio mode is not enabled, cannot get preview scene.')
raise typer.Exit(1)
if preview: if preview:
resp = ctx.obj['obsws'].get_current_preview_scene() resp = ctx.obj['obsws'].get_current_preview_scene()
console.out.print( console.out.print(
@ -91,27 +90,26 @@ def current(
) )
@app.command('switch') @app.command('switch | set')
@app.command('set', hidden=True)
def switch( def switch(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
str, str, typer.Argument(..., help='Name of the scene to switch to')
typer.Argument(
...,
help='Name of the scene to switch to',
callback=validate.scene_in_scenes,
),
], ],
preview: Annotated[ preview: Annotated[
bool, bool,
typer.Option( typer.Option(help='Switch to the preview scene instead of the program scene'),
help='Switch to the preview scene instead of the program scene',
callback=validate.studio_mode_enabled,
),
] = False, ] = False,
): ):
"""Switch to a scene.""" """Switch to a scene."""
if preview and not validate.studio_mode_enabled(ctx):
console.err.print('Studio mode is not enabled, cannot set the preview scene.')
raise typer.Exit(1)
if not validate.scene_in_scenes(ctx, scene_name):
console.err.print(f'Scene [yellow]{scene_name}[/yellow] not found.')
raise typer.Exit(1)
if preview: if preview:
ctx.obj['obsws'].set_current_preview_scene(scene_name) ctx.obj['obsws'].set_current_preview_scene(scene_name)
console.out.print( console.out.print(

View File

@ -5,9 +5,10 @@ from typing import Annotated
import typer import typer
from rich.table import Table from rich.table import Table
from obsws_cli import console, validate from . import console, validate
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -15,8 +16,7 @@ def main():
"""Control scene collections in OBS.""" """Control scene collections in OBS."""
@app.command('list') @app.command('list | ls')
@app.command('ls', hidden=True)
def list_(ctx: typer.Context): def list_(ctx: typer.Context):
"""List all scene collections.""" """List all scene collections."""
resp = ctx.obj['obsws'].get_scene_collection_list() resp = ctx.obj['obsws'].get_scene_collection_list()
@ -40,8 +40,7 @@ def list_(ctx: typer.Context):
console.out.print(table) console.out.print(table)
@app.command('current') @app.command('current | get')
@app.command('get', hidden=True)
def current(ctx: typer.Context): def current(ctx: typer.Context):
"""Get the current scene collection.""" """Get the current scene collection."""
resp = ctx.obj['obsws'].get_scene_collection_list() resp = ctx.obj['obsws'].get_scene_collection_list()
@ -50,20 +49,20 @@ def current(ctx: typer.Context):
) )
@app.command('switch') @app.command('switch | set')
@app.command('set', hidden=True)
def switch( def switch(
ctx: typer.Context, ctx: typer.Context,
scene_collection_name: Annotated[ scene_collection_name: Annotated[
str, str, typer.Argument(..., help='Name of the scene collection to switch to')
typer.Argument(
...,
help='Name of the scene collection to switch to',
callback=validate.scene_collection_in_scene_collections,
),
], ],
): ):
"""Switch to a scene collection.""" """Switch to a scene collection."""
if not validate.scene_collection_in_scene_collections(ctx, scene_collection_name):
console.err.print(
f'Scene collection [yellow]{scene_collection_name}[/yellow] not found.'
)
raise typer.Exit(1)
current_scene_collection = ( current_scene_collection = (
ctx.obj['obsws'].get_scene_collection_list().current_scene_collection_name ctx.obj['obsws'].get_scene_collection_list().current_scene_collection_name
) )
@ -79,20 +78,20 @@ def switch(
) )
@app.command('create') @app.command('create | new')
@app.command('new', hidden=True)
def create( def create(
ctx: typer.Context, ctx: typer.Context,
scene_collection_name: Annotated[ scene_collection_name: Annotated[
str, str, typer.Argument(..., help='Name of the scene collection to create')
typer.Argument(
...,
help='Name of the scene collection to create',
callback=validate.scene_collection_not_in_scene_collections,
),
], ],
): ):
"""Create a new scene collection.""" """Create a new scene collection."""
if validate.scene_collection_in_scene_collections(ctx, scene_collection_name):
console.err.print(
f'Scene collection [yellow]{scene_collection_name}[/yellow] already exists.'
)
raise typer.Exit(1)
ctx.obj['obsws'].create_scene_collection(scene_collection_name) ctx.obj['obsws'].create_scene_collection(scene_collection_name)
console.out.print( console.out.print(
f'Created scene collection {console.highlight(ctx, scene_collection_name)}.' f'Created scene collection {console.highlight(ctx, scene_collection_name)}.'

View File

@ -5,9 +5,10 @@ from typing import Annotated, Optional
import typer import typer
from rich.table import Table from rich.table import Table
from obsws_cli import console, util, validate from . import console, util, validate
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -15,8 +16,7 @@ def main():
"""Control items in OBS scenes.""" """Control items in OBS scenes."""
@app.command('list') @app.command('list | ls')
@app.command('ls', hidden=True)
def list_( def list_(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
@ -24,15 +24,18 @@ def list_(
typer.Argument( typer.Argument(
show_default='The current scene', show_default='The current scene',
help='Scene name to list items for', help='Scene name to list items for',
callback=validate.scene_in_scenes,
), ),
] = None, ] = None,
uuid: Annotated[bool, typer.Option(help='Show UUIDs of scene items')] = False, uuid: Annotated[bool, typer.Option(help='Show UUIDs of scene items')] = False,
): ):
"""List all items in a scene.""" """List all items in a scene."""
if scene_name is None: if not scene_name:
scene_name = ctx.obj['obsws'].get_current_program_scene().scene_name scene_name = ctx.obj['obsws'].get_current_program_scene().scene_name
if not validate.scene_in_scenes(ctx, scene_name):
console.err.print(f'Scene [yellow]{scene_name}[/yellow] not found.')
raise typer.Exit(1)
resp = ctx.obj['obsws'].get_scene_item_list(scene_name) resp = ctx.obj['obsws'].get_scene_item_list(scene_name)
items = sorted( items = sorted(
( (
@ -186,8 +189,7 @@ def _get_scene_name_and_item_id(
return scene_name, scene_item_id return scene_name, scene_item_id
@app.command('show') @app.command('show | sh')
@app.command('sh', hidden=True)
def show( def show(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
@ -229,8 +231,7 @@ def show(
) )
@app.command('hide') @app.command('hide | h')
@app.command('h', hidden=True)
def hide( def hide(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
@ -271,8 +272,7 @@ def hide(
) )
@app.command('toggle') @app.command('toggle | tg')
@app.command('tg', hidden=True)
def toggle( def toggle(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
@ -333,8 +333,7 @@ def toggle(
) )
@app.command('visible') @app.command('visible | v')
@app.command('v', hidden=True)
def visible( def visible(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[
@ -378,8 +377,7 @@ def visible(
) )
@app.command('transform') @app.command('transform | t')
@app.command('t', hidden=True)
def transform( def transform(
ctx: typer.Context, ctx: typer.Context,
scene_name: Annotated[ scene_name: Annotated[

View File

@ -6,9 +6,10 @@ from typing import Annotated
import obsws_python as obsws import obsws_python as obsws
import typer import typer
from obsws_cli import console from . import console
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -16,8 +17,7 @@ def main():
"""Take screenshots using OBS.""" """Take screenshots using OBS."""
@app.command('save') @app.command('save | sv')
@app.command('s', hidden=True)
def save( def save(
ctx: typer.Context, ctx: typer.Context,
source_name: Annotated[ source_name: Annotated[

View File

@ -6,9 +6,10 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util from . import console, util
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -16,8 +17,7 @@ def main():
"""Manage OBS settings.""" """Manage OBS settings."""
@app.command('show') @app.command('show | sh')
@app.command('sh', hidden=True)
def show( def show(
ctx: typer.Context, ctx: typer.Context,
video: Annotated[ video: Annotated[
@ -136,8 +136,7 @@ def show(
console.out.print(profile_table) console.out.print(profile_table)
@app.command('profile') @app.command('profile | pr')
@app.command('pr', hidden=True)
def profile( def profile(
ctx: typer.Context, ctx: typer.Context,
category: Annotated[ category: Annotated[
@ -183,8 +182,7 @@ def profile(
) )
@app.command('stream-service') @app.command('stream-service | ss')
@app.command('ss', hidden=True)
def stream_service( def stream_service(
ctx: typer.Context, ctx: typer.Context,
type_: Annotated[ type_: Annotated[
@ -251,8 +249,7 @@ def stream_service(
console.out.print('Stream service settings updated.') console.out.print('Stream service settings updated.')
@app.command('video') @app.command('video | vi')
@app.command('vi', hidden=True)
def video( def video(
ctx: typer.Context, ctx: typer.Context,
base_width: Annotated[ base_width: Annotated[

View File

@ -2,9 +2,10 @@
import typer import typer
from obsws_cli import console from . import console
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -18,8 +19,7 @@ def _get_streaming_status(ctx: typer.Context) -> tuple:
return resp.output_active, resp.output_duration return resp.output_active, resp.output_duration
@app.command('start') @app.command('start | s')
@app.command('s', hidden=True)
def start(ctx: typer.Context): def start(ctx: typer.Context):
"""Start streaming.""" """Start streaming."""
active, _ = _get_streaming_status(ctx) active, _ = _get_streaming_status(ctx)
@ -31,8 +31,7 @@ def start(ctx: typer.Context):
console.out.print('Streaming started successfully.') console.out.print('Streaming started successfully.')
@app.command('stop') @app.command('stop | st')
@app.command('st', hidden=True)
def stop(ctx: typer.Context): def stop(ctx: typer.Context):
"""Stop streaming.""" """Stop streaming."""
active, _ = _get_streaming_status(ctx) active, _ = _get_streaming_status(ctx)
@ -44,8 +43,7 @@ def stop(ctx: typer.Context):
console.out.print('Streaming stopped successfully.') console.out.print('Streaming stopped successfully.')
@app.command('toggle') @app.command('toggle | tg')
@app.command('tg', hidden=True)
def toggle(ctx: typer.Context): def toggle(ctx: typer.Context):
"""Toggle streaming.""" """Toggle streaming."""
resp = ctx.obj['obsws'].toggle_stream() resp = ctx.obj['obsws'].toggle_stream()
@ -55,8 +53,7 @@ def toggle(ctx: typer.Context):
console.out.print('Streaming stopped successfully.') console.out.print('Streaming stopped successfully.')
@app.command('status') @app.command('status | ss')
@app.command('ss', hidden=True)
def status(ctx: typer.Context): def status(ctx: typer.Context):
"""Get streaming status.""" """Get streaming status."""
active, duration = _get_streaming_status(ctx) active, duration = _get_streaming_status(ctx)

View File

@ -2,9 +2,10 @@
import typer import typer
from obsws_cli import console from . import console
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -12,24 +13,21 @@ def main():
"""Control studio mode in OBS.""" """Control studio mode in OBS."""
@app.command('enable') @app.command('enable | on')
@app.command('on', hidden=True)
def enable(ctx: typer.Context): def enable(ctx: typer.Context):
"""Enable studio mode.""" """Enable studio mode."""
ctx.obj['obsws'].set_studio_mode_enabled(True) ctx.obj['obsws'].set_studio_mode_enabled(True)
console.out.print('Studio mode has been enabled.') console.out.print('Studio mode has been enabled.')
@app.command('disable') @app.command('disable | off')
@app.command('off', hidden=True)
def disable(ctx: typer.Context): def disable(ctx: typer.Context):
"""Disable studio mode.""" """Disable studio mode."""
ctx.obj['obsws'].set_studio_mode_enabled(False) ctx.obj['obsws'].set_studio_mode_enabled(False)
console.out.print('Studio mode has been disabled.') console.out.print('Studio mode has been disabled.')
@app.command('toggle') @app.command('toggle | tg')
@app.command('tg', hidden=True)
def toggle(ctx: typer.Context): def toggle(ctx: typer.Context):
"""Toggle studio mode.""" """Toggle studio mode."""
resp = ctx.obj['obsws'].get_studio_mode_enabled() resp = ctx.obj['obsws'].get_studio_mode_enabled()
@ -41,8 +39,7 @@ def toggle(ctx: typer.Context):
console.out.print('Studio mode is now enabled.') console.out.print('Studio mode is now enabled.')
@app.command('status') @app.command('status | ss')
@app.command('ss', hidden=True)
def status(ctx: typer.Context): def status(ctx: typer.Context):
"""Get the status of studio mode.""" """Get the status of studio mode."""
resp = ctx.obj['obsws'].get_studio_mode_enabled() resp = ctx.obj['obsws'].get_studio_mode_enabled()

View File

@ -4,9 +4,10 @@ from typing import Annotated, Optional
import typer import typer
from obsws_cli import console, validate from . import console, validate
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -14,8 +15,7 @@ def main():
"""Control text inputs in OBS.""" """Control text inputs in OBS."""
@app.command('current') @app.command('current | get')
@app.command('get', hidden=True)
def current( def current(
ctx: typer.Context, ctx: typer.Context,
input_name: Annotated[ input_name: Annotated[
@ -41,8 +41,7 @@ def current(
) )
@app.command('update') @app.command('update | set')
@app.command('set', hidden=True)
def update( def update(
ctx: typer.Context, ctx: typer.Context,
input_name: Annotated[ input_name: Annotated[

View File

@ -20,28 +20,3 @@ def check_mark(value: bool, empty_if_false: bool = False) -> str:
if os.getenv('NO_COLOR', '') != '': if os.getenv('NO_COLOR', '') != '':
return '' if value else '' return '' if value else ''
return '' if value else '' return '' if value else ''
def timecode_to_milliseconds(timecode: str) -> int:
"""Convert a timecode string (HH:MM:SS) to total milliseconds."""
match timecode.split(':'):
case [mm, ss]:
hours = 0
minutes = int(mm)
seconds = int(ss)
case [hh, mm, ss]:
hours = int(hh)
minutes = int(mm)
seconds = int(ss)
return (hours * 3600 + minutes * 60 + seconds) * 1000
def milliseconds_to_timecode(milliseconds: int) -> str:
"""Convert total milliseconds to a timecode string (HH:MM:SS)."""
total_seconds = milliseconds // 1000
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
if hours == 0:
return f'{minutes:02}:{seconds:02}'
return f'{hours:02}:{minutes:02}:{seconds:02}'

View File

@ -1,7 +1,5 @@
"""module containing validation functions.""" """module containing validation functions."""
from typing import Optional
import typer import typer
from . import console from . import console
@ -10,7 +8,7 @@ from . import console
skipped_option = typer.Option(parser=lambda _: _, hidden=True, expose_value=False) skipped_option = typer.Option(parser=lambda _: _, hidden=True, expose_value=False)
def input_in_inputs(ctx: typer.Context, input_name: str) -> str: def input_in_inputs(ctx: typer.Context, input_name: str) -> bool:
"""Ensure the given input exists in the list of inputs.""" """Ensure the given input exists in the list of inputs."""
resp = ctx.obj['obsws'].get_input_list() resp = ctx.obj['obsws'].get_input_list()
if not any(input.get('inputName') == input_name for input in resp.inputs): if not any(input.get('inputName') == input_name for input in resp.inputs):
@ -19,7 +17,7 @@ def input_in_inputs(ctx: typer.Context, input_name: str) -> str:
return input_name return input_name
def input_not_in_inputs(ctx: typer.Context, input_name: str) -> str: def input_not_in_inputs(ctx: typer.Context, input_name: str) -> bool:
"""Ensure an input does not already exist in the list of inputs.""" """Ensure an input does not already exist in the list of inputs."""
resp = ctx.obj['obsws'].get_input_list() resp = ctx.obj['obsws'].get_input_list()
if any(input.get('inputName') == input_name for input in resp.inputs): if any(input.get('inputName') == input_name for input in resp.inputs):
@ -28,57 +26,26 @@ def input_not_in_inputs(ctx: typer.Context, input_name: str) -> str:
return input_name return input_name
def scene_in_scenes(ctx: typer.Context, scene_name: Optional[str]) -> str | None: def scene_in_scenes(ctx: typer.Context, scene_name: str) -> bool:
"""Check if a scene exists in the list of scenes.""" """Check if a scene exists in the list of scenes."""
if scene_name is None:
return
resp = ctx.obj['obsws'].get_scene_list() resp = ctx.obj['obsws'].get_scene_list()
if not any(scene.get('sceneName') == scene_name for scene in resp.scenes): return any(scene.get('sceneName') == scene_name for scene in resp.scenes)
console.err.print(f'Scene [yellow]{scene_name}[/yellow] not found.')
raise typer.Exit(1)
return scene_name
def studio_mode_enabled(ctx: typer.Context, preview: bool) -> bool: def studio_mode_enabled(ctx: typer.Context) -> bool:
"""Ensure studio mode is enabled if preview option is used.""" """Check if studio mode is enabled."""
resp = ctx.obj['obsws'].get_studio_mode_enabled() resp = ctx.obj['obsws'].get_studio_mode_enabled()
if preview and not resp.studio_mode_enabled: return resp.studio_mode_enabled
console.err.print(
'Studio mode is disabled. This action requires it to be enabled.'
)
raise typer.Exit(1)
return preview
def scene_collection_in_scene_collections( def scene_collection_in_scene_collections(
ctx: typer.Context, scene_collection_name: str ctx: typer.Context, scene_collection_name: str
) -> str: ) -> bool:
"""Ensure a scene collection exists in the list of scene collections.""" """Check if a scene collection exists."""
resp = ctx.obj['obsws'].get_scene_collection_list() resp = ctx.obj['obsws'].get_scene_collection_list()
if not any( return any(
collection == scene_collection_name for collection in resp.scene_collections collection == scene_collection_name for collection in resp.scene_collections
): )
console.err.print(
f'Scene collection [yellow]{scene_collection_name}[/yellow] not found.'
)
raise typer.Exit(1)
return scene_collection_name
def scene_collection_not_in_scene_collections(
ctx: typer.Context, scene_collection_name: str
) -> str:
"""Ensure a scene collection does not already exist in the list of scene collections."""
resp = ctx.obj['obsws'].get_scene_collection_list()
if any(
collection == scene_collection_name for collection in resp.scene_collections
):
console.err.print(
f'Scene collection [yellow]{scene_collection_name}[/yellow] already exists.'
)
raise typer.Exit(1)
return scene_collection_name
def item_in_scene_item_list( def item_in_scene_item_list(
@ -89,22 +56,16 @@ def item_in_scene_item_list(
return any(item.get('sourceName') == item_name for item in resp.scene_items) return any(item.get('sourceName') == item_name for item in resp.scene_items)
def profile_exists(ctx: typer.Context, profile_name: str) -> str: def profile_exists(ctx: typer.Context, profile_name: str) -> bool:
"""Ensure a profile exists.""" """Check if a profile exists."""
resp = ctx.obj['obsws'].get_profile_list() resp = ctx.obj['obsws'].get_profile_list()
if not any(profile == profile_name for profile in resp.profiles): return any(profile == profile_name for profile in resp.profiles)
console.err.print(f'Profile [yellow]{profile_name}[/yellow] not found.')
raise typer.Exit(1)
return profile_name
def profile_not_exists(ctx: typer.Context, profile_name: str) -> str: def monitor_exists(ctx: typer.Context, monitor_index: int) -> bool:
"""Ensure a profile does not exist.""" """Check if a monitor exists."""
resp = ctx.obj['obsws'].get_profile_list() resp = ctx.obj['obsws'].get_monitor_list()
if any(profile == profile_name for profile in resp.profiles): return any(monitor['monitorIndex'] == monitor_index for monitor in resp.monitors)
console.err.print(f'Profile [yellow]{profile_name}[/yellow] already exists.')
raise typer.Exit(1)
return profile_name
def kind_in_input_kinds(ctx: typer.Context, input_kind: str) -> str: def kind_in_input_kinds(ctx: typer.Context, input_kind: str) -> str:
@ -114,29 +75,3 @@ def kind_in_input_kinds(ctx: typer.Context, input_kind: str) -> str:
console.err.print(f'Input kind [yellow]{input_kind}[/yellow] not found.') console.err.print(f'Input kind [yellow]{input_kind}[/yellow] not found.')
raise typer.Exit(1) raise typer.Exit(1)
return input_kind return input_kind
def timecode_format(ctx: typer.Context, timecode: Optional[str]) -> str | None:
"""Validate that a timecode is in HH:MM:SS or MM:SS format."""
if timecode is None:
return
match timecode.split(':'):
case [mm, ss]:
if not (mm.isdigit() and ss.isdigit()):
console.err.print(
f'Timecode [yellow]{timecode}[/yellow] is not valid. Use MM:SS or HH:MM:SS format.'
)
raise typer.Exit(1)
case [hh, mm, ss]:
if not (hh.isdigit() and mm.isdigit() and ss.isdigit()):
console.err.print(
f'Timecode [yellow]{timecode}[/yellow] is not valid. Use MM:SS or HH:MM:SS format.'
)
raise typer.Exit(1)
case _:
console.err.print(
f'Timecode [yellow]{timecode}[/yellow] is not valid. Use MM:SS or HH:MM:SS format.'
)
raise typer.Exit(1)
return timecode

View File

@ -2,9 +2,10 @@
import typer import typer
from obsws_cli import console from . import console
from .alias import SubTyperAliasGroup
app = typer.Typer() app = typer.Typer(cls=SubTyperAliasGroup)
@app.callback() @app.callback()
@ -12,24 +13,21 @@ def main():
"""Control virtual camera in OBS.""" """Control virtual camera in OBS."""
@app.command('start') @app.command('start | s')
@app.command('s', hidden=True)
def start(ctx: typer.Context): def start(ctx: typer.Context):
"""Start the virtual camera.""" """Start the virtual camera."""
ctx.obj['obsws'].start_virtual_cam() ctx.obj['obsws'].start_virtual_cam()
console.out.print('Virtual camera started.') console.out.print('Virtual camera started.')
@app.command('stop') @app.command('stop | p')
@app.command('p', hidden=True)
def stop(ctx: typer.Context): def stop(ctx: typer.Context):
"""Stop the virtual camera.""" """Stop the virtual camera."""
ctx.obj['obsws'].stop_virtual_cam() ctx.obj['obsws'].stop_virtual_cam()
console.out.print('Virtual camera stopped.') console.out.print('Virtual camera stopped.')
@app.command('toggle') @app.command('toggle | tg')
@app.command('tg', hidden=True)
def toggle(ctx: typer.Context): def toggle(ctx: typer.Context):
"""Toggle the virtual camera.""" """Toggle the virtual camera."""
resp = ctx.obj['obsws'].toggle_virtual_cam() resp = ctx.obj['obsws'].toggle_virtual_cam()
@ -39,8 +37,7 @@ def toggle(ctx: typer.Context):
console.out.print('Virtual camera is disabled.') console.out.print('Virtual camera is disabled.')
@app.command('status') @app.command('status | ss')
@app.command('ss', hidden=True)
def status(ctx: typer.Context): def status(ctx: typer.Context):
"""Get the status of the virtual camera.""" """Get the status of the virtual camera."""
resp = ctx.obj['obsws'].get_virtual_cam_status() resp = ctx.obj['obsws'].get_virtual_cam_status()

View File

@ -35,9 +35,6 @@ obsws-cli = "obsws_cli:app"
[tool.hatch.version] [tool.hatch.version]
path = "obsws_cli/__about__.py" path = "obsws_cli/__about__.py"
[tool.hatch.env]
requires = ["hatch-dotenv"]
[tool.hatch.envs.default] [tool.hatch.envs.default]
dependencies = ["click-man>=0.5.1"] dependencies = ["click-man>=0.5.1"]
@ -48,10 +45,6 @@ man = "python man/generate.py --output=./man"
[tool.hatch.envs.hatch-test] [tool.hatch.envs.hatch-test]
randomize = true randomize = true
[tool.hatch.env.collectors.dotenv.hatch-test]
env-files = [".env", ".test.env"]
fail-on-missing = true
[tool.hatch.envs.types] [tool.hatch.envs.types]
extra-dependencies = ["mypy>=1.0.0"] extra-dependencies = ["mypy>=1.0.0"]
[tool.hatch.envs.types.scripts] [tool.hatch.envs.types.scripts]

View File

@ -4,6 +4,7 @@ import os
import time import time
import obsws_python as obsws import obsws_python as obsws
from dotenv import find_dotenv, load_dotenv
def pytest_configure(config): def pytest_configure(config):
@ -20,9 +21,9 @@ def pytest_sessionstart(session):
""" """
# Initialize the OBS WebSocket client # Initialize the OBS WebSocket client
session.obsws = obsws.ReqClient( session.obsws = obsws.ReqClient(
host=os.environ['OBSWS_CLI_HOST'], host=os.environ['OBS_HOST'],
port=os.environ['OBSWS_CLI_PORT'], port=os.environ['OBS_PORT'],
password=os.environ['OBSWS_CLI_PASSWORD'], password=os.environ['OBS_PASSWORD'],
timeout=5, timeout=5,
) )
resp = session.obsws.get_version() resp = session.obsws.get_version()
@ -33,12 +34,14 @@ def pytest_sessionstart(session):
) )
print(' '.join(out)) print(' '.join(out))
load_dotenv(find_dotenv('.test.env'))
session.obsws.set_stream_service_settings( session.obsws.set_stream_service_settings(
'rtmp_common', 'rtmp_common',
{ {
'service': 'Twitch', 'service': 'Twitch',
'server': 'auto', 'server': 'auto',
'key': os.environ['OBSWS_CLI_TESTS_STREAM_KEY'], 'key': os.environ['OBS_STREAM_KEY'],
}, },
) )
@ -60,7 +63,7 @@ def pytest_sessionstart(session):
'linux': 'pulse_output_capture', 'linux': 'pulse_output_capture',
'darwin': 'coreaudio_output_capture', 'darwin': 'coreaudio_output_capture',
} }
platform = os.environ['OBSWS_CLI_TESTS_PLATFORM'] platform = os.environ.get('OBS_TESTS_PLATFORM', os.uname().sysname.lower())
try: try:
session.obsws.create_input( session.obsws.create_input(
sceneName='pytest_scene', sceneName='pytest_scene',

View File

@ -9,7 +9,7 @@ from obsws_cli.app import app
runner = CliRunner() runner = CliRunner()
if os.environ.get('OBSWS_CLI_TESTS_SKIP_GROUP_TESTS'): if os.environ.get('OBS_TESTS_SKIP_GROUP_TESTS'):
pytest.skip( pytest.skip(
'Skipping group tests as per environment variable', allow_module_level=True 'Skipping group tests as per environment variable', allow_module_level=True
) )