7 Commits

Author SHA1 Message Date
4cd420dc44 fix workflow dir name 2025-06-27 12:13:05 +01:00
06c9fa236d add release workflow 2025-06-27 12:09:05 +01:00
6490b5aa54 move style section and add imgs 2025-06-27 12:05:53 +01:00
f7345155f1 disable --help wrapping, realign the text
add --help shortname -h

patch bump
2025-06-27 12:05:25 +01:00
1c2d1abb2a add --style validation
add Disabled class to style registry

patch bump
2025-06-22 12:36:29 +01:00
fe3a975ba3 leave it up to rich... if no style is set we still get colour on stderr, but this can be overriden with NO_COLOR manually
patch bump
2025-06-22 11:33:52 +01:00
d8622ab037 set default values for no_colour style to 'none'.
This fixes rich markup error

ensure errors are written without colour if NO_COLOR is set
2025-06-22 10:49:05 +01:00
14 changed files with 143 additions and 80 deletions

26
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Release
on:
release:
types: [published]
push:
tags:
- 'v*.*.*'
jobs:
pypi-publish:
name: upload release to PyPI
runs-on: ubuntu-latest
environment: pypi
permissions:
# This permission is needed for private repositories.
contents: read
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pdm-project/setup-pdm@v4
- name: Publish package distributions to PyPI
run: pdm publish

View File

