mirror of
https://github.com/onyx-and-iris/q3rcon-tui.git
synced 2026-04-09 05:33:31 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fcc91b7e34 | |||
| 9b3ae629f3 | |||
| 74ed189ca5 | |||
| 6e50e0861f | |||
| ab4898dac3 | |||
| 086eeba916 | |||
| 2075e98c17 | |||
| 923faa67ec | |||
| 76483a24b9 | |||
| b3a3a4759a | |||
| fc6c2e99a5 | |||
| 97458682ea | |||
| 3181377c18 | |||
| 30ed9e9e6c | |||
| 8139ecf21f | |||
| f74ce3e17c | |||
| 82fb9b9fc4 | |||
| 8d11f60201 | |||
| c62d18289b |
17
README.md
17
README.md
@@ -40,6 +40,13 @@ Pass `--host`, `--port` and `--password` as flags:
|
||||
q3rcon-tui --host=localhost --port=28960 --password=rconpassword
|
||||
```
|
||||
|
||||
Additional flags:
|
||||
|
||||
- `--raw`: Boolean flag, if set the RichLog will print raw responses without rendering tables.
|
||||
- `--append`: Boolean flag, if set the RichLog output will append each response continuously.
|
||||
- `--version`: Print the version of the TUI.
|
||||
- `--help`: Print the help message.
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
Store and load from dotenv files located at:
|
||||
@@ -48,13 +55,19 @@ Store and load from dotenv files located at:
|
||||
|
||||
example .env:
|
||||
|
||||
```bash
|
||||
```env
|
||||
Q3RCON_TUI_HOST=localhost
|
||||
Q3RCON_TUI_PORT=28960
|
||||
Q3RCON_TUI_PASSWORD=rconpassword
|
||||
Q3RCON_TUI_REFRESH_OUTPUT=true
|
||||
Q3RCON_TUI_RAW=false
|
||||
Q3RCON_TUI_APPEND=false
|
||||
```
|
||||
|
||||
## Special Thanks
|
||||
|
||||
- [lapetus-11](https://github.com/Iapetus-11) for writing the [aio-q3-rcon](https://github.com/Iapetus-11/aio-q3-rcon) package.
|
||||
- The developers at [Textualize](https://github.com/Textualize) for writing the [textual](https://github.com/Textualize/textual) package.
|
||||
|
||||
## License
|
||||
|
||||
`q3rcon-tui` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
||||
|
||||
BIN
img/tui.png
BIN
img/tui.png
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2026-present onyx-and-iris <code@onyxandiris.online>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
__version__ = '0.3.0'
|
||||
__version__ = '0.5.1'
|
||||
|
||||
64
src/q3rcon_tui/configscreen.py
Normal file
64
src/q3rcon_tui/configscreen.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Input, Label, Static
|
||||
|
||||
from .settings import settings
|
||||
|
||||
|
||||
class ConfigScreen(ModalScreen[bool]):
|
||||
"""Modal dialog for configuring connection settings."""
|
||||
|
||||
def __init__(self, current_host: str, current_port: int, current_password: str):
|
||||
super().__init__()
|
||||
self.current_host = current_host
|
||||
self.current_port = current_port
|
||||
self.current_password = current_password
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id='config-dialog'):
|
||||
yield Static('Connection Configuration', id='config-title')
|
||||
with Vertical(id='config-form'):
|
||||
yield Label('Host:')
|
||||
yield Input(
|
||||
value=self.current_host, placeholder='localhost', id='host-input'
|
||||
)
|
||||
yield Label('Port:')
|
||||
yield Input(
|
||||
value=str(self.current_port), placeholder='28960', id='port-input'
|
||||
)
|
||||
yield Label('Password:')
|
||||
yield Input(
|
||||
value=self.current_password,
|
||||
placeholder='Enter password',
|
||||
password=True,
|
||||
id='password-input',
|
||||
)
|
||||
with Horizontal(id='config-buttons'):
|
||||
yield Button('Save', variant='success', id='config-save')
|
||||
yield Button('Cancel', variant='error', id='config-cancel')
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == 'config-save':
|
||||
try:
|
||||
new_host = (
|
||||
self.query_one('#host-input', Input).value.strip() or 'localhost'
|
||||
)
|
||||
new_port = int(self.query_one('#port-input', Input).value or '28960')
|
||||
new_password = self.query_one('#password-input', Input).value
|
||||
|
||||
if new_port < 1 or new_port > 65535:
|
||||
raise ValueError('Port must be between 1 and 65535')
|
||||
|
||||
if len(new_password) < 8:
|
||||
raise ValueError('Password must be at least 8 characters long')
|
||||
|
||||
settings.host = new_host
|
||||
settings.port = new_port
|
||||
settings.password = new_password
|
||||
|
||||
self.dismiss(True)
|
||||
except ValueError:
|
||||
self.app.bell()
|
||||
elif event.button.id == 'config-cancel':
|
||||
self.dismiss(False)
|
||||
@@ -44,43 +44,123 @@
|
||||
|
||||
Button {
|
||||
width: 100%;
|
||||
height: 3;
|
||||
height: 4;
|
||||
margin: 0 1;
|
||||
background: #5e81ac;
|
||||
color: #eceff4;
|
||||
border: none;
|
||||
text-style: bold;
|
||||
border: solid #5e81ac;
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
background: #81a1c1;
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
Button:focus {
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
Button.success {
|
||||
background: #a3be8c;
|
||||
border: solid #a3be8c;
|
||||
}
|
||||
|
||||
Button.success:hover {
|
||||
background: #8fbcbb;
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
Button.success:focus {
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
Button.warning {
|
||||
background: #ebcb8b;
|
||||
color: #2e3440;
|
||||
border: solid #ebcb8b;
|
||||
}
|
||||
|
||||
Button.warning:hover {
|
||||
background: #d08770;
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
Button.warning:focus {
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
Button.error {
|
||||
background: #bf616a;
|
||||
border: solid #bf616a;
|
||||
}
|
||||
|
||||
Button.error:hover {
|
||||
background: #d08770;
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
Button.error:focus {
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
#send {
|
||||
background: #a3be8c;
|
||||
border: solid #a3be8c;
|
||||
}
|
||||
|
||||
#send:focus {
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
#send:hover {
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
#config {
|
||||
background: #ebcb8b;
|
||||
color: #2e3440;
|
||||
border: solid #ebcb8b;
|
||||
}
|
||||
|
||||
#config:hover {
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
#config:focus {
|
||||
background: #88c0d0;
|
||||
color: #2e3440;
|
||||
text-style: bold;
|
||||
border: solid #88c0d0;
|
||||
}
|
||||
|
||||
#quit {
|
||||
@@ -90,25 +170,16 @@ Button.error:hover {
|
||||
|
||||
#quit:hover {
|
||||
background: #d08770;
|
||||
border: solid #ebcb8b;
|
||||
color: #eceff4;
|
||||
text-style: bold;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#send {
|
||||
background: #a3be8c;
|
||||
}
|
||||
|
||||
#config {
|
||||
background: #ebcb8b;
|
||||
color: #2e3440;
|
||||
}
|
||||
|
||||
#config:hover {
|
||||
#quit:focus {
|
||||
background: #d08770;
|
||||
color: #2e3440;
|
||||
}
|
||||
|
||||
#send:hover {
|
||||
background: #8fbcbb;
|
||||
color: #eceff4;
|
||||
text-style: bold;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Configuration Dialog Styles */
|
||||
67
src/q3rcon_tui/settings.py
Normal file
67
src/q3rcon_tui/settings.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Type
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import AfterValidator, BeforeValidator
|
||||
from pydantic_settings import BaseSettings, CliSettingsSource, SettingsConfigDict
|
||||
|
||||
from .__about__ import __version__ as version
|
||||
|
||||
|
||||
def version_callback(value: bool) -> bool | None:
|
||||
if value:
|
||||
print(f'q3rcon-tui version: {version}')
|
||||
raise SystemExit(0)
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_password(password: str) -> str | None:
|
||||
if len(password) < 8:
|
||||
raise ValueError('Password must be at least 8 characters long')
|
||||
return password
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
host: str = 'localhost'
|
||||
port: int = 28960
|
||||
password: Annotated[str, AfterValidator(is_valid_password)] = ''
|
||||
append: bool = False
|
||||
raw: bool = False
|
||||
version: Annotated[bool, BeforeValidator(version_callback)] = False
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=(
|
||||
'.env',
|
||||
Path.home() / '.config' / 'q3rcon-tui' / 'config.env',
|
||||
),
|
||||
env_file_encoding='utf-8',
|
||||
env_prefix='Q3RCON_TUI_',
|
||||
cli_prefix='',
|
||||
cli_parse_args=True,
|
||||
cli_implicit_flags=True,
|
||||
validate_assignment=False,
|
||||
frozen=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls: Type[BaseSettings],
|
||||
init_settings: ...,
|
||||
env_settings: ...,
|
||||
dotenv_settings: ...,
|
||||
file_secret_settings: ...,
|
||||
) -> tuple:
|
||||
return (
|
||||
CliSettingsSource(settings_cls),
|
||||
env_settings,
|
||||
dotenv_settings,
|
||||
init_settings,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
settings = Settings()
|
||||
except ValueError as e:
|
||||
logger.error(e)
|
||||
raise SystemExit(1)
|
||||
@@ -1,139 +1,19 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Type
|
||||
|
||||
from aioq3rcon import Client, RCONError
|
||||
from loguru import logger
|
||||
from pydantic import AfterValidator, BeforeValidator
|
||||
from pydantic_settings import BaseSettings, CliSettingsSource, SettingsConfigDict
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Grid, Horizontal, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Input, Label, RichLog, Static
|
||||
from textual.containers import Grid
|
||||
from textual.widgets import Button, Input, RichLog
|
||||
|
||||
from .__about__ import __version__ as version
|
||||
|
||||
|
||||
def is_valid_password(password: str) -> str | None:
|
||||
if len(password) < 8:
|
||||
raise ValueError('Password must be at least 8 characters long')
|
||||
return password
|
||||
|
||||
|
||||
def version_callback(value: bool) -> bool | None:
|
||||
if value:
|
||||
print(f'q3rcon-tui version: {version}')
|
||||
raise SystemExit(0)
|
||||
return False
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
host: str = 'localhost'
|
||||
port: int = 28960
|
||||
password: Annotated[str, AfterValidator(is_valid_password)] = ''
|
||||
refresh_output: bool = False
|
||||
version: Annotated[bool, BeforeValidator(version_callback)] = False
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=(
|
||||
'.env',
|
||||
Path.home() / '.config' / 'q3rcon-tui' / 'config.env',
|
||||
),
|
||||
env_file_encoding='utf-8',
|
||||
env_prefix='Q3RCON_TUI_',
|
||||
cli_prefix='',
|
||||
cli_parse_args=True,
|
||||
cli_implicit_flags=True,
|
||||
# Allow field assignment for runtime updates
|
||||
validate_assignment=False,
|
||||
frozen=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls: Type[BaseSettings],
|
||||
init_settings: ...,
|
||||
env_settings: ...,
|
||||
dotenv_settings: ...,
|
||||
file_secret_settings: ...,
|
||||
) -> tuple:
|
||||
return (
|
||||
CliSettingsSource(settings_cls),
|
||||
env_settings,
|
||||
dotenv_settings,
|
||||
init_settings,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
settings = Settings()
|
||||
except ValueError as e:
|
||||
logger.error(e)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
class ConfigScreen(ModalScreen[bool]):
|
||||
"""Modal dialog for configuring connection settings."""
|
||||
|
||||
def __init__(self, current_host: str, current_port: int, current_password: str):
|
||||
super().__init__()
|
||||
self.current_host = current_host
|
||||
self.current_port = current_port
|
||||
self.current_password = current_password
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id='config-dialog'):
|
||||
yield Static('Connection Configuration', id='config-title')
|
||||
with Vertical(id='config-form'):
|
||||
yield Label('Host:')
|
||||
yield Input(
|
||||
value=self.current_host, placeholder='localhost', id='host-input'
|
||||
)
|
||||
yield Label('Port:')
|
||||
yield Input(
|
||||
value=str(self.current_port), placeholder='28960', id='port-input'
|
||||
)
|
||||
yield Label('Password:')
|
||||
yield Input(
|
||||
value=self.current_password,
|
||||
placeholder='Enter password',
|
||||
password=True,
|
||||
id='password-input',
|
||||
)
|
||||
with Horizontal(id='config-buttons'):
|
||||
yield Button('Save', variant='success', id='config-save')
|
||||
yield Button('Cancel', variant='error', id='config-cancel')
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == 'config-save':
|
||||
try:
|
||||
new_host = (
|
||||
self.query_one('#host-input', Input).value.strip() or 'localhost'
|
||||
)
|
||||
new_port = int(self.query_one('#port-input', Input).value or '28960')
|
||||
new_password = self.query_one('#password-input', Input).value
|
||||
|
||||
if new_port < 1 or new_port > 65535:
|
||||
raise ValueError('Port must be between 1 and 65535')
|
||||
|
||||
if len(new_password) < 8:
|
||||
raise ValueError('Password must be at least 8 characters long')
|
||||
|
||||
settings.host = new_host
|
||||
settings.port = new_port
|
||||
settings.password = new_password
|
||||
|
||||
self.dismiss(True)
|
||||
except ValueError:
|
||||
self.app.bell()
|
||||
elif event.button.id == 'config-cancel':
|
||||
self.dismiss(False)
|
||||
from .configscreen import ConfigScreen
|
||||
from .settings import settings
|
||||
from .writable import Writable
|
||||
|
||||
|
||||
class RconApp(App):
|
||||
RE_COLOR_CODES = re.compile(r'\^[0-9]')
|
||||
CSS_PATH = 'rcon_tui.tcss'
|
||||
CSS_PATH = 'q3rcon_tui.tcss'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.writable = Writable()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
@@ -146,6 +26,10 @@ class RconApp(App):
|
||||
)
|
||||
|
||||
async def on_key(self, event) -> None:
|
||||
# prevent keypresses from ConfigScreen from triggering actions in RconApp
|
||||
if self.screen and isinstance(self.screen, ConfigScreen):
|
||||
return
|
||||
|
||||
match event.key:
|
||||
case 'enter' if self.query_one('#command', Input).has_focus:
|
||||
self.query_one('#send', Button).press()
|
||||
@@ -168,30 +52,34 @@ class RconApp(App):
|
||||
if event.button.id != 'send':
|
||||
return
|
||||
|
||||
if settings.refresh_output:
|
||||
if not settings.append:
|
||||
self.query_one('#response', RichLog).clear()
|
||||
|
||||
cmd = self.query_one('#command', Input).value.strip()
|
||||
if not cmd:
|
||||
self.app.bell()
|
||||
return
|
||||
|
||||
try:
|
||||
async with Client(
|
||||
settings.host, settings.port, settings.password
|
||||
) as client:
|
||||
response = await client.send_command(
|
||||
self.query_one('#command', Input).value
|
||||
)
|
||||
response = await client.send_command(cmd)
|
||||
self.query_one('#response', RichLog).write(
|
||||
self.remove_color_codes(response.removeprefix('print\n'))
|
||||
self.writable.parse(cmd, response)
|
||||
)
|
||||
except RCONError as e:
|
||||
except RCONError:
|
||||
output = (
|
||||
f'Unable to execute command {cmd}. ',
|
||||
'It may be due to a map change or a server restart.',
|
||||
'If the problem persists, please check your connection settings and ensure the server is running.',
|
||||
)
|
||||
self.query_one('#response', RichLog).write(
|
||||
f'{type(e).__name__}: Unable to connect to server: is the server running and are the host, port, and password correct? ({e})'
|
||||
self.writable.error('\n'.join(output))
|
||||
)
|
||||
|
||||
self.query_one('#command', Input).value = ''
|
||||
|
||||
@staticmethod
|
||||
def remove_color_codes(s: str) -> str:
|
||||
return RconApp.RE_COLOR_CODES.sub('', s)
|
||||
|
||||
|
||||
def main():
|
||||
app = RconApp()
|
||||
|
||||
106
src/q3rcon_tui/writable.py
Normal file
106
src/q3rcon_tui/writable.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import re
|
||||
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from .settings import settings
|
||||
|
||||
Renderable = Text | Table | str
|
||||
|
||||
|
||||
class Writable:
|
||||
RE_COLOR_CODES = re.compile(r'\^[0-9]')
|
||||
RE_PLAYER_FROM_STATUS = re.compile(
|
||||
r'^\s*(?P<slot>[0-9]+)\s+'
|
||||
r'(?P<score>[0-9-]+)\s+'
|
||||
r'(?P<ping>[0-9]+)\s+'
|
||||
r'(?P<guid>[0-9a-f]+)\s+'
|
||||
r'(?P<name>.*?)\s+'
|
||||
r'(?P<last>[0-9]+?)\s*'
|
||||
r'(?P<ip>(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}'
|
||||
r'(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])):?'
|
||||
r'(?P<port>-?[0-9]{1,5})\s*'
|
||||
r'(?P<qport>-?[0-9]{1,5})\s+'
|
||||
r'(?P<rate>[0-9]+)$',
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
RE_CVAR = re.compile(
|
||||
r'^["](?P<name>[a-z_]+)["]\sis[:]\s'
|
||||
r'["](?P<value>.*?)\^7["]\s'
|
||||
r'default[:]\s'
|
||||
r'["](?P<default>.*?)\^7["]\s'
|
||||
r'info[:]\s'
|
||||
r'["](?P<info>.*?)\^7["]$'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def remove_color_codes(s: str) -> str:
|
||||
return Writable.RE_COLOR_CODES.sub('', s)
|
||||
|
||||
def parse(self, cmd, response: str, style=None) -> Renderable:
|
||||
response = response.removeprefix('print\n')
|
||||
if settings.raw:
|
||||
return Text(response, style=style)
|
||||
|
||||
match cmd:
|
||||
case 'status':
|
||||
return self.status_table(response)
|
||||
case _:
|
||||
if m := self.RE_CVAR.match(response):
|
||||
return self.cvar_table(m)
|
||||
else:
|
||||
return Text(self.remove_color_codes(response), style=style)
|
||||
|
||||
def error(self, message: str) -> Text:
|
||||
return Text(message, style='#c73d4b')
|
||||
|
||||
def status_table(self, status_response: str) -> Table | str:
|
||||
table = Table(show_header=True, header_style='bold #88c0d0')
|
||||
table.add_column('Slot', justify='center')
|
||||
table.add_column('Score', justify='center')
|
||||
table.add_column('Ping', justify='center')
|
||||
table.add_column('GUID', justify='center')
|
||||
table.add_column('Name', justify='center')
|
||||
table.add_column('Last', justify='center')
|
||||
table.add_column('IP', justify='center')
|
||||
table.add_column('Port', justify='center')
|
||||
table.add_column('QPort', justify='center')
|
||||
table.add_column('Rate', justify='center')
|
||||
|
||||
for line in status_response.splitlines():
|
||||
match self.RE_PLAYER_FROM_STATUS.match(line):
|
||||
case None:
|
||||
continue
|
||||
case m:
|
||||
table.add_row(
|
||||
m.group('slot'),
|
||||
m.group('score'),
|
||||
m.group('ping'),
|
||||
m.group('guid'),
|
||||
self.remove_color_codes(m.group('name')),
|
||||
m.group('last'),
|
||||
m.group('ip'),
|
||||
m.group('port'),
|
||||
m.group('qport'),
|
||||
m.group('rate'),
|
||||
)
|
||||
|
||||
if len(table.rows) == 0:
|
||||
return 'No players connected.'
|
||||
return table
|
||||
|
||||
def cvar_table(self, m: re.Match) -> Table:
|
||||
table = Table(show_header=True, header_style='bold #88c0d0')
|
||||
table.add_column('Name', justify='center')
|
||||
table.add_column('Value', justify='center')
|
||||
table.add_column('Default', justify='center')
|
||||
table.add_column('Info', justify='center')
|
||||
|
||||
table.add_row(
|
||||
m.group('name'),
|
||||
self.remove_color_codes(m.group('value')),
|
||||
self.remove_color_codes(m.group('default')),
|
||||
self.remove_color_codes(m.group('info')),
|
||||
)
|
||||
|
||||
return table
|
||||
Reference in New Issue
Block a user