Refactor palette (#1378)

* Refactor palette

* Modifiers / change static strings to colors

* Colors...

* Error-based tests

* Styling linting

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
This commit is contained in:
Batuhan Taskaya 2022-05-05 18:17:05 +03:00 committed by GitHub
parent 0f9fd76852
commit f7c1bb269e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 312 additions and 195 deletions

View File

@ -18,20 +18,28 @@ from .config import DEFAULT_CONFIG_DIR, Config, ConfigFileError
from .encoding import UTF8 from .encoding import UTF8
from .utils import repr_dict from .utils import repr_dict
from httpie.output.ui import rich_palette as palette from .output.ui.palette import GenericColor
if TYPE_CHECKING: if TYPE_CHECKING:
from rich.console import Console from rich.console import Console
class Levels(str, Enum): class LogLevel(str, Enum):
INFO = 'info'
WARNING = 'warning' WARNING = 'warning'
ERROR = 'error' ERROR = 'error'
DISPLAY_THRESHOLDS = { LOG_LEVEL_COLORS = {
Levels.WARNING: 2, LogLevel.INFO: GenericColor.PINK,
Levels.ERROR: float('inf'), # Never hide errors. LogLevel.WARNING: GenericColor.ORANGE,
LogLevel.ERROR: GenericColor.RED,
}
LOG_LEVEL_DISPLAY_THRESHOLDS = {
LogLevel.INFO: 1,
LogLevel.WARNING: 2,
LogLevel.ERROR: float('inf'), # Never hide errors.
} }
@ -159,16 +167,22 @@ class Environment:
self.stdout = original_stdout self.stdout = original_stdout
self.stderr = original_stderr self.stderr = original_stderr
def log_error(self, msg: str, level: Levels = Levels.ERROR) -> None: def log_error(self, msg: str, level: LogLevel = LogLevel.ERROR) -> None:
if self.stdout_isatty and self.quiet >= DISPLAY_THRESHOLDS[level]: if self.stdout_isatty and self.quiet >= LOG_LEVEL_DISPLAY_THRESHOLDS[level]:
stderr = self.stderr # Not directly /dev/null, since stderr might be mocked stderr = self.stderr # Not directly /dev/null, since stderr might be mocked
else: else:
stderr = self._orig_stderr stderr = self._orig_stderr
rich_console = self._make_rich_console(file=stderr, force_terminal=stderr.isatty())
stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n') rich_console.print(
f'\n{self.program_name}: {level}: {msg}\n\n',
style=LOG_LEVEL_COLORS[level],
markup=False,
highlight=False,
soft_wrap=True
)
def apply_warnings_filter(self) -> None: def apply_warnings_filter(self) -> None:
if self.quiet >= DISPLAY_THRESHOLDS[Levels.WARNING]: if self.quiet >= LOG_LEVEL_DISPLAY_THRESHOLDS[LogLevel.WARNING]:
warnings.simplefilter("ignore") warnings.simplefilter("ignore")
def _make_rich_console( def _make_rich_console(
@ -177,32 +191,17 @@ class Environment:
force_terminal: bool force_terminal: bool
) -> 'Console': ) -> 'Console':
from rich.console import Console from rich.console import Console
from rich.theme import Theme from httpie.output.ui.rich_palette import _make_rich_color_theme
from rich.style import Style
style = getattr(self.args, 'style', palette.AUTO_STYLE)
theme = {}
if style in palette.STYLE_SHADES:
shade = palette.STYLE_SHADES[style]
theme.update({
color: Style(
color=palette.get_color(
color,
shade,
palette=palette.RICH_THEME_PALETTE
),
bold=True
)
for color in palette.RICH_THEME_PALETTE
})
style = getattr(self.args, 'style', None)
theme = _make_rich_color_theme(style)
# Rich infers the rest of the knowledge (e.g encoding) # Rich infers the rest of the knowledge (e.g encoding)
# dynamically by looking at the file/stderr. # dynamically by looking at the file/stderr.
return Console( return Console(
file=file, file=file,
force_terminal=force_terminal, force_terminal=force_terminal,
no_color=(self.colors == 0), no_color=(self.colors == 0),
theme=Theme(theme) theme=theme
) )
# Rich recommends separating the actual console (stdout) from # Rich recommends separating the actual console (stdout) from

View File

