Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New feature to change theme in slcli like dark, light o maintain in default #1775

Merged
merged 4 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 35 additions & 39 deletions SoftLayer/CLI/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@
import click

from rich import box
from rich.console import Console
from rich.highlighter import RegexHighlighter
from rich.table import Table
from rich.text import Text
from rich.theme import Theme

from SoftLayer.CLI import environment

Expand All @@ -26,68 +24,58 @@ class OptionHighlighter(RegexHighlighter):
r"(?P<switch>^\-\w)", # single options like -v
r"(?P<option>\-\-[\w\-]+)", # long options like --verbose
r"(?P<default_option>\[[^\]]+\])", # anything between [], usually default options

]


# Colors defined in https://rich.readthedocs.io/en/latest/_modules/rich/color.html#ColorType
SLCLI_THEME = Theme(
{
"option": "bold cyan",
"switch": "bold green",
"default_option": "light_pink1",
"option_keyword": "bold cyan",
"args_keyword": "bold green"
}
)


class CommandLoader(click.MultiCommand):
"""Loads module for click."""

def __init__(self, *path, **attrs):
click.MultiCommand.__init__(self, **attrs)
self.path = path

self.highlighter = OptionHighlighter()
self.console = Console(
theme=SLCLI_THEME
)
self.env = None
self.console = None

def ensure_env(self, ctx):
"""ensures self.env is set"""
if self.env is None:
self.env = ctx.ensure_object(environment.Environment)
self.env.load()
if self.console is None:
self.console = self.env.console

def list_commands(self, ctx):
"""List all sub-commands."""
env = ctx.ensure_object(environment.Environment)
env.load()

return sorted(env.list_commands(*self.path))
self.ensure_env(ctx)
return sorted(self.env.list_commands(*self.path))

def get_command(self, ctx, cmd_name):
"""Get command for click."""
env = ctx.ensure_object(environment.Environment)
env.load()

self.ensure_env(ctx)
# Do alias lookup (only available for root commands)
if len(self.path) == 0:
cmd_name = env.resolve_alias(cmd_name)
cmd_name = self.env.resolve_alias(cmd_name)

new_path = list(self.path)
new_path.append(cmd_name)
module = env.get_command(*new_path)
module = self.env.get_command(*new_path)
if isinstance(module, types.ModuleType):
return CommandLoader(*new_path, help=module.__doc__ or '')
else:
return module

