mirror of
https://github.com/onyx-and-iris/obsws-cli.git
synced 2026-04-20 16:03:38 +00:00
Compare commits
10 Commits
add-styles
...
4bf8edb692
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bf8edb692 | |||
| d68326f37a | |||
| a001455dad | |||
| 4632260961 | |||
| 55a7da67db | |||
| 7bec573ef9 | |||
| 55e60ff977 | |||
| 922efddf7a | |||
| 4a0147aa8a | |||
| cec76df1d1 |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -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
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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[
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.'
|
||||||
|
)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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 '❌'
|
||||||
|
|||||||
Reference in New Issue
Block a user