@ -13,7 +13,7 @@ from . import __version__ as httpie_version
from .cli.constants import OUT_REQ_BODY from .cli.constants import OUT_REQ_BODY
from .cli.nested_json import HTTPieSyntaxError from .cli.nested_json import HTTPieSyntaxError
from .client import collect_messages from .client import collect_messages
from .context import Environment, Levels from .context import Environment, LogLevel
from .downloads import Downloader from .downloads import Downloader
from .models import ( from .models import (
RequestsMessageKind, RequestsMessageKind,
@ -223,7 +223,7 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
if args.check_status or downloader: if args.check_status or downloader:
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow) exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1): if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=Levels.WARNING) env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=LogLevel.WARNING)
write_message( write_message(
requests_message=message, requests_message=message,
env=env, env=env,

View File

@ -17,13 +17,15 @@ from pygments.util import ClassNotFound
from ..lexers.json import EnhancedJsonLexer from ..lexers.json import EnhancedJsonLexer
from ..lexers.metadata import MetadataLexer from ..lexers.metadata import MetadataLexer
from ..ui.palette import AUTO_STYLE, SHADE_NAMES, get_color from ..ui.palette import AUTO_STYLE, SHADE_TO_PIE_STYLE, PieColor, ColorString, get_color
from ...context import Environment from ...context import Environment
from ...plugins import FormatterPlugin from ...plugins import FormatterPlugin
DEFAULT_STYLE = AUTO_STYLE DEFAULT_STYLE = AUTO_STYLE
SOLARIZED_STYLE = 'solarized' # Bundled here SOLARIZED_STYLE = 'solarized' # Bundled here
PYGMENTS_BOLD = ColorString('bold')
PYGMENTS_ITALIC = ColorString('italic')
BUNDLED_STYLES = { BUNDLED_STYLES = {
SOLARIZED_STYLE, SOLARIZED_STYLE,
@ -253,11 +255,11 @@ class Solarized256Style(pygments.style.Style):
pygments.token.Comment.Preproc: GREEN, pygments.token.Comment.Preproc: GREEN,
pygments.token.Comment.Special: GREEN, pygments.token.Comment.Special: GREEN,
pygments.token.Generic.Deleted: CYAN, pygments.token.Generic.Deleted: CYAN,
pygments.token.Generic.Emph: 'italic', pygments.token.Generic.Emph: PYGMENTS_ITALIC,
pygments.token.Generic.Error: RED, pygments.token.Generic.Error: RED,
pygments.token.Generic.Heading: ORANGE, pygments.token.Generic.Heading: ORANGE,
pygments.token.Generic.Inserted: GREEN, pygments.token.Generic.Inserted: GREEN,
pygments.token.Generic.Strong: 'bold', pygments.token.Generic.Strong: PYGMENTS_BOLD,
pygments.token.Generic.Subheading: ORANGE, pygments.token.Generic.Subheading: ORANGE,
pygments.token.Token: BASE1, pygments.token.Token: BASE1,
pygments.token.Token.Other: ORANGE, pygments.token.Token.Other: ORANGE,
@ -266,86 +268,86 @@ class Solarized256Style(pygments.style.Style):
PIE_HEADER_STYLE = { PIE_HEADER_STYLE = {
# HTTP line / Headers / Etc. # HTTP line / Headers / Etc.
pygments.token.Name.Namespace: 'bold primary', pygments.token.Name.Namespace: PYGMENTS_BOLD | PieColor.PRIMARY,
pygments.token.Keyword.Reserved: 'bold grey', pygments.token.Keyword.Reserved: PYGMENTS_BOLD | PieColor.GREY,
pygments.token.Operator: 'bold grey', pygments.token.Operator: PYGMENTS_BOLD | PieColor.GREY,
pygments.token.Number: 'bold grey', pygments.token.Number: PYGMENTS_BOLD | PieColor.GREY,
pygments.token.Name.Function.Magic: 'bold green', pygments.token.Name.Function.Magic: PYGMENTS_BOLD | PieColor.GREEN,
pygments.token.Name.Exception: 'bold green', pygments.token.Name.Exception: PYGMENTS_BOLD | PieColor.GREEN,
pygments.token.Name.Attribute: 'blue', pygments.token.Name.Attribute: PieColor.BLUE,
pygments.token.String: 'primary', pygments.token.String: PieColor.PRIMARY,
# HTTP Methods # HTTP Methods
pygments.token.Name.Function: 'bold grey', pygments.token.Name.Function: PYGMENTS_BOLD | PieColor.GREY,
pygments.token.Name.Function.HTTP.GET: 'bold green', pygments.token.Name.Function.HTTP.GET: PYGMENTS_BOLD | PieColor.GREEN,
pygments.token.Name.Function.HTTP.HEAD: 'bold green', pygments.token.Name.Function.HTTP.HEAD: PYGMENTS_BOLD | PieColor.GREEN,
pygments.token.Name.Function.HTTP.POST: 'bold yellow', pygments.token.Name.Function.HTTP.POST: PYGMENTS_BOLD | PieColor.YELLOW,
pygments.token.Name.Function.HTTP.PUT: 'bold orange', pygments.token.Name.Function.HTTP.PUT: PYGMENTS_BOLD | PieColor.ORANGE,
pygments.token.Name.Function.HTTP.PATCH: 'bold orange', pygments.token.Name.Function.HTTP.PATCH: PYGMENTS_BOLD | PieColor.ORANGE,
pygments.token.Name.Function.HTTP.DELETE: 'bold red', pygments.token.Name.Function.HTTP.DELETE: PYGMENTS_BOLD | PieColor.RED,
# HTTP status codes # HTTP status codes
pygments.token.Number.HTTP.INFO: 'bold aqua', pygments.token.Number.HTTP.INFO: PYGMENTS_BOLD | PieColor.AQUA,
pygments.token.Number.HTTP.OK: 'bold green', pygments.token.Number.HTTP.OK: PYGMENTS_BOLD | PieColor.GREEN,
pygments.token.Number.HTTP.REDIRECT: 'bold yellow', pygments.token.Number.HTTP.REDIRECT: PYGMENTS_BOLD | PieColor.YELLOW,
pygments.token.Number.HTTP.CLIENT_ERR: 'bold orange', pygments.token.Number.HTTP.CLIENT_ERR: PYGMENTS_BOLD | PieColor.ORANGE,
pygments.token.Number.HTTP.SERVER_ERR: 'bold red', pygments.token.Number.HTTP.SERVER_ERR: PYGMENTS_BOLD | PieColor.RED,
# Metadata # Metadata
pygments.token.Name.Decorator: 'grey', pygments.token.Name.Decorator: PieColor.GREY,
pygments.token.Number.SPEED.FAST: 'bold green', pygments.token.Number.SPEED.FAST: PYGMENTS_BOLD | PieColor.GREEN,
pygments.token.Number.SPEED.AVG: 'bold yellow', pygments.token.Number.SPEED.AVG: PYGMENTS_BOLD | PieColor.YELLOW,
pygments.token.Number.SPEED.SLOW: 'bold orange', pygments.token.Number.SPEED.SLOW: PYGMENTS_BOLD | PieColor.ORANGE,
pygments.token.Number.SPEED.VERY_SLOW: 'bold red', pygments.token.Number.SPEED.VERY_SLOW: PYGMENTS_BOLD | PieColor.RED,
} }
PIE_BODY_STYLE = { PIE_BODY_STYLE = {
# {}[]: # {}[]:
pygments.token.Punctuation: 'grey', pygments.token.Punctuation: PieColor.GREY,
# Keys # Keys
pygments.token.Name.Tag: 'pink', pygments.token.Name.Tag: PieColor.PINK,
# Values # Values
pygments.token.Literal.String: 'green', pygments.token.Literal.String: PieColor.GREEN,
pygments.token.Literal.String.Double: 'green', pygments.token.Literal.String.Double: PieColor.GREEN,
pygments.token.Literal.Number: 'aqua', pygments.token.Literal.Number: PieColor.AQUA,
pygments.token.Keyword: 'orange', pygments.token.Keyword: PieColor.ORANGE,
# Other stuff # Other stuff
pygments.token.Text: 'primary', pygments.token.Text: PieColor.PRIMARY,
pygments.token.Name.Attribute: 'primary', pygments.token.Name.Attribute: PieColor.PRIMARY,
pygments.token.Name.Builtin: 'blue', pygments.token.Name.Builtin: PieColor.BLUE,
pygments.token.Name.Builtin.Pseudo: 'blue', pygments.token.Name.Builtin.Pseudo: PieColor.BLUE,
pygments.token.Name.Class: 'blue', pygments.token.Name.Class: PieColor.BLUE,
pygments.token.Name.Constant: 'orange', pygments.token.Name.Constant: PieColor.ORANGE,
pygments.token.Name.Decorator: 'blue', pygments.token.Name.Decorator: PieColor.BLUE,
pygments.token.Name.Entity: 'orange', pygments.token.Name.Entity: PieColor.ORANGE,
pygments.token.Name.Exception: 'yellow', pygments.token.Name.Exception: PieColor.YELLOW,
pygments.token.Name.Function: 'blue', pygments.token.Name.Function: PieColor.BLUE,
pygments.token.Name.Variable: 'blue', pygments.token.Name.Variable: PieColor.BLUE,
pygments.token.String: 'aqua', pygments.token.String: PieColor.AQUA,
pygments.token.String.Backtick: 'secondary', pygments.token.String.Backtick: PieColor.SECONDARY,
pygments.token.String.Char: 'aqua', pygments.token.String.Char: PieColor.AQUA,
pygments.token.String.Doc: 'aqua', pygments.token.String.Doc: PieColor.AQUA,
pygments.token.String.Escape: 'red', pygments.token.String.Escape: PieColor.RED,
pygments.token.String.Heredoc: 'aqua', pygments.token.String.Heredoc: PieColor.AQUA,
pygments.token.String.Regex: 'red', pygments.token.String.Regex: PieColor.RED,
pygments.token.Number: 'aqua', pygments.token.Number: PieColor.AQUA,
pygments.token.Operator: 'primary', pygments.token.Operator: PieColor.PRIMARY,
pygments.token.Operator.Word: 'green', pygments.token.Operator.Word: PieColor.GREEN,
pygments.token.Comment: 'secondary', pygments.token.Comment: PieColor.SECONDARY,
pygments.token.Comment.Preproc: 'green', pygments.token.Comment.Preproc: PieColor.GREEN,
pygments.token.Comment.Special: 'green', pygments.token.Comment.Special: PieColor.GREEN,
pygments.token.Generic.Deleted: 'aqua', pygments.token.Generic.Deleted: PieColor.AQUA,
pygments.token.Generic.Emph: 'italic', pygments.token.Generic.Emph: PYGMENTS_ITALIC,
pygments.token.Generic.Error: 'red', pygments.token.Generic.Error: PieColor.RED,
pygments.token.Generic.Heading: 'orange', pygments.token.Generic.Heading: PieColor.ORANGE,
pygments.token.Generic.Inserted: 'green', pygments.token.Generic.Inserted: PieColor.GREEN,
pygments.token.Generic.Strong: 'bold', pygments.token.Generic.Strong: PYGMENTS_BOLD,
pygments.token.Generic.Subheading: 'orange', pygments.token.Generic.Subheading: PieColor.ORANGE,
pygments.token.Token: 'primary', pygments.token.Token: PieColor.PRIMARY,
pygments.token.Token.Other: 'orange', pygments.token.Token.Other: PieColor.ORANGE,
} }
@ -369,7 +371,7 @@ def make_style(name, raw_styles, shade):
def make_styles(): def make_styles():
styles = {} styles = {}
for shade, name in SHADE_NAMES.items(): for shade, name in SHADE_TO_PIE_STYLE.items():
styles[name] = [ styles[name] = [
make_style(name, style_map, shade) make_style(name, style_map, shade)
for style_name, style_map in [ for style_name, style_map in [

View File

@ -18,7 +18,7 @@ def is_available(program: str) -> bool:
[MAN_COMMAND, program], [MAN_COMMAND, program],
shell=False, shell=False,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL,
) )
return process.returncode == 0 return process.returncode == 0
@ -27,7 +27,5 @@ def display_for(env: Environment, program: str) -> None:
"""Display the man page for the given command (http/https).""" """Display the man page for the given command (http/https)."""
subprocess.run( subprocess.run(
[MAN_COMMAND, program], [MAN_COMMAND, program], stdout=env.stdout, stderr=env.stderr
stdout=env.stdout,
stderr=env.stderr
) )

View File

@ -1,16 +1,97 @@
from typing import Dict, Optional from enum import Enum, auto
from typing import Optional
PYGMENTS_BRIGHT_BLACK = 'ansibrightblack'
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
STYLE_PIE = 'pie'
STYLE_PIE_DARK = 'pie-dark'
STYLE_PIE_LIGHT = 'pie-light'
class Styles(Enum):
PIE = auto()
ANSI = auto()
class PieStyle(str, Enum):
UNIVERSAL = 'pie'
DARK = 'pie-dark'
LIGHT = 'pie-light'
PIE_STYLE_TO_SHADE = {
PieStyle.DARK: '500',
PieStyle.UNIVERSAL: '600',
PieStyle.LIGHT: '700',
}
SHADE_TO_PIE_STYLE = {
shade: style for style, shade in PIE_STYLE_TO_SHADE.items()
}
class ColorString(str):
def __or__(self, other: str) -> 'ColorString':
"""Combine a style with a property.
E.g: PieColor.BLUE | BOLD | ITALIC
"""
return ColorString(self + ' ' + other)
class PieColor(ColorString, Enum):
"""Styles that are available only in Pie themes."""
PRIMARY = 'primary'
SECONDARY = 'secondary'
WHITE = 'white'
BLACK = 'black'
GREY = 'grey'
AQUA = 'aqua'
PURPLE = 'purple'
ORANGE = 'orange'
RED = 'red'
BLUE = 'blue'
PINK = 'pink'
GREEN = 'green'
YELLOW = 'yellow'
class GenericColor(Enum):
"""Generic colors that are safe to use everywhere."""
# <https://rich.readthedocs.io/en/stable/appendix/colors.html>
WHITE = {Styles.PIE: PieColor.WHITE, Styles.ANSI: 'white'}
BLACK = {Styles.PIE: PieColor.BLACK, Styles.ANSI: 'black'}
GREEN = {Styles.PIE: PieColor.GREEN, Styles.ANSI: 'green'}
ORANGE = {Styles.PIE: PieColor.ORANGE, Styles.ANSI: 'yellow'}
YELLOW = {Styles.PIE: PieColor.YELLOW, Styles.ANSI: 'bright_yellow'}
BLUE = {Styles.PIE: PieColor.BLUE, Styles.ANSI: 'blue'}
PINK = {Styles.PIE: PieColor.PINK, Styles.ANSI: 'bright_magenta'}
PURPLE = {Styles.PIE: PieColor.PURPLE, Styles.ANSI: 'magenta'}
RED = {Styles.PIE: PieColor.RED, Styles.ANSI: 'red'}
AQUA = {Styles.PIE: PieColor.AQUA, Styles.ANSI: 'cyan'}
GREY = {Styles.PIE: PieColor.GREY, Styles.ANSI: 'bright_black'}
def apply_style(
self, style: Styles, *, style_name: Optional[str] = None
) -> str:
"""Apply the given style to a particular value."""
exposed_color = self.value[style]
if style is Styles.PIE:
assert style_name is not None
shade = PIE_STYLE_TO_SHADE[PieStyle(style_name)]
return get_color(exposed_color, shade)
else:
return exposed_color
# noinspection PyDictCreation
COLOR_PALETTE = { COLOR_PALETTE = {
# Copy the brand palette # Copy the brand palette
'white': '#F5F5F0', PieColor.WHITE: '#F5F5F0',
'black': '#1C1818', PieColor.BLACK: '#1C1818',
'grey': { PieColor.GREY: {
'50': '#F5F5F0', '50': '#F5F5F0',
'100': '#EDEDEB', '100': '#EDEDEB',
'200': '#D1D1CF', '200': '#D1D1CF',
@ -23,7 +104,7 @@ COLOR_PALETTE = {
'900': '#1C1818', '900': '#1C1818',
'DEFAULT': '#7D7D7D', 'DEFAULT': '#7D7D7D',
}, },
'aqua': { PieColor.AQUA: {
'50': '#E8F0F5', '50': '#E8F0F5',
'100': '#D6E3ED', '100': '#D6E3ED',
'200': '#C4D9E5', '200': '#C4D9E5',
@ -36,7 +117,7 @@ COLOR_PALETTE = {
'900': '#455966', '900': '#455966',
'DEFAULT': '#8CB4CD', 'DEFAULT': '#8CB4CD',
}, },
'purple': { PieColor.PURPLE: {
'50': '#F0E0FC', '50': '#F0E0FC',
'100': '#E3C7FA', '100': '#E3C7FA',
'200': '#D9ADF7', '200': '#D9ADF7',
@ -49,7 +130,7 @@ COLOR_PALETTE = {
'900': '#5C2982', '900': '#5C2982',
'DEFAULT': '#B464F0', 'DEFAULT': '#B464F0',
}, },
'orange': { PieColor.ORANGE: {
'50': '#FFEDDB', '50': '#FFEDDB',
'100': '#FFDEBF', '100': '#FFDEBF',
'200': '#FFCFA3', '200': '#FFCFA3',
@ -62,7 +143,7 @@ COLOR_PALETTE = {
'900': '#C75E0A', '900': '#C75E0A',
'DEFAULT': '#FFA24E', 'DEFAULT': '#FFA24E',
}, },
'red': { PieColor.RED: {
'50': '#FFE0DE', '50': '#FFE0DE',
'100': '#FFC7C4', '100': '#FFC7C4',
'200': '#FFB0AB', '200': '#FFB0AB',
@ -75,7 +156,7 @@ COLOR_PALETTE = {
'900': '#910A00', '900': '#910A00',
'DEFAULT': '#FF665B', 'DEFAULT': '#FF665B',
}, },
'blue': { PieColor.BLUE: {
'50': '#DBE3FA', '50': '#DBE3FA',
'100': '#BFCFF5', '100': '#BFCFF5',
'200': '#A1B8F2', '200': '#A1B8F2',
@ -88,7 +169,7 @@ COLOR_PALETTE = {
'900': '#2B478F', '900': '#2B478F',
'DEFAULT': '#4B78E6', 'DEFAULT': '#4B78E6',
}, },
'pink': { PieColor.PINK: {
'50': '#FFEBFF', '50': '#FFEBFF',
'100': '#FCDBFC', '100': '#FCDBFC',
'200': '#FCCCFC', '200': '#FCCCFC',
@ -101,7 +182,7 @@ COLOR_PALETTE = {
'900': '#8C3D8A', '900': '#8C3D8A',
'DEFAULT': '#FA9BFA', 'DEFAULT': '#FA9BFA',
}, },
'green': { PieColor.GREEN: {
'50': '#E3F7E8', '50': '#E3F7E8',
'100': '#CCF2D6', '100': '#CCF2D6',
'200': '#B5EDC4', '200': '#B5EDC4',
@ -114,7 +195,7 @@ COLOR_PALETTE = {
'900': '#307842', '900': '#307842',
'DEFAULT': '#73DC8C', 'DEFAULT': '#73DC8C',
}, },
'yellow': { PieColor.YELLOW: {
'50': '#F7F7DB', '50': '#F7F7DB',
'100': '#F2F2BF', '100': '#F2F2BF',
'200': '#EDEDA6', '200': '#EDEDA6',
@ -128,47 +209,38 @@ COLOR_PALETTE = {
'DEFAULT': '#DBDE52', 'DEFAULT': '#DBDE52',
}, },
} }
COLOR_PALETTE.update(
# Grey is the same no matter shade for the colors {
COLOR_PALETTE['grey'] = { # Terminal-specific palette customizations.
shade: COLOR_PALETTE['grey']['500'] for shade in COLOR_PALETTE['grey'].keys() PieColor.GREY: {
} # Grey is the same no matter shade for the colors
shade: COLOR_PALETTE[PieColor.GREY]['500']
COLOR_PALETTE['primary'] = { for shade in COLOR_PALETTE[PieColor.GREY].keys()
'700': COLOR_PALETTE['black'], },
'600': 'ansibrightblack', PieColor.PRIMARY: {
'500': COLOR_PALETTE['white'], '700': COLOR_PALETTE[PieColor.BLACK],
} '600': PYGMENTS_BRIGHT_BLACK,
'500': COLOR_PALETTE[PieColor.WHITE],
COLOR_PALETTE['secondary'] = {'700': '#37523C', '600': '#6c6969', '500': '#6c6969'} },
PieColor.SECONDARY: {
'700': '#37523C',
'600': '#6c6969',
'500': '#6c6969',
},
}
)
SHADE_NAMES = { def boldify(color: PieColor) -> str:
'500': STYLE_PIE_DARK, return f'bold {color}'
'600': STYLE_PIE,
'700': STYLE_PIE_LIGHT
}
STYLE_SHADES = {
style: shade
for shade, style in SHADE_NAMES.items()
}
SHADES = [
'50',
*map(str, range(100, 1000, 100))
]
# noinspection PyDefaultArgument
def get_color( def get_color(
color: str, color: PieColor, shade: str, *, palette=COLOR_PALETTE
shade: str,
*,
palette: Dict[str, Dict[str, str]] = COLOR_PALETTE
) -> Optional[str]: ) -> Optional[str]:
if color not in palette: if color not in palette:
return None return None
color_code = palette[color] color_code = palette[color]
if isinstance(color_code, dict) and shade in color_code: if isinstance(color_code, dict) and shade in color_code:
return color_code[shade] return color_code[shade]

View File

@ -10,16 +10,18 @@ from rich.text import Text
from httpie.cli.constants import SEPARATOR_GROUP_ALL_ITEMS from httpie.cli.constants import SEPARATOR_GROUP_ALL_ITEMS
from httpie.cli.options import Argument, ParserSpec, Qualifiers from httpie.cli.options import Argument, ParserSpec, Qualifiers
from httpie.output.ui.palette import GenericColor
SEPARATORS = '|'.join(map(re.escape, SEPARATOR_GROUP_ALL_ITEMS)) SEPARATORS = '|'.join(map(re.escape, SEPARATOR_GROUP_ALL_ITEMS))
STYLE_METAVAR = 'yellow' STYLE_METAVAR = GenericColor.YELLOW
STYLE_SWITCH = 'green' STYLE_SWITCH = GenericColor.GREEN
STYLE_PROGRAM_NAME = 'bold green' STYLE_PROGRAM_NAME = GenericColor.GREEN # .boldify()
STYLE_USAGE_OPTIONAL = 'grey46' STYLE_USAGE_OPTIONAL = GenericColor.GREY
STYLE_USAGE_REGULAR = 'white' STYLE_USAGE_REGULAR = GenericColor.WHITE
STYLE_USAGE_ERROR = 'red' STYLE_USAGE_ERROR = GenericColor.RED
STYLE_USAGE_MISSING = 'yellow' STYLE_USAGE_MISSING = GenericColor.YELLOW
STYLE_BOLD = 'bold'
MAX_CHOICE_CHARS = 80 MAX_CHOICE_CHARS = 80
@ -77,7 +79,7 @@ def to_usage(
# shown first # shown first
shown_arguments.sort(key=lambda argument: argument.aliases, reverse=True) shown_arguments.sort(key=lambda argument: argument.aliases, reverse=True)
text = Text(program_name or spec.program, style='bold') text = Text(program_name or spec.program, style=STYLE_BOLD)
for argument in shown_arguments: for argument in shown_arguments:
text.append(' ') text.append(' ')

View File

@ -1,23 +1,65 @@
from httpie.output.ui.palette import * # noqa from collections import ChainMap
from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING:
from rich.theme import Theme
from httpie.output.ui.palette import GenericColor, PieStyle, Styles # noqa
# Rich-specific color code declarations # Rich-specific color code declarations
# https://github.com/Textualize/rich/blob/fcd684dd3a482977cab620e71ccaebb94bf13ac9/rich/default_styles.py#L5 # <https://github.com/Textualize/rich/blob/fcd684dd3a482977cab620e71ccaebb94bf13ac9/rich/default_styles.py>
CUSTOM_STYLES = { CUSTOM_STYLES = {
'progress.description': 'white', 'progress.description': GenericColor.WHITE,
'progress.data.speed': 'green', 'progress.data.speed': GenericColor.GREEN,
'progress.percentage': 'aqua', 'progress.percentage': GenericColor.AQUA,
'progress.download': 'aqua', 'progress.download': GenericColor.AQUA,
'progress.remaining': 'orange', 'progress.remaining': GenericColor.ORANGE,
'bar.complete': 'purple', 'bar.complete': GenericColor.PURPLE,
'bar.finished': 'green', 'bar.finished': GenericColor.GREEN,
'bar.pulse': 'purple', 'bar.pulse': GenericColor.PURPLE,
'option': 'pink' 'option': GenericColor.PINK,
} }
RICH_THEME_PALETTE = COLOR_PALETTE.copy() # noqa
RICH_THEME_PALETTE.update( class _GenericColorCaster(dict):
{ """
custom_style: RICH_THEME_PALETTE[color] Translate GenericColor to a regular string on the attribute access
for custom_style, color in CUSTOM_STYLES.items() phase.
} """
)
def _translate(self, key: Any) -> Any:
if isinstance(key, GenericColor):
return key.name.lower()
else:
return key
def __getitem__(self, key: Any) -> Any:
return super().__getitem__(self._translate(key))
def get(self, key: Any) -> Any:
return super().get(self._translate(key))
def _make_rich_color_theme(style_name: Optional[str]) -> 'Theme':
from rich.style import Style
from rich.theme import Theme
try:
PieStyle(style_name)
except ValueError:
style = Styles.ANSI
else:
style = Styles.PIE
theme = Theme()
for color, color_set in ChainMap(
GenericColor.__members__, CUSTOM_STYLES
).items():
theme.styles[color.lower()] = Style(
color=color_set.apply_style(style, style_name=style_name),
bold=style is Styles.PIE,
)
# E.g translate GenericColor.BLUE into blue on key access
theme.styles = _GenericColorCaster(theme.styles)
return theme

View File

@ -28,10 +28,7 @@ class BaseDisplay:
return self.env.rich_error_console return self.env.rich_error_console
def _print_summary( def _print_summary(
self, self, is_finished: bool, observed_steps: int, time_spent: float
is_finished: bool,
observed_steps: int,
time_spent: float
): ):
from rich import filesize from rich import filesize
@ -50,7 +47,9 @@ class BaseDisplay:
else: else:
total_time = f'{minutes:02d}:{seconds:0.5f}' total_time = f'{minutes:02d}:{seconds:0.5f}'
self.console.print(f'[progress.description]{verb}. {total_size} in {total_time} ({avg_speed}/s)') self.console.print(
f'[progress.description]{verb}. {total_size} in {total_time} ({avg_speed}/s)'
)
class DummyDisplay(BaseDisplay): class DummyDisplay(BaseDisplay):
@ -65,7 +64,9 @@ class StatusDisplay(BaseDisplay):
self, *, total: Optional[float], at: float, description: str self, *, total: Optional[float], at: float, description: str
) -> None: ) -> None:
self.observed = at self.observed = at
self.description = f'[progress.description]{description}[/progress.description]' self.description = (
f'[progress.description]{description}[/progress.description]'
)
self.status = self.console.status(self.description, spinner='line') self.status = self.console.status(self.description, spinner='line')
self.status.start() self.status.start()
@ -75,8 +76,12 @@ class StatusDisplay(BaseDisplay):
self.observed += steps self.observed += steps
observed_amount, observed_unit = filesize.decimal(self.observed).split() observed_amount, observed_unit = filesize.decimal(
self.status.update(status=f'{self.description} [progress.download]{observed_amount}/? {observed_unit}[/progress.download]') self.observed
).split()
self.status.update(
status=f'{self.description} [progress.download]{observed_amount}/? {observed_unit}[/progress.download]'
)
def stop(self, time_spent: float) -> None: def stop(self, time_spent: float) -> None:
self.status.stop() self.status.stop()
@ -85,7 +90,7 @@ class StatusDisplay(BaseDisplay):
self._print_summary( self._print_summary(
is_finished=True, is_finished=True,
observed_steps=self.observed, observed_steps=self.observed,
time_spent=time_spent time_spent=time_spent,
) )
@ -114,7 +119,7 @@ class ProgressDisplay(BaseDisplay):
TimeRemainingColumn(), TimeRemainingColumn(),
TransferSpeedColumn(), TransferSpeedColumn(),
console=self.console, console=self.console,
transient=True transient=True,
) )
self.progress_bar.start() self.progress_bar.start()
self.transfer_task = self.progress_bar.add_task( self.transfer_task = self.progress_bar.add_task(
@ -132,5 +137,5 @@ class ProgressDisplay(BaseDisplay):
self._print_summary( self._print_summary(
is_finished=task.finished, is_finished=task.finished,
observed_steps=task.completed, observed_steps=task.completed,
time_spent=time_spent time_spent=time_spent,
) )

View File

@ -11,11 +11,8 @@ def render_as_string(renderable: RenderableType) -> str:
"""Render any `rich` object in a fake console and """Render any `rich` object in a fake console and
return a *style-less* version of it as a string.""" return a *style-less* version of it as a string."""
with open(os.devnull, "w") as null_stream: with open(os.devnull, 'w') as null_stream:
fake_console = Console( fake_console = Console(file=null_stream, record=True)
file=null_stream,
record=True
)
fake_console.print(renderable) fake_console.print(renderable)
return fake_console.export_text() return fake_console.export_text()

View File

@ -13,7 +13,7 @@ from typing import Any, Dict, List, Optional, Union
from requests.auth import AuthBase from requests.auth import AuthBase
from requests.cookies import RequestsCookieJar, remove_cookie_by_name from requests.cookies import RequestsCookieJar, remove_cookie_by_name
from .context import Environment, Levels from .context import Environment, LogLevel
from .cookies import HTTPieCookiePolicy from .cookies import HTTPieCookiePolicy
from .cli.dicts import HTTPHeadersDict from .cli.dicts import HTTPHeadersDict
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
@ -313,7 +313,7 @@ class Session(BaseConfigDict):
self.env.log_error( self.env.log_error(
warning, warning,
level=Levels.WARNING level=LogLevel.WARNING
) )
# We don't want to spam multiple warnings on each usage, # We don't want to spam multiple warnings on each usage,