def format_usage(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None:
"""Formats and colorizes the usage information."""
self.ensure_env(ctx)
pieces = self.collect_usage_pieces(ctx)
for index, piece in enumerate(pieces):
if piece == "[OPTIONS]":
pieces[index] = "[bold cyan][OPTIONS][/bold cyan]"
pieces[index] = "[options][OPTIONS][/]"
elif piece == "COMMAND [ARGS]...":
pieces[index] = "[orange1]COMMAND[/orange1] [bold cyan][ARGS][/bold cyan] ..."
pieces[index] = "[command]COMMAND[/] [args][ARGS][/] ..."

self.console.print(f"Usage: [bold red]{ctx.command_path}[/bold red] {' '.join(pieces)}")
self.console.print(f"Usage: [path]{ctx.command_path}[/] {' '.join(pieces)}")

def format_help_text(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None:
"""Writes the help text"""
Expand Down Expand Up @@ -153,9 +141,9 @@ def format_commands(self, ctx, formatter):
if len(commands):
for subcommand, cmd in commands:
help_text = cmd.get_short_help_str(120)
command_style = Text(f" {subcommand}", style="orange1")
command_style = Text(f" {subcommand}", style="sub_command")
command_table.add_row(command_style, help_text)
self.console.print("\n[bold orange1]Commands:[/]")
self.console.print("\n[name_sub_command]Commands:[/]")
self.console.print(command_table)


Expand All @@ -165,20 +153,28 @@ class SLCommand(click.Command):
def __init__(self, **attrs):
click.Command.__init__(self, **attrs)
self.highlighter = OptionHighlighter()
self.console = Console(
theme=SLCLI_THEME
)
self.env = None
self.console = None

def ensure_env(self, ctx):
"""ensures self.env is set"""
if self.env is None:
self.env = ctx.ensure_object(environment.Environment)
self.env.load()
if self.console is None:
self.console = self.env.console

def format_usage(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None:
"""Formats and colorizes the usage information."""
self.ensure_env(ctx)
pieces = self.collect_usage_pieces(ctx)
for index, piece in enumerate(pieces):
if piece == "[OPTIONS]":
pieces[index] = "[bold cyan][OPTIONS][/bold cyan]"
pieces[index] = "[options][OPTIONS][/]"
elif piece == "COMMAND [ARGS]...":
pieces[index] = "[orange1]COMMAND[/orange1] [bold cyan][ARGS][/bold cyan] ..."
pieces[index] = "[command]COMMAND[/] [args][ARGS][/] ..."

self.console.print(f"Usage: [bold red]{ctx.command_path}[/bold red] {' '.join(pieces)}")
self.console.print(f"Usage: [path]{ctx.command_path}[/] {' '.join(pieces)}")

def format_help_text(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None:
"""Writes the help text"""
Expand Down
4 changes: 3 additions & 1 deletion SoftLayer/CLI/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def _resolve_transport(transport):
return _resolve_transport(nested_transport)


def get_settings_from_client(client):
def get_settings_from_client(client, theme):
"""Pull out settings from a SoftLayer.BaseClient instance.

:param client: SoftLayer.BaseClient instance
Expand All @@ -23,6 +23,7 @@ def get_settings_from_client(client):
'api_key': '',
'timeout': '',
'endpoint_url': '',
'theme': theme
}
try:
settings['username'] = client.auth.username
Expand All @@ -49,4 +50,5 @@ def config_table(settings):
table.add_row(['API Key', settings['api_key'] or 'not set'])
table.add_row(['Endpoint URL', settings['endpoint_url'] or 'not set'])
table.add_row(['Timeout', settings['timeout'] or 'not set'])
table.add_row(['Theme', settings['theme'] or 'not set'])
return table
9 changes: 7 additions & 2 deletions SoftLayer/CLI/config/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def cli(env, auth):
api_key = None

timeout = 0
defaults = config.get_settings_from_client(env.client)
defaults = config.get_settings_from_client(env.client, env.theme)
endpoint_url = get_endpoint_url(env, defaults.get('endpoint_url', 'public'))
# Get ths username and API key
if auth == 'ibmid':
Expand All @@ -92,6 +92,9 @@ def cli(env, auth):
# Ask for timeout, convert to float, then to int
timeout = int(float(env.input('Timeout', default=defaults['timeout'] or 0)))

# Ask theme for console
theme = env.input('Theme (dark/light)', default=defaults['theme'] or 'dark')

path = '~/.softlayer'
if env.config_file:
path = env.config_file
Expand All @@ -100,7 +103,8 @@ def cli(env, auth):
env.out(env.fmt(config.config_table({'username': username,
'api_key': api_key,
'endpoint_url': endpoint_url,
'timeout': timeout})))
'timeout': timeout,
'theme': theme})))

if not formatting.confirm('Are you sure you want to write settings to "%s"?' % config_path, default=True):
raise exceptions.CLIAbort('Aborted.')
Expand All @@ -118,6 +122,7 @@ def cli(env, auth):
parsed_config.set('softlayer', 'api_key', api_key)
parsed_config.set('softlayer', 'endpoint_url', endpoint_url)
parsed_config.set('softlayer', 'timeout', timeout)
parsed_config.set('softlayer', 'theme', theme)

config_fd = os.fdopen(os.open(config_path, (os.O_WRONLY | os.O_CREAT | os.O_TRUNC), 0o600), 'w')
try:
Expand Down
2 changes: 1 addition & 1 deletion SoftLayer/CLI/config/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
def cli(env):
"""Show current configuration."""

settings = config.get_settings_from_client(env.client)
settings = config.get_settings_from_client(client=env.client, theme=env.theme)
env.fout(config.config_table(settings))
1 change: 1 addition & 0 deletions SoftLayer/CLI/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def cli(env,
env.skip_confirmations = really
env.config_file = config
env.format = format
env.set_env_theme(config_file=config)
env.ensure_client(config_file=config, is_demo=demo, proxy=proxy)
env.vars['_start'] = time.time()
logger = logging.getLogger()
Expand Down
28 changes: 26 additions & 2 deletions SoftLayer/CLI/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

:license: MIT, see LICENSE for more details.
"""
import configparser
import os

import importlib
from json.decoder import JSONDecodeError

Expand All @@ -16,6 +19,7 @@
import SoftLayer
from SoftLayer.CLI import formatting
from SoftLayer.CLI import routes
from SoftLayer import utils

# pylint: disable=too-many-instance-attributes, invalid-name

Expand All @@ -35,7 +39,8 @@ def __init__(self):
self.vars = {}

self.client = None
self.console = Console()
self.theme = self.set_env_theme()
self.console = utils.console_color_themes(self.theme)
self.err_console = Console(stderr=True)
self.format = 'table'
self.skip_confirmations = False
Expand Down Expand Up @@ -76,7 +81,7 @@ def fmt(self, output, fmt=None):
"""Format output based on current the environment format."""
if fmt is None:
fmt = self.format
return formatting.format_output(output, fmt)
return formatting.format_output(output, fmt, self.theme)

def format_output_is_json(self):
"""Return True if format output is json or jsonraw"""
Expand Down Expand Up @@ -208,6 +213,25 @@ def ensure_client(self, config_file=None, is_demo=False, proxy=None):
)
self.client = client

def set_env_theme(self, config_file=None):
"""Get theme to color console and set in env"""
theme = os.environ.get('SL_THEME')
if theme:
return theme
else:
config_files = ['/etc/softlayer.conf', '~/.softlayer']
path_os = os.getenv('HOME')
if path_os:
config_files.append(path_os + '\\AppData\\Roaming\\softlayer')
if config_file:
config_files.append(config_file)
config = configparser.RawConfigParser({'theme': 'dark'})
config.read(config_files)
if config.has_section('softlayer'):
self.theme = config.get('softlayer', 'theme')
return config.get('softlayer', 'theme')
return 'dark'


class ModuleLoader(object):
"""Module loader that acts a little like an EntryPoint object."""
Expand Down
15 changes: 8 additions & 7 deletions SoftLayer/CLI/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
FALSE_VALUES = ['0', 'false', 'FALSE', 'no', 'False']


def format_output(data, fmt='table'): # pylint: disable=R0911,R0912
def format_output(data, fmt='table', theme=None): # pylint: disable=R0911,R0912
"""Given some data, will format it for console output.

:param data: One of: String, Table, FormattedItem, List, Tuple, SequentialOutput
Expand All @@ -40,7 +40,7 @@ def format_output(data, fmt='table'): # pylint: disable=R0911,R0912

# responds to .prettytable()
if hasattr(data, 'prettytable') and fmt in ('table', 'raw'):
return format_prettytable(data, fmt)
return format_prettytable(data, fmt, theme)

# responds to .to_python()
if hasattr(data, 'to_python'):
Expand Down Expand Up @@ -73,7 +73,7 @@ def format_output(data, fmt='table'): # pylint: disable=R0911,R0912
return str(data)


def format_prettytable(table, fmt='table'):
def format_prettytable(table, fmt='table', theme=None):
"""Converts SoftLayer.CLI.formatting.Table instance to a prettytable."""
for i, row in enumerate(table.rows):
for j, item in enumerate(row):
Expand All @@ -83,7 +83,7 @@ def format_prettytable(table, fmt='table'):
table.rows[i][j] = format_output(item)
else:
table.rows[i][j] = str(item)
ptable = table.prettytable(fmt)
ptable = table.prettytable(fmt, theme)
return ptable


Expand Down Expand Up @@ -267,12 +267,13 @@ def to_python(self):
items.append(dict(zip(self.columns, formatted_row)))
return items

def prettytable(self, fmt='table'):
def prettytable(self, fmt='table', theme=None):
"""Returns a RICH table instance."""
box_style = box.SQUARE
if fmt == 'raw':
box_style = None
table = rTable(title=self.title, box=box_style, header_style="bright_cyan")
color_table = utils.table_color_theme(theme)
table = rTable(title=self.title, box=box_style, header_style=color_table['header'])
if self.sortby:
try:
# https://docs.python.org/3/howto/sorting.html#key-functions
Expand Down Expand Up @@ -300,7 +301,7 @@ def prettytable(self, fmt='table'):
justify = 'left'
# Special coloring for some columns
if col in ('id', 'Id', 'ID'):
style = "pale_violet_red1"
style = color_table['id_columns']
table.add_column(col, justify=justify, style=style)

for row in self.rows:
Expand Down
Loading