Skip to content

Commit

Permalink
Merge pull request #1775 from edsonarios/issue1652
Browse files Browse the repository at this point in the history
New feature to change theme in slcli like dark, light o maintain in default
  • Loading branch information
allmightyspiff authored Oct 28, 2022
2 parents a7c5b53 + d7238d5 commit 44773e6
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 59 deletions.
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

0 comments on commit 44773e6

Please sign in to comment.