Compare commits

...

19 Commits

Author SHA1 Message Date
ab71414d27 no need to create list here 2025-06-04 18:03:29 +01:00
ab0679174b patch bump 2025-06-04 17:34:59 +01:00
37781f6de7 clean up defaults in help messages 2025-06-04 17:34:45 +01:00
5e84becc57 wrap annotations with Annotated 2025-06-04 16:46:29 +01:00
b8dd94ccbc wrap annotations with Annotated 2025-06-04 16:31:43 +01:00
657fa84ea3 wrap scene switch annotations with Annotated 2025-06-04 16:26:27 +01:00
59f52417cd wrap annotations with Annotated 2025-06-04 15:52:35 +01:00
2d351e00b5 wrap annotations with Annotated 2025-06-04 15:49:44 +01:00
5f606b42d0 wrap annotations with Annotated 2025-06-04 15:46:52 +01:00
ae4ec542aa wrap annotations with Annotated 2025-06-04 15:39:53 +01:00
6ac63aa5e8 patch bump 2025-06-04 15:27:03 +01:00
df90614352 add Changed filter list to 0.16.1 2025-06-04 15:25:14 +01:00
d8e89285cc upd Filter section in readme 2025-06-04 15:24:48 +01:00
3e2a1e4663 wrap annotations with Annotated
filter list source_name now optional, defaults to current scene

filter list now prints default values if they are unchanged
2025-06-04 15:24:35 +01:00
723d79e306 dry up the imports 2025-06-04 15:23:13 +01:00
868d40ec8d minor bump 2025-06-04 12:53:20 +01:00
30f19f4d87 add 0.16.0 to CHANGELOG 2025-06-04 12:53:04 +01:00
5b9dd97167 add screenshot sub typer 2025-06-04 12:52:51 +01:00
d41ad994b7 add screenshot section 2025-06-04 12:51:47 +01:00
14 changed files with 378 additions and 92 deletions

View File