@@ -14,6 +14,7 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
- [Installation](#installation) - [Installation](#installation)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Style](#style)
- [Commands](#commands) - [Commands](#commands)
- [License](#license) - [License](#license)
@@ -68,6 +69,30 @@ Flags can be used to override environment variables.
[sl-desktop]: https://streamlabs.com/streamlabs-live-streaming-software?srsltid=AfmBOopnswGBgEyvVSi2DIc_vsGovKn2HQZyLw1Cg6LEo51OJhONXnAX [sl-desktop]: https://streamlabs.com/streamlabs-live-streaming-software?srsltid=AfmBOopnswGBgEyvVSi2DIc_vsGovKn2HQZyLw1Cg6LEo51OJhONXnAX
## Style
Styling is opt-in, by default you will get a colourless output:
![colourless](./img/colourless.png)
You may enable styling with the --style/-s flag:
```console
slobs-cli --style="yellow" audio list
```
Available styles: _red, magenta, purple, blue, cyan, green, yellow, orange, white, grey, navy, black_
![coloured](./img/coloured-border.png)
Optionally you may disable the border colouring with the --no-border flag:
![coloured-no-border](./img/coloured-no-border.png)
```console
slobs-cli --style="yellow' --no-border audio list
```
## Commands ## Commands
#### Scene #### Scene
@@ -293,38 +318,10 @@ slobs-cli scenecollection load "ExistingCollection"
slobs-cli scenecollection rename "ExistingCollection" "NewName" slobs-cli scenecollection rename "ExistingCollection" "NewName"
``` ```
## Style
By default styling is disabled but you may enable it with:
- --style/-s: Style used in output.
- SLOBS_STYLE
- --no-border/-b: Disable table border styling in output.
- SLOBS_STYLE_NO_BORDER
Available styles:
- red
- magenta
- purple
- blue
- cyan
- green
- yellow
- orange
- white
- grey
- navy
- black
```console
slobs-cli --style=cyan --no-border scene list
```
## Special Thanks ## Special Thanks
- [Julian-0](https://github.com/Julian-O) For writing the [PySLOBS wrapper](https://github.com/Julian-O/PySLOBS) on which this CLI depends. - [Julian-0](https://github.com/Julian-O) For writing the [PySLOBS wrapper](https://github.com/Julian-O/PySLOBS) on which this CLI depends.
## License ## License
`slobs-cli` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. `slobs-cli` is distributed under the terms of the [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) license.

BIN
img/coloured-border.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
img/coloured-no-border.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
img/colourless.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

38
pdm.lock generated
View File

@@ -281,29 +281,29 @@ files = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.12.0" version = "0.12.1"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust." summary = "An extremely fast Python linter and code formatter, written in Rust."
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848"}, {file = "ruff-0.12.1-py3-none-linux_armv6l.whl", hash = "sha256:6013a46d865111e2edb71ad692fbb8262e6c172587a57c0669332a449384a36b"},
{file = "ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6"}, {file = "ruff-0.12.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b3f75a19e03a4b0757d1412edb7f27cffb0c700365e9d6b60bc1b68d35bc89e0"},
{file = "ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0"}, {file = "ruff-0.12.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a256522893cb7e92bb1e1153283927f842dea2e48619c803243dccc8437b8be"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48"}, {file = "ruff-0.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:069052605fe74c765a5b4272eb89880e0ff7a31e6c0dbf8767203c1fbd31c7ff"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807"}, {file = "ruff-0.12.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a684f125a4fec2d5a6501a466be3841113ba6847827be4573fddf8308b83477d"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82"}, {file = "ruff-0.12.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdecdef753bf1e95797593007569d8e1697a54fca843d78f6862f7dc279e23bd"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c"}, {file = "ruff-0.12.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70d52a058c0e7b88b602f575d23596e89bd7d8196437a4148381a3f73fcd5010"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165"}, {file = "ruff-0.12.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84d0a69d1e8d716dfeab22d8d5e7c786b73f2106429a933cee51d7b09f861d4e"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2"}, {file = "ruff-0.12.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cc32e863adcf9e71690248607ccdf25252eeeab5193768e6873b901fd441fed"},
{file = "ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4"}, {file = "ruff-0.12.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd49a4619f90d5afc65cf42e07b6ae98bb454fd5029d03b306bd9e2273d44cc"},
{file = "ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514"}, {file = "ruff-0.12.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ed5af6aaaea20710e77698e2055b9ff9b3494891e1b24d26c07055459bb717e9"},
{file = "ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88"}, {file = "ruff-0.12.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:801d626de15e6bf988fbe7ce59b303a914ff9c616d5866f8c79eb5012720ae13"},
{file = "ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51"}, {file = "ruff-0.12.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2be9d32a147f98a1972c1e4df9a6956d612ca5f5578536814372113d09a27a6c"},
{file = "ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a"}, {file = "ruff-0.12.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:49b7ce354eed2a322fbaea80168c902de9504e6e174fd501e9447cad0232f9e6"},
{file = "ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb"}, {file = "ruff-0.12.1-py3-none-win32.whl", hash = "sha256:d973fa626d4c8267848755bd0414211a456e99e125dcab147f24daa9e991a245"},
{file = "ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0"}, {file = "ruff-0.12.1-py3-none-win_amd64.whl", hash = "sha256:9e1123b1c033f77bd2590e4c1fe7e8ea72ef990a85d2484351d408224d603013"},
{file = "ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b"}, {file = "ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc"},
{file = "ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c"}, {file = "ruff-0.12.1.tar.gz", hash = "sha256:806bbc17f1104fd57451a98a58df35388ee3ab422e029e8f5cf30aa4af2c138c"},
] ]
[[package]] [[package]]

View File

@@ -1,3 +1,3 @@
"""module for package metadata.""" """module for package metadata."""
__version__ = '0.11.0' __version__ = '0.11.4'

View File

@@ -47,8 +47,8 @@ async def list(ctx: click.Context, id: bool = False):
('Audio Source Name', 'left'), ('Audio Source Name', 'left'),
('Muted', 'center'), ('Muted', 'center'),
] ]
for col_name, col_justify in columns: for heading, justify in columns:
table.add_column(Text(col_name, justify='center'), justify=col_justify) table.add_column(Text(heading, justify='center'), justify=justify)
for source in sources: for source in sources:
model = await source.get_model() model = await source.get_model()

View File

