10 Commits

Author SHA1 Message Date
4bf8edb692 add 0.19.0 to CHANGELOG 2025-06-23 09:11:26 +01:00
d68326f37a add record split/chapter to README 2025-06-23 09:11:15 +01:00
a001455dad add record split/chapter commands 2025-06-23 09:10:53 +01:00
4632260961 add --style validation
add Disabled class to style registry

patch bump
2025-06-22 12:35:21 +01:00
55a7da67db reword 2025-06-22 10:19:46 +01:00
7bec573ef9 by setting values in the default style to 'none' we avoid the rich markup errors in console.highlight
add comment to util.check_mark and test only NO_COLOR

patch bump
2025-06-22 10:14:46 +01:00
55e60ff977 in case NO_COLOR is set manually
patch bump
2025-06-22 02:49:32 +01:00
922efddf7a check if we're in colourless mode before passing back highlighted text.
pass context to check_mark so we can do the same there.

Fixes  rich.errors.MarkupError
2025-06-22 01:57:58 +01:00
4a0147aa8a import as version 2025-06-22 00:38:19 +01:00
cec76df1d1 add 0.18.0 to CHANGELOG 2025-06-21 23:47:19 +01:00
8 changed files with 126 additions and 45 deletions

View File

@@ -5,6 +5,22 @@ 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.19.0] - 2025-06-23
### Added
- record split and record chapter commands, see [Record](https://github.com/onyx-and-iris/obsws-cli?tab=readme-ov-file#record)
- As of OBS 30.2.0, the only file format supporting *record chapter* is Hybrid MP4.
# [0.18.0] - 2025-06-21
### Added
- Various colouring styles, see [Style](https://github.com/onyx-and-iris/obsws-cli/tree/main?tab=readme-ov-file#style)
- colouring is applied to list tables as well as highlighted information in stdout/stderr output.
- table border styling may be optionally disabled with the --no-border flag.
# [0.17.3] - 2025-06-20 # [0.17.3] - 2025-06-20
### Added ### Added

View File

@@ -354,6 +354,21 @@ obsws-cli record directory "/home/me/obs-vids/"
obsws-cli record directory "C:/Users/me/Videos" obsws-cli record directory "C:/Users/me/Videos"
``` ```
- split: Split the current recording.
```console
obsws-cli record split
```
- chapter: Create a chapter in the current recording.
*optional*
- args: <chapter_name>
```console
obsws-cli record chapter "Chapter Name"
```
#### Stream #### Stream
- start: Start streaming. - start: Start streaming.

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.17.6" __version__ = "0.19.0"

View File

@@ -7,7 +7,7 @@ from typing import Annotated
import obsws_python as obsws import obsws_python as obsws
import typer import typer
from obsws_cli.__about__ import __version__ as obsws_cli_version from obsws_cli.__about__ import __version__ as version
from . import console, settings, styles from . import console, settings, styles
from .alias import RootTyperAliasGroup from .alias import RootTyperAliasGroup
@@ -37,7 +37,7 @@ for sub_typer in (
def version_callback(value: bool): def version_callback(value: bool):
"""Show the version of the CLI.""" """Show the version of the CLI."""
if value: if value:
console.out.print(f'obsws-cli version: {obsws_cli_version}') console.out.print(f'obsws-cli version: {version}')
raise typer.Exit() raise typer.Exit()
@@ -50,6 +50,15 @@ def setup_logging(debug: bool):
) )
def validate_style(value: str):
"""Validate and return the style."""
if value not in styles.registry:
raise typer.BadParameter(
f'Invalid style: {value}. Available styles: {", ".join(styles.registry.keys())}'
)
return value
@app.callback() @app.callback()
def main( def main(
ctx: typer.Context, ctx: typer.Context,
@@ -112,6 +121,7 @@ def main(
envvar='OBS_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,
), ),
] = settings.get('style'), ] = settings.get('style'),
no_border: Annotated[ no_border: Annotated[

View File

@@ -35,7 +35,9 @@ def list_(ctx: typer.Context):
for profile in resp.profiles: for profile in resp.profiles:
table.add_row( table.add_row(
profile, profile,
util.check_mark(profile == resp.current_profile_name, empty_if_false=True), util.check_mark(
ctx, profile == resp.current_profile_name, empty_if_false=True
),
) )
console.out.print(table) console.out.print(table)

View File

@@ -130,3 +130,43 @@ def directory(
console.out.print( console.out.print(
f'Recording directory: {console.highlight(ctx, resp.record_directory)}' f'Recording directory: {console.highlight(ctx, resp.record_directory)}'
) )
@app.command('split | sp')
def split(ctx: typer.Context):
"""Split the current recording."""
active, paused = _get_recording_status(ctx)
if not active:
console.err.print('Recording is not in progress, cannot split.')
raise typer.Exit(1)
if paused:
console.err.print('Recording is paused, cannot split.')
raise typer.Exit(1)
ctx.obj['obsws'].split_record_file()
console.out.print('Recording split successfully.')
@app.command('chapter | ch')
def chapter(
ctx: typer.Context,
chapter_name: Annotated[
Optional[str],
typer.Argument(
help='Name of the chapter to create.',
),
] = None,
):
"""Create a chapter in the current recording."""
active, paused = _get_recording_status(ctx)
if not active:
console.err.print('Recording is not in progress, cannot create chapter.')
raise typer.Exit(1)
if paused:
console.err.print('Recording is paused, cannot create chapter.')
raise typer.Exit(1)
ctx.obj['obsws'].create_record_chapter(chapter_name)
console.out.print(
f'Chapter {console.highlight(chapter_name or "unnamed")} created successfully.'
)

View File

@@ -3,15 +3,15 @@
import os import os
from dataclasses import dataclass from dataclasses import dataclass
_registry = {} registry = {}
def register_style(cls): def register_style(cls):
"""Register a style class.""" """Register a style class."""
key = cls.__name__.lower() key = cls.__name__.lower()
if key in _registry: if key in registry:
raise ValueError(f'Style {key} is already registered.') raise ValueError(f'Style {key} is already registered.')
_registry[key] = cls registry[key] = cls
return cls return cls
@@ -19,11 +19,10 @@ def register_style(cls):
class Style: class Style:
"""Base class for styles.""" """Base class for styles."""
name: str = 'no_colour' name: str
description: str = 'Style disabled' border: str
border: str | None = None column: str
column: str | None = None highlight: str
highlight: str | None = None
no_border: bool = False no_border: bool = False
def __post_init__(self): def __post_init__(self):
@@ -32,9 +31,21 @@ class Style:
if self.no_border: if self.no_border:
self.border = None self.border = None
def __str__(self):
"""Return a string representation of the style.""" @register_style
return f'{self.name} - {self.description}' @dataclass
class Disabled(Style):
"""Disabled style."""
name: str = 'disabled'
border: str = 'none'
column: str = 'none'
highlight: str = 'none'
def __post_init__(self):
"""Post-initialization to set default values."""
super().__post_init__()
os.environ['NO_COLOR'] = '1'
@register_style @register_style
@@ -43,10 +54,9 @@ class Red(Style):
"""Red style.""" """Red style."""
name: str = 'red' name: str = 'red'
description: str = 'Red text color'
border: str = 'red3' border: str = 'red3'
highlight: str = 'red1'
column: str = 'red1' column: str = 'red1'
highlight: str = 'red1'
@register_style @register_style
@@ -55,10 +65,9 @@ class Magenta(Style):
"""Magenta style.""" """Magenta style."""
name: str = 'magenta' name: str = 'magenta'
description: str = 'Magenta text color'
border: str = 'magenta3' border: str = 'magenta3'
highlight: str = 'orchid1'
column: str = 'orchid1' column: str = 'orchid1'
highlight: str = 'orchid1'
@register_style @register_style
@@ -67,10 +76,9 @@ class Purple(Style):
"""Purple style.""" """Purple style."""
name: str = 'purple' name: str = 'purple'
description: str = 'Purple text color'
border: str = 'medium_purple4' border: str = 'medium_purple4'
highlight: str = 'medium_purple'
column: str = 'medium_purple' column: str = 'medium_purple'
highlight: str = 'medium_purple'
@register_style @register_style
@@ -79,10 +87,9 @@ class Blue(Style):
"""Blue style.""" """Blue style."""
name: str = 'blue' name: str = 'blue'
description: str = 'Blue text color'
border: str = 'cornflower_blue' border: str = 'cornflower_blue'
highlight: str = 'sky_blue2'
column: str = 'sky_blue2' column: str = 'sky_blue2'
highlight: str = 'sky_blue2'
@register_style @register_style
@@ -91,10 +98,9 @@ class Cyan(Style):
"""Cyan style.""" """Cyan style."""
name: str = 'cyan' name: str = 'cyan'
description: str = 'Cyan text color'
border: str = 'dark_cyan' border: str = 'dark_cyan'
highlight: str = 'cyan'
column: str = 'cyan' column: str = 'cyan'
highlight: str = 'cyan'
@register_style @register_style
@@ -103,10 +109,9 @@ class Green(Style):
"""Green style.""" """Green style."""
name: str = 'green' name: str = 'green'
description: str = 'Green text color'
border: str = 'green4' border: str = 'green4'
highlight: str = 'spring_green3'
column: str = 'spring_green3' column: str = 'spring_green3'
highlight: str = 'spring_green3'
@register_style @register_style
@@ -115,10 +120,9 @@ class Yellow(Style):
"""Yellow style.""" """Yellow style."""
name: str = 'yellow' name: str = 'yellow'
description: str = 'Yellow text color'
border: str = 'yellow3' border: str = 'yellow3'
highlight: str = 'wheat1'
column: str = 'wheat1' column: str = 'wheat1'
highlight: str = 'wheat1'
@register_style @register_style
@@ -127,10 +131,9 @@ class Orange(Style):
"""Orange style.""" """Orange style."""
name: str = 'orange' name: str = 'orange'
description: str = 'Orange text color'
border: str = 'dark_orange' border: str = 'dark_orange'
highlight: str = 'orange1'
column: str = 'orange1' column: str = 'orange1'
highlight: str = 'orange1'
@register_style @register_style
@@ -139,10 +142,9 @@ class White(Style):
"""White style.""" """White style."""
name: str = 'white' name: str = 'white'
description: str = 'White text color'
border: str = 'grey82' border: str = 'grey82'
highlight: str = 'grey100'
column: str = 'grey100' column: str = 'grey100'
highlight: str = 'grey100'
@register_style @register_style
@@ -151,10 +153,9 @@ class Grey(Style):
"""Grey style.""" """Grey style."""
name: str = 'grey' name: str = 'grey'
description: str = 'Grey text color'
border: str = 'grey50' border: str = 'grey50'
highlight: str = 'grey70'
column: str = 'grey70' column: str = 'grey70'
highlight: str = 'grey70'
@register_style @register_style
@@ -163,10 +164,9 @@ class Navy(Style):
"""Navy Blue style.""" """Navy Blue style."""
name: str = 'navyblue' name: str = 'navyblue'
description: str = 'Navy Blue text color'
border: str = 'deep_sky_blue4' border: str = 'deep_sky_blue4'
highlight: str = 'light_sky_blue3'
column: str = 'light_sky_blue3' column: str = 'light_sky_blue3'
highlight: str = 'light_sky_blue3'
@register_style @register_style
@@ -175,17 +175,11 @@ class Black(Style):
"""Black style.""" """Black style."""
name: str = 'black' name: str = 'black'
description: str = 'Black text color'
border: str = 'grey19' border: str = 'grey19'
column: str = 'grey11' column: str = 'grey11'
highlight: str = 'grey11'
def request_style_obj(style_name: str, no_border: bool) -> Style: def request_style_obj(style_name: str, no_border: bool) -> Style:
"""Entry point for style objects. Returns a Style object based on the style name.""" """Entry point for style objects. Returns a Style object based on the style name."""
style_name = str(style_name).lower() # coerce the type to string and lowercase it return registry[style_name.lower()](no_border=no_border)
if style_name not in _registry:
os.environ['NO_COLOR'] = '1' # Disable colour output
return Style()
return _registry[style_name](no_border=no_border)

View File

@@ -13,6 +13,10 @@ def check_mark(value: bool, empty_if_false: bool = False) -> str:
if empty_if_false and not value: if empty_if_false and not value:
return '' return ''
# rich gracefully handles the absence of colour throughout the rest of the application,
# but here we must handle it manually.
# If NO_COLOR is set, we return plain text symbols.
# Otherwise, we return coloured symbols.
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 ''