@ -5,6 +5,18 @@ 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.16.1] - 2025-06-04
### Added
- screenshot save command, see [Screenshot](https://github.com/onyx-and-iris/obsws-cli/tree/main?tab=readme-ov-file#screenshot)
### Changed
- filter list:
- source_name arg is now optional, it defaults to the current scene.
- default values are printed if unmodified.
# [0.15.0] - 2025-06-02 # [0.15.0] - 2025-06-02
### Added ### Added

View File

@ -519,7 +519,10 @@ obsws-cli hotkey trigger-sequence OBS_KEY_F1 --shift --ctrl
#### Filter #### Filter
- list: List filters for a source. - list: List filters for a source.
*optional*
- args: <source_name> - args: <source_name>
- defaults to current scene
```console ```console
obsws-cli filter list "Mic/Aux" obsws-cli filter list "Mic/Aux"
@ -580,6 +583,24 @@ obsws-cli projector open --monitor-index=1 "test_scene"
obsws-cli projector open --monitor-index=1 "test_group" obsws-cli projector open --monitor-index=1 "test_group"
``` ```
#### Screenshot
- save: Take a screenshot and save it to a file.
- flags:
*optional*
- --width:
- defaults to 1920
- --height:
- defaults to 1080
- --quality:
- defaults to -1
- args: <source_name> <output_path>
```console
obsws-cli screenshot save --width=2560 --height=1440 "Scene" "C:\Users\me\Videos\screenshot.png"
```
## 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.15.2" __version__ = "0.16.2"

View File

@ -1,5 +1,6 @@
"""Command line interface for the OBS WebSocket API.""" """Command line interface for the OBS WebSocket API."""
import importlib
from typing import Annotated from typing import Annotated
import obsws_python as obsws import obsws_python as obsws
@ -8,43 +9,29 @@ from rich.console import Console
from obsws_cli.__about__ import __version__ as obsws_cli_version from obsws_cli.__about__ import __version__ as obsws_cli_version
from . import ( from . import settings
filter,
group,
hotkey,
input,
profile,
projector,
record,
replaybuffer,
scene,
scenecollection,
sceneitem,
settings,
stream,
studiomode,
virtualcam,
)
from .alias import AliasGroup from .alias import AliasGroup
app = typer.Typer(cls=AliasGroup) app = typer.Typer(cls=AliasGroup)
for module in ( for sub_typer in (
filter, 'filter',
group, 'group',
hotkey, 'hotkey',
input, 'input',
projector, 'profile',
profile, 'projector',
record, 'record',
replaybuffer, 'replaybuffer',
scene, 'scene',
scenecollection, 'scenecollection',
sceneitem, 'sceneitem',
stream, 'screenshot',
studiomode, 'stream',
virtualcam, 'studiomode',
'virtualcam',
): ):
app.add_typer(module.app, name=module.__name__.split('.')[-1]) module = importlib.import_module(f'.{sub_typer}', package=__package__)
app.add_typer(module.app, name=sub_typer)
out_console = Console() out_console = Console()
err_console = Console(stderr=True) err_console = Console(stderr=True)

View File

@ -1,5 +1,7 @@
"""module containing commands for manipulating filters in scenes.""" """module containing commands for manipulating filters in scenes."""
from typing import Annotated, Optional
import obsws_python as obsws import obsws_python as obsws
import typer import typer
from rich.console import Console from rich.console import Console
@ -19,8 +21,20 @@ def main():
@app.command('list | ls') @app.command('list | ls')
def list(ctx: typer.Context, source_name: str): def list_(
ctx: typer.Context,
source_name: Annotated[
Optional[str],
typer.Argument(
show_default='The current scene',
help='The source to list filters for',
),
] = None,
):
"""List filters for a source.""" """List filters for a source."""
if not source_name:
source_name = ctx.obj.get_current_program_scene().scene_name
try: try:
resp = ctx.obj.get_source_filter_list(source_name) resp = ctx.obj.get_source_filter_list(source_name)
except obsws.error.OBSSDKRequestError as e: except obsws.error.OBSSDKRequestError as e:
@ -44,6 +58,9 @@ def list(ctx: typer.Context, source_name: str):
) )
for filter in resp.filters: for filter in resp.filters:
resp = ctx.obj.get_source_filter_default_settings(filter['filterKind'])
settings = resp.default_filter_settings | filter['filterSettings']
table.add_row( table.add_row(
filter['filterName'], filter['filterName'],
util.snakecase_to_titlecase(filter['filterKind']), util.snakecase_to_titlecase(filter['filterKind']),
@ -51,7 +68,7 @@ def list(ctx: typer.Context, source_name: str):
'\n'.join( '\n'.join(
[ [
f'{util.snakecase_to_titlecase(k):<20} {v:>10}' f'{util.snakecase_to_titlecase(k):<20} {v:>10}'
for k, v in filter['filterSettings'].items() for k, v in settings.items()
] ]
), ),
) )
@ -68,8 +85,18 @@ def _get_filter_enabled(ctx: typer.Context, source_name: str, filter_name: str):
@app.command('enable | on') @app.command('enable | on')
def enable( def enable(
ctx: typer.Context, ctx: typer.Context,
source_name: str = typer.Argument(..., help='The source to enable the filter for'), source_name: Annotated[
filter_name: str = typer.Argument(..., help='The name of the filter to enable'), str,
typer.Argument(
..., show_default=False, help='The source to enable the filter for'
),
],
filter_name: Annotated[
str,
typer.Argument(
..., show_default=False, help='The name of the filter to enable'
),
],
): ):
"""Enable a filter for a source.""" """Enable a filter for a source."""
if _get_filter_enabled(ctx, source_name, filter_name): if _get_filter_enabled(ctx, source_name, filter_name):
@ -85,8 +112,18 @@ def enable(
@app.command('disable | off') @app.command('disable | off')
def disable( def disable(
ctx: typer.Context, ctx: typer.Context,
source_name: str = typer.Argument(..., help='The source to disable the filter for'), source_name: Annotated[
filter_name: str = typer.Argument(..., help='The name of the filter to disable'), str,
typer.Argument(
..., show_default=False, help='The source to disable the filter for'
),
],
filter_name: Annotated[
str,
typer.Argument(
..., show_default=False, help='The name of the filter to disable'
),
],
): ):
"""Disable a filter for a source.""" """Disable a filter for a source."""
if not _get_filter_enabled(ctx, source_name, filter_name): if not _get_filter_enabled(ctx, source_name, filter_name):
@ -102,8 +139,18 @@ def disable(
@app.command('toggle | tg') @app.command('toggle | tg')
def toggle( def toggle(
ctx: typer.Context, ctx: typer.Context,
source_name: str = typer.Argument(..., help='The source to toggle the filter for'), source_name: Annotated[
filter_name: str = typer.Argument(..., help='The name of the filter to toggle'), str,
typer.Argument(
..., show_default=False, help='The source to toggle the filter for'
),
],
filter_name: Annotated[
str,
typer.Argument(
..., show_default=False, help='The name of the filter to toggle'
),
],
): ):
"""Toggle a filter for a source.""" """Toggle a filter for a source."""
is_enabled = _get_filter_enabled(ctx, source_name, filter_name) is_enabled = _get_filter_enabled(ctx, source_name, filter_name)
@ -119,12 +166,18 @@ def toggle(
@app.command('status | ss') @app.command('status | ss')
def status( def status(
ctx: typer.Context, ctx: typer.Context,
source_name: str = typer.Argument( source_name: Annotated[
..., help='The source to get the filter status for' str,
), typer.Argument(
filter_name: str = typer.Argument( ..., show_default=False, help='The source to get the filter status for'
..., help='The name of the filter to get the status for' ),
), ],
filter_name: Annotated[
str,
typer.Argument(
..., show_default=False, help='The name of the filter to get the status for'
),
],
): ):
"""Get the status of a filter for a source.""" """Get the status of a filter for a source."""
is_enabled = _get_filter_enabled(ctx, source_name, filter_name) is_enabled = _get_filter_enabled(ctx, source_name, filter_name)

View File

@ -1,5 +1,7 @@
"""module containing commands for manipulating groups in scenes.""" """module containing commands for manipulating groups in scenes."""
from typing import Annotated, Optional
import typer import typer
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
@ -19,11 +21,15 @@ def main():
@app.command('list | ls') @app.command('list | ls')
def list( def list_(
ctx: typer.Context, ctx: typer.Context,
scene_name: str = typer.Argument( scene_name: Annotated[
None, help='Scene name (optional, defaults to current scene)' Optional[str],
), typer.Argument(
show_default='The current scene',
help='Scene name to list groups for',
),
] = None,
): ):
"""List groups in a scene.""" """List groups in a scene."""
if not scene_name: if not scene_name:
@ -75,7 +81,16 @@ def _get_group(group_name: str, resp: DataclassProtocol) -> dict | None:
@app.command('show | sh') @app.command('show | sh')
def show(ctx: typer.Context, scene_name: str, group_name: str): def show(
ctx: typer.Context,
scene_name: Annotated[
str,
typer.Argument(..., show_default=False, help='Scene name the group is in'),
],
group_name: Annotated[
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): if not validate.scene_in_scenes(ctx, scene_name):
err_console.print(f"Scene '{scene_name}' not found.") err_console.print(f"Scene '{scene_name}' not found.")
@ -96,7 +111,15 @@ def show(ctx: typer.Context, scene_name: str, group_name: str):
@app.command('hide | h') @app.command('hide | h')
def hide(ctx: typer.Context, scene_name: str, group_name: str): def hide(
ctx: typer.Context,
scene_name: Annotated[
str, typer.Argument(..., show_default=False, help='Scene name the group is in')
],
group_name: Annotated[
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): if not validate.scene_in_scenes(ctx, scene_name):
err_console.print(f"Scene '{scene_name}' not found.") err_console.print(f"Scene '{scene_name}' not found.")
@ -117,7 +140,15 @@ def hide(ctx: typer.Context, scene_name: str, group_name: str):
@app.command('toggle | tg') @app.command('toggle | tg')
def toggle(ctx: typer.Context, scene_name: str, group_name: str): def toggle(
ctx: typer.Context,
scene_name: Annotated[
str, typer.Argument(..., show_default=False, help='Scene name the group is in')
],
group_name: Annotated[
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): if not validate.scene_in_scenes(ctx, scene_name):
err_console.print(f"Scene '{scene_name}' not found.") err_console.print(f"Scene '{scene_name}' not found.")
@ -142,7 +173,15 @@ def toggle(ctx: typer.Context, scene_name: str, group_name: str):
@app.command('status | ss') @app.command('status | ss')
def status(ctx: typer.Context, scene_name: str, group_name: str): def status(
ctx: typer.Context,
scene_name: Annotated[
str, typer.Argument(..., show_default=False, help='Scene name the group is in')
],
group_name: Annotated[
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): if not validate.scene_in_scenes(ctx, scene_name):
err_console.print(f"Scene '{scene_name}' not found.") err_console.print(f"Scene '{scene_name}' not found.")

View File

@ -1,5 +1,7 @@
"""module containing commands for hotkey management.""" """module containing commands for hotkey management."""
from typing import Annotated
import typer import typer
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
@ -17,7 +19,7 @@ def main():
@app.command('list | ls') @app.command('list | ls')
def list( def list_(
ctx: typer.Context, ctx: typer.Context,
): ):
"""List all hotkeys.""" """List all hotkeys."""
@ -35,7 +37,9 @@ def list(
@app.command('trigger | tr') @app.command('trigger | tr')
def trigger( def trigger(
ctx: typer.Context, ctx: typer.Context,
hotkey: str = typer.Argument(..., help='The hotkey to trigger'), hotkey: Annotated[
str, typer.Argument(..., show_default=False, help='The hotkey to trigger')
],
): ):
"""Trigger a hotkey by name.""" """Trigger a hotkey by name."""
ctx.obj.trigger_hotkey_by_name(hotkey) ctx.obj.trigger_hotkey_by_name(hotkey)
@ -44,14 +48,26 @@ def trigger(
@app.command('trigger-sequence | trs') @app.command('trigger-sequence | trs')
def trigger_sequence( def trigger_sequence(
ctx: typer.Context, ctx: typer.Context,
shift: bool = typer.Option(False, help='Press shift when triggering the hotkey'), key_id: Annotated[
ctrl: bool = typer.Option(False, help='Press control when triggering the hotkey'), str,
alt: bool = typer.Option(False, help='Press alt when triggering the hotkey'), typer.Argument(
cmd: bool = typer.Option(False, help='Press cmd when triggering the hotkey'), ...,
key_id: str = typer.Argument( show_default=False,
..., help='The OBS key ID to trigger, see https://github.com/onyx-and-iris/obsws-cli?tab=readme-ov-file#hotkey for more info',
help='The OBS key ID to trigger, see https://github.com/onyx-and-iris/obsws-cli?tab=readme-ov-file#hotkey for more info', ),
), ],
shift: Annotated[
bool, typer.Option(..., help='Press shift when triggering the hotkey')
] = False,
ctrl: Annotated[
bool, typer.Option(..., help='Press control when triggering the hotkey')
] = False,
alt: Annotated[
bool, typer.Option(..., help='Press alt when triggering the hotkey')
] = False,
cmd: Annotated[
bool, typer.Option(..., help='Press cmd when triggering the hotkey')
] = False,
): ):
"""Trigger a hotkey by sequence.""" """Trigger a hotkey by sequence."""
ctx.obj.trigger_hotkey_by_key_sequence(key_id, shift, ctrl, alt, cmd) ctx.obj.trigger_hotkey_by_key_sequence(key_id, shift, ctrl, alt, cmd)

View File

@ -20,7 +20,7 @@ def main():
@app.command('list | ls') @app.command('list | ls')
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,
output: Annotated[bool, typer.Option(help='Filter by output type.')] = False, output: Annotated[bool, typer.Option(help='Filter by output type.')] = False,
@ -67,7 +67,12 @@ def list(
@app.command('mute | m') @app.command('mute | m')
def mute(ctx: typer.Context, input_name: str): def mute(
ctx: typer.Context,
input_name: Annotated[
str, typer.Argument(..., show_default=False, help='Name of the input to mute.')
],
):
"""Mute an input.""" """Mute an input."""
if not validate.input_in_inputs(ctx, input_name): if not validate.input_in_inputs(ctx, input_name):
err_console.print(f"Input '{input_name}' not found.") err_console.print(f"Input '{input_name}' not found.")
@ -82,7 +87,13 @@ def mute(ctx: typer.Context, input_name: str):
@app.command('unmute | um') @app.command('unmute | um')
def unmute(ctx: typer.Context, input_name: str): def unmute(
ctx: typer.Context,
input_name: Annotated[
str,
typer.Argument(..., show_default=False, help='Name of the input to unmute.'),
],
):
"""Unmute an input.""" """Unmute an input."""
if not validate.input_in_inputs(ctx, input_name): if not validate.input_in_inputs(ctx, input_name):
err_console.print(f"Input '{input_name}' not found.") err_console.print(f"Input '{input_name}' not found.")
@ -97,7 +108,13 @@ def unmute(ctx: typer.Context, input_name: str):
@app.command('toggle | tg') @app.command('toggle | tg')
def toggle(ctx: typer.Context, input_name: str): def toggle(
ctx: typer.Context,
input_name: Annotated[
str,
typer.Argument(..., show_default=False, help='Name of the input to toggle.'),
],
):
"""Toggle an input.""" """Toggle an input."""
if not validate.input_in_inputs(ctx, input_name): if not validate.input_in_inputs(ctx, input_name):
err_console.print(f"Input '{input_name}' not found.") err_console.print(f"Input '{input_name}' not found.")

View File

@ -1,5 +1,7 @@
"""module containing commands for manipulating profiles in OBS.""" """module containing commands for manipulating profiles in OBS."""
from typing import Annotated
import typer import typer
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
@ -18,7 +20,7 @@ def main():
@app.command('list | ls') @app.command('list | ls')
def list(ctx: typer.Context): def list_(ctx: typer.Context):
"""List profiles.""" """List profiles."""
resp = ctx.obj.get_profile_list() resp = ctx.obj.get_profile_list()
@ -47,7 +49,15 @@ def current(ctx: typer.Context):
@app.command('switch | set') @app.command('switch | set')
def switch(ctx: typer.Context, profile_name: str): def switch(
ctx: typer.Context,
profile_name: Annotated[
str,
typer.Argument(
..., show_default=False, help='Name of the profile to switch to'
),
],
):
"""Switch to a profile.""" """Switch to a profile."""
if not validate.profile_exists(ctx, profile_name): if not validate.profile_exists(ctx, profile_name):
err_console.print(f"Profile '{profile_name}' not found.") err_console.print(f"Profile '{profile_name}' not found.")
@ -63,7 +73,13 @@ def switch(ctx: typer.Context, profile_name: str):
@app.command('create | new') @app.command('create | new')
def create(ctx: typer.Context, profile_name: str): def create(
ctx: typer.Context,
profile_name: Annotated[
str,
typer.Argument(..., show_default=False, help='Name of the profile to create.'),
],
):
"""Create a new profile.""" """Create a new profile."""
if validate.profile_exists(ctx, profile_name): if validate.profile_exists(ctx, profile_name):
err_console.print(f"Profile '{profile_name}' already exists.") err_console.print(f"Profile '{profile_name}' already exists.")
@ -74,7 +90,13 @@ def create(ctx: typer.Context, profile_name: str):
@app.command('remove | rm') @app.command('remove | rm')
def remove(ctx: typer.Context, profile_name: str): def remove(
ctx: typer.Context,
profile_name: Annotated[
str,
typer.Argument(..., show_default=False, help='Name of the profile to remove.'),
],
):
"""Remove a profile.""" """Remove a profile."""
if not validate.profile_exists(ctx, profile_name): if not validate.profile_exists(ctx, profile_name):
err_console.print(f"Profile '{profile_name}' not found.") err_console.print(f"Profile '{profile_name}' not found.")

View File

@ -52,7 +52,8 @@ def open(
source_name: Annotated[ source_name: Annotated[
str, str,
typer.Argument( typer.Argument(
help='Name of the source to project. (optional, defaults to current scene)' show_default='The current scene',
help='Name of the source to project.',
), ),
] = '', ] = '',
): ):

View File

@ -20,7 +20,7 @@ def main():
@app.command('list | ls') @app.command('list | ls')
def list(ctx: typer.Context): def list_(ctx: typer.Context):
"""List all scenes.""" """List all scenes."""
resp = ctx.obj.get_scene_list() resp = ctx.obj.get_scene_list()
scenes = ( scenes = (
@ -64,7 +64,9 @@ def current(
@app.command('switch | set') @app.command('switch | set')
def switch( def switch(
ctx: typer.Context, ctx: typer.Context,
scene_name: str, scene_name: Annotated[
str, typer.Argument(..., help='Name of the scene to switch to')
],
preview: Annotated[ preview: Annotated[
bool, bool,
typer.Option(help='Switch to the preview scene instead of the program scene'), typer.Option(help='Switch to the preview scene instead of the program scene'),

View File

@ -1,5 +1,7 @@
"""module containing commands for manipulating scene collections.""" """module containing commands for manipulating scene collections."""
from typing import Annotated
import typer import typer
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
@ -18,7 +20,7 @@ def main():
@app.command('list | ls') @app.command('list | ls')
def list(ctx: typer.Context): def list_(ctx: typer.Context):
"""List all scene collections.""" """List all scene collections."""
resp = ctx.obj.get_scene_collection_list() resp = ctx.obj.get_scene_collection_list()
@ -39,7 +41,12 @@ def current(ctx: typer.Context):
@app.command('switch | set') @app.command('switch | set')
def switch(ctx: typer.Context, scene_collection_name: str): def switch(
ctx: typer.Context,
scene_collection_name: Annotated[
str, typer.Argument(..., help='Name of the scene collection to switch to')
],
):
"""Switch to a scene collection.""" """Switch to a scene collection."""
if not validate.scene_collection_in_scene_collections(ctx, scene_collection_name): if not validate.scene_collection_in_scene_collections(ctx, scene_collection_name):
err_console.print(f"Scene collection '{scene_collection_name}' not found.") err_console.print(f"Scene collection '{scene_collection_name}' not found.")
@ -59,7 +66,12 @@ def switch(ctx: typer.Context, scene_collection_name: str):
@app.command('create | new') @app.command('create | new')
def create(ctx: typer.Context, scene_collection_name: str): def create(
ctx: typer.Context,
scene_collection_name: Annotated[
str, typer.Argument(..., help='Name of the scene collection to create')
],
):
"""Create a new scene collection.""" """Create a new scene collection."""
if validate.scene_collection_in_scene_collections(ctx, scene_collection_name): if validate.scene_collection_in_scene_collections(ctx, scene_collection_name):
err_console.print(f"Scene collection '{scene_collection_name}' already exists.") err_console.print(f"Scene collection '{scene_collection_name}' already exists.")

View File

@ -22,11 +22,15 @@ def main():
@app.command('list | ls') @app.command('list | ls')
def list( def list_(
ctx: typer.Context, ctx: typer.Context,
scene_name: str = typer.Argument( scene_name: Annotated[
None, help='Scene name (optional, defaults to current scene)' Optional[str],
), typer.Argument(
show_default='The current scene',
help='Scene name to list items for',
),
] = None,
): ):
"""List all items in a scene.""" """List all items in a scene."""
if not scene_name: if not scene_name:
@ -163,8 +167,10 @@ def _get_scene_name_and_item_id(
@app.command('show | sh') @app.command('show | sh')
def show( def show(
ctx: typer.Context, ctx: typer.Context,
scene_name: str, scene_name: Annotated[str, typer.Argument(..., help='Scene name the item is in')],
item_name: str, item_name: Annotated[
str, typer.Argument(..., help='Item name to show in the scene')
],
group: Annotated[Optional[str], typer.Option(help='Parent group name')] = None, group: Annotated[Optional[str], typer.Option(help='Parent group name')] = None,
): ):
"""Show an item in a scene.""" """Show an item in a scene."""
@ -194,8 +200,10 @@ def show(
@app.command('hide | h') @app.command('hide | h')
def hide( def hide(
ctx: typer.Context, ctx: typer.Context,
scene_name: str, scene_name: Annotated[str, typer.Argument(..., help='Scene name the item is in')],
item_name: str, item_name: Annotated[
str, typer.Argument(..., help='Item name to hide in the scene')
],
group: Annotated[Optional[str], typer.Option(help='Parent group name')] = None, group: Annotated[Optional[str], typer.Option(help='Parent group name')] = None,
): ):
"""Hide an item in a scene.""" """Hide an item in a scene."""
@ -227,8 +235,10 @@ def hide(
@app.command('toggle | tg') @app.command('toggle | tg')
def toggle( def toggle(
ctx: typer.Context, ctx: typer.Context,
scene_name: str, scene_name: Annotated[str, typer.Argument(..., help='Scene name the item is in')],
item_name: str, item_name: Annotated[
str, typer.Argument(..., help='Item name to toggle in the scene')
],
group: Annotated[Optional[str], typer.Option(help='Parent group name')] = None, group: Annotated[Optional[str], typer.Option(help='Parent group name')] = None,
): ):
"""Toggle an item in a scene.""" """Toggle an item in a scene."""
@ -291,8 +301,10 @@ def toggle(
@app.command('visible | v') @app.command('visible | v')
def visible( def visible(
ctx: typer.Context, ctx: typer.Context,
scene_name: str, scene_name: Annotated[str, typer.Argument(..., help='Scene name the item is in')],
item_name: str, item_name: Annotated[
str, typer.Argument(..., help='Item name to check visibility in the scene')
],
group: Annotated[Optional[str], typer.Option(help='Parent group name')] = None, group: Annotated[Optional[str], typer.Option(help='Parent group name')] = None,
): ):
"""Check if an item in a scene is visible.""" """Check if an item in a scene is visible."""

92
obsws_cli/screenshot.py Normal file
View File

@ -0,0 +1,92 @@
"""module for taking screenshots using OBS WebSocket API."""
from pathlib import Path
from typing import Annotated
import obsws_python as obsws
import typer
from rich.console import Console
from .alias import AliasGroup
app = typer.Typer(cls=AliasGroup)
out_console = Console()
err_console = Console(
stderr=True,
)
@app.callback()
def main():
"""Take screenshots using OBS."""
@app.command('save | sv')
def save(
ctx: typer.Context,
source_name: Annotated[
str,
typer.Argument(
...,
show_default=False,
help='Name of the source to take a screenshot of.',
),
],
output_path: Annotated[
Path,
# Since the CLI and OBS may be running on different platforms,
# we won't validate the path here.
typer.Argument(
...,
show_default=False,
file_okay=True,
dir_okay=False,
help='Path to save the screenshot (must include file name and extension).',
),
],
width: Annotated[
float,
typer.Option(
help='Width of the screenshot.',
),
] = 1920,
height: Annotated[
float,
typer.Option(
help='Height of the screenshot.',
),
] = 1080,
quality: Annotated[
float,
typer.Option(
min=-1,
max=100,
help='Quality of the screenshot.',
),
] = -1,
):
"""Take a screenshot and save it to a file."""
try:
ctx.obj.save_source_screenshot(
name=source_name,
img_format=output_path.suffix.lstrip('.').lower(),
file_path=str(output_path),
width=width,
height=height,
quality=quality,
)
except obsws.error.OBSSDKRequestError as e:
match e.code:
case 403:
err_console.print(
'The image format (file extension) must be included in the file name '
"for example: '/path/to/screenshot.png'.",
)
raise typer.Exit(1)
case 600:
err_console.print(f"No source was found by the name of '{source_name}'")
raise typer.Exit(1)
case _:
raise
out_console.print(f"Screenshot saved to [bold]'{output_path}'[/bold].")