@@ -8,7 +8,21 @@ from . import styles
from .__about__ import __version__ as version from .__about__ import __version__ as version
@click.group() def validate_style(ctx: click.Context, param: click.Parameter, value: str) -> str:
"""Validate the style option."""
if value not in styles.registry:
raise click.BadParameter(
f"Invalid style '{value}'. Available styles: {', '.join(styles.registry.keys())}"
)
return value
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
@click.group(
context_settings=CONTEXT_SETTINGS,
)
@click.option( @click.option(
'-d', '-d',
'--domain', '--domain',
@@ -16,7 +30,7 @@ from .__about__ import __version__ as version
envvar='SLOBS_DOMAIN', envvar='SLOBS_DOMAIN',
show_default=True, show_default=True,
show_envvar=True, show_envvar=True,
help='The domain of the SLOBS server.', help='\b\nStreamlabs Desktop WebSocket domain or IP address.\t',
) )
@click.option( @click.option(
'-p', '-p',
@@ -25,7 +39,7 @@ from .__about__ import __version__ as version
envvar='SLOBS_PORT', envvar='SLOBS_PORT',
show_default=True, show_default=True,
show_envvar=True, show_envvar=True,
help='The port of the SLOBS server.', help='\b\nStreamlabs Desktop WebSocket port.\t\t\t',
) )
@click.option( @click.option(
'-t', '-t',
@@ -33,7 +47,7 @@ from .__about__ import __version__ as version
envvar='SLOBS_TOKEN', envvar='SLOBS_TOKEN',
show_envvar=True, show_envvar=True,
required=True, required=True,
help='The token for the SLOBS server.', help='\b\nStreamlabs Desktop WebSocket authentication token.\t',
) )
@click.option( @click.option(
'-s', '-s',
@@ -42,7 +56,8 @@ from .__about__ import __version__ as version
envvar='SLOBS_STYLE', envvar='SLOBS_STYLE',
show_default=True, show_default=True,
show_envvar=True, show_envvar=True,
help='The style to use for output.', help='\b\nThe style to use for output.\t\t\t\t',
callback=validate_style,
) )
@click.option( @click.option(
'-b', '-b',
@@ -52,7 +67,7 @@ from .__about__ import __version__ as version
envvar='SLOBS_STYLE_NO_BORDER', envvar='SLOBS_STYLE_NO_BORDER',
show_default=True, show_default=True,
show_envvar=True, show_envvar=True,
help='Disable borders in the output.', help='\b\nDisable borders in the output.\t\t\t\t',
) )
@click.version_option( @click.version_option(
version, '-v', '--version', message='%(prog)s version: %(version)s' version, '-v', '--version', message='%(prog)s version: %(version)s'

View File

@@ -9,13 +9,9 @@ err = Console(stderr=True, style='bold red')
def highlight(ctx: click.Context, text: str) -> str: def highlight(ctx: click.Context, text: str) -> str:
"""Highlight text for console output.""" """Highlight text for console output."""
if ctx.obj['style'].name == 'no_colour':
return text
return f'[{ctx.obj["style"].highlight}]{text}[/{ctx.obj["style"].highlight}]' return f'[{ctx.obj["style"].highlight}]{text}[/{ctx.obj["style"].highlight}]'
def warning(ctx: click.Context, text: str) -> str: def warning(ctx: click.Context, text: str) -> str:
"""Format warning text for console output.""" """Format warning text for console output."""
if ctx.obj['style'].name == 'no_colour': return f'[{ctx.obj["style"].warning}]{text}[/{ctx.obj["style"].warning}]'
return text
return f'[magenta]{text}[/magenta]'

View File

@@ -52,8 +52,8 @@ async def list(ctx: click.Context, id: bool = False):
('Active', 'center'), ('Active', 'center'),
] ]
for col_name, col_justify in columns: for heading, justify in columns:
table.add_column(Text(col_name, justify='center'), justify=col_justify) table.add_column(Text(heading, justify='center'), justify=justify)
for scene in scenes: for scene in scenes:
to_append = [Text(scene.name, style=style.cell)] to_append = [Text(scene.name, style=style.cell)]

View File

@@ -52,8 +52,8 @@ async def list(ctx: click.Context, id: bool):
('Active', 'center'), ('Active', 'center'),
] ]
for col_name, col_justify in columns: for heading, justify in columns:
table.add_column(Text(col_name, justify='center'), justify=col_justify) table.add_column(Text(heading, justify='center'), justify=justify)
for collection in collections: for collection in collections:
to_append = [Text(collection.name, style=style.cell)] to_append = [Text(collection.name, style=style.cell)]

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,12 @@ def register_style(cls):
class Style: class Style:
"""Base class for styles.""" """Base class for styles."""
name: str = 'no_colour' name: str
border: str | None = None border: str
header: str | None = None header: str
cell: str | None = None cell: str
highlight: str | None = None highlight: str
warning: str
no_border: bool = False no_border: bool = False
def __post_init__(self): def __post_init__(self):
@@ -33,6 +34,19 @@ class Style:
self.border = None self.border = None
@register_style
@dataclass
class Disabled(Style):
"""Disabled style."""
name: str = 'disabled'
header: str = ''
border: str = 'none'
cell: str = 'none'
highlight: str = 'none'
warning: str = 'none'
@register_style @register_style
@dataclass @dataclass
class Red(Style): class Red(Style):
@@ -43,6 +57,7 @@ class Red(Style):
border: str = 'dark_red' border: str = 'dark_red'
cell: str = 'red' cell: str = 'red'
highlight: str = 'red3' highlight: str = 'red3'
warning: str = 'magenta'
@register_style @register_style
@@ -55,6 +70,7 @@ class Magenta(Style):
border: str = 'dark_magenta' border: str = 'dark_magenta'
cell: str = 'magenta' cell: str = 'magenta'
highlight: str = 'magenta3' highlight: str = 'magenta3'
warning: str = 'magenta'
@register_style @register_style
@@ -67,6 +83,7 @@ class Purple(Style):
border: str = 'purple' border: str = 'purple'
cell: str = 'medium_orchid' cell: str = 'medium_orchid'
highlight: str = 'medium_orchid' highlight: str = 'medium_orchid'
warning: str = 'magenta'
@register_style @register_style
@@ -79,6 +96,7 @@ class Blue(Style):
border: str = 'dark_blue' border: str = 'dark_blue'
cell: str = 'blue' cell: str = 'blue'
highlight: str = 'blue3' highlight: str = 'blue3'
warning: str = 'magenta'
@register_style @register_style
@@ -91,6 +109,7 @@ class Cyan(Style):
border: str = 'dark_cyan' border: str = 'dark_cyan'
cell: str = 'cyan' cell: str = 'cyan'
highlight: str = 'cyan3' highlight: str = 'cyan3'
warning: str = 'magenta'
@register_style @register_style
@@ -103,6 +122,7 @@ class Green(Style):
border: str = 'dark_green' border: str = 'dark_green'
cell: str = 'green' cell: str = 'green'
highlight: str = 'green3' highlight: str = 'green3'
warning: str = 'magenta'
@register_style @register_style
@@ -115,6 +135,7 @@ class Yellow(Style):
border: str = 'yellow3' border: str = 'yellow3'
cell: str = 'wheat1' cell: str = 'wheat1'
highlight: str = 'yellow3' highlight: str = 'yellow3'
warning: str = 'magenta'
@register_style @register_style
@@ -127,6 +148,7 @@ class Orange(Style):
border: str = 'dark_orange' border: str = 'dark_orange'
cell: str = 'orange' cell: str = 'orange'
highlight: str = 'orange3' highlight: str = 'orange3'
warning: str = 'magenta'
@register_style @register_style
@@ -139,6 +161,7 @@ class White(Style):
border: str = 'white' border: str = 'white'
cell: str = 'white' cell: str = 'white'
highlight: str = 'white' highlight: str = 'white'
warning: str = 'magenta'
@register_style @register_style
@@ -151,6 +174,7 @@ class Grey(Style):
border: str = 'grey50' border: str = 'grey50'
cell: str = 'grey70' cell: str = 'grey70'
highlight: str = 'grey90' highlight: str = 'grey90'
warning: str = 'magenta'
@register_style @register_style
@@ -163,6 +187,7 @@ class Navy(Style):
border: str = 'deep_sky_blue4' border: str = 'deep_sky_blue4'
cell: str = 'light_sky_blue3' cell: str = 'light_sky_blue3'
highlight: str = 'light_sky_blue3' highlight: str = 'light_sky_blue3'
warning: str = 'magenta'
@register_style @register_style
@@ -175,12 +200,12 @@ class Black(Style):
border: str = 'black' border: str = 'black'
cell: str = 'grey30' cell: str = 'grey30'
highlight: str = 'grey30' highlight: str = 'grey30'
warning: str = 'magenta'
def request_style_obj(style_name: str, no_border: bool) -> Style: def request_style_obj(style_name: str, no_border: bool) -> Style:
"""Request a style object by name.""" """Request a style object by name."""
key = style_name.lower() if style_name == 'disabled':
if key not in _registry: os.environ['NO_COLOR'] = '1'
os.environ['NO_COLOR'] = '1' # Disable colour output
return Style(no_border=no_border) return registry[style_name.lower()](no_border=no_border)
return _registry[key](no_border=no_border)

View File

@@ -10,6 +10,10 @@ def check_mark(ctx: click.Context, value: bool, empty_if_false: bool = False) ->
if empty_if_false and not value: if empty_if_false and not value:
return '' return ''
if os.getenv('NO_COLOR', '') != '' or ctx.obj['style'].name == 'no_colour': # 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', '') != '':
return '' if value else '' return '' if value else ''
return '' if value else '' return '' if value else ''