Implement new pie and pie-light styles (#1238)

* Implement new `pie` and `pie-light` styles

* Change some pallete

* Integrate the color palette

* some docs

* some docs

* Rework on code generation

* Apply suggestions from code review

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
This commit is contained in:
Batuhan Taskaya 2021-12-19 13:41:42 +03:00 committed by GitHub
parent 1bd8422fb5
commit 8dc6c0df77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 384 additions and 20 deletions

View File

@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- Added support for _receiving_ multiple HTTP headers lines with the same name. ([#1207](https://github.com/httpie/httpie/issues/1207))
- Added support for basic JSON types on `--form`/`--multipart` when using JSON only operators (`:=`/`:=@`). ([#1212](https://github.com/httpie/httpie/issues/1212))
- Added support for automatically enabling `--stream` when `Content-Type` is `text/event-stream`. ([#376](https://github.com/httpie/httpie/issues/376))
- Added new `pie-dark`/`pie-light` (and `pie`) styles that match with [HTTPie for Web and Desktop](https://httpie.io/product). ([#1237](https://github.com/httpie/httpie/issues/1237))
- Broken plugins will no longer crash the whole application. ([#1204](https://github.com/httpie/httpie/issues/1204))
- Fixed auto addition of XML declaration to every formatted XML response. ([#1156](https://github.com/httpie/httpie/issues/1156))
- Fixed highlighting when `Content-Type` specifies `charset`. ([#1242](https://github.com/httpie/httpie/issues/1242))

View File

@ -1717,13 +1717,16 @@ Syntax highlighting is applied to HTTP headers and bodies (where it makes sense)
You can choose your preferred color scheme via the `--style` option if you dont like the default one.
There are dozens of styles available, here are just a few notable ones:
| Style | Description |
| --------: | ----------------------------------------------------------------------------------------------------------------------------------- |
| `auto` | Follows your terminal ANSI color styles. This is the default style used by HTTPie |
| `default` | Default styles of the underlying Pygments library. Not actually used by default by HTTPie. You can enable it with `--style=default` |
| `monokai` | A popular color scheme. Enable with `--style=monokai` |
| `fruity` | A bold, colorful scheme. Enable with `--style=fruity` |
| … | See `$ http --help` for all the possible `--style` values |
| Style | Description |
| ---------: | ------------------------------------------------------------------------------------------------------------------------------------ |
| `auto` | Follows your terminal ANSI color styles. This is the default style used by HTTPie |
| `default` | Default styles of the underlying Pygments library. Not actually used by default by HTTPie. You can enable it with `--style=default` |
| `pie-dark` | HTTPies original brand style. Also used in [HTTPie for Web and Desktop](https://httpie.io/product). |
|`pie-light` | Like `pie-dark`, but for terminals with light background colors. |
| `pie` | A generic version of `pie-dark` and `pie-light` themes that can work with any terminal background. Its universality requires compromises in terms of legibility, but its useful if you frequently switch your terminal between dark and light backgrounds. |
| `monokai` | A popular color scheme. Enable with `--style=monokai` |
| `fruity` | A bold, colorful scheme. Enable with `--style=fruity` |
| … | See `$ http --help` for all the possible `--style` values |
Use one of these options to control output processing:

View File

@ -1,6 +1,7 @@
import json
from typing import Optional, Type
from typing import Optional, Type, Tuple
import pygments.formatters
import pygments.lexer
import pygments.lexers
import pygments.style
@ -15,6 +16,7 @@ from pygments.lexers.text import HttpLexer as PygmentsHttpLexer
from pygments.util import ClassNotFound
from ..lexers.json import EnhancedJsonLexer
from ..ui.palette import SHADE_NAMES, get_color
from ...compat import is_windows
from ...context import Environment
from ...plugins import FormatterPlugin
@ -23,6 +25,7 @@ from ...plugins import FormatterPlugin
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
DEFAULT_STYLE = AUTO_STYLE
SOLARIZED_STYLE = 'solarized' # Bundled here
if is_windows:
# Colors on Windows via colorama don't look that
# great and fruity seems to give the best result there.
@ -66,22 +69,23 @@ class ColorFormatter(FormatterPlugin):
if use_auto_style or not has_256_colors:
http_lexer = PygmentsHttpLexer()
formatter = TerminalFormatter()
body_formatter = formatter
header_formatter = formatter
else:
from ..lexers.http import SimplifiedHTTPLexer
http_lexer = SimplifiedHTTPLexer()
formatter = Terminal256Formatter(
style=self.get_style_class(color_scheme)
)
header_formatter, body_formatter, precise = self.get_formatters(color_scheme)
http_lexer = SimplifiedHTTPLexer(precise=precise)
self.explicit_json = explicit_json # --json
self.formatter = formatter
self.header_formatter = header_formatter
self.body_formatter = body_formatter
self.http_lexer = http_lexer
def format_headers(self, headers: str) -> str:
return pygments.highlight(
code=headers,
lexer=self.http_lexer,
formatter=self.formatter,
formatter=self.header_formatter,
).strip()
def format_body(self, body: str, mime: str) -> str:
@ -90,7 +94,7 @@ class ColorFormatter(FormatterPlugin):
body = pygments.highlight(
code=body,
lexer=lexer,
formatter=self.formatter,
formatter=self.body_formatter,
)
return body
@ -104,6 +108,25 @@ class ColorFormatter(FormatterPlugin):
body=body,
)
def get_formatters(self, color_scheme: str) -> Tuple[
pygments.formatter.Formatter,
pygments.formatter.Formatter,
bool
]:
if color_scheme in PIE_STYLES:
header_style, body_style = PIE_STYLES[color_scheme]
precise = True
else:
header_style = self.get_style_class(color_scheme)
body_style = header_style
precise = False
return (
Terminal256Formatter(style=header_style),
Terminal256Formatter(style=body_style),
precise
)
@staticmethod
def get_style_class(color_scheme: str) -> Type[pygments.style.Style]:
try:
@ -237,3 +260,117 @@ class Solarized256Style(pygments.style.Style):
pygments.token.Token: BASE1,
pygments.token.Token.Other: ORANGE,
}
PIE_HEADER_STYLE = {
# HTTP line / Headers / Etc.
pygments.token.Name.Namespace: 'bold primary',
pygments.token.Keyword.Reserved: 'bold grey',
pygments.token.Operator: 'bold grey',
pygments.token.Number: 'bold grey',
pygments.token.Name.Function.Magic: 'bold green',
pygments.token.Name.Exception: 'bold green',
pygments.token.Name.Attribute: 'blue',
pygments.token.String: 'primary',
# HTTP Methods
pygments.token.Name.Function: 'bold grey',
pygments.token.Name.Function.HTTP.GET: 'bold green',
pygments.token.Name.Function.HTTP.HEAD: 'bold green',
pygments.token.Name.Function.HTTP.POST: 'bold yellow',
pygments.token.Name.Function.HTTP.PUT: 'bold orange',
pygments.token.Name.Function.HTTP.PATCH: 'bold orange',
pygments.token.Name.Function.HTTP.DELETE: 'bold red',
# HTTP status codes
pygments.token.Number.HTTP.INFO: 'bold aqua',
pygments.token.Number.HTTP.OK: 'bold green',
pygments.token.Number.HTTP.REDIRECT: 'bold yellow',
pygments.token.Number.HTTP.CLIENT_ERR: 'bold orange',
pygments.token.Number.HTTP.SERVER_ERR: 'bold red',
}
PIE_BODY_STYLE = {
# {}[]:
pygments.token.Punctuation: 'grey',
# Keys
pygments.token.Name.Tag: 'pink',
# Values
pygments.token.Literal.String: 'green',
pygments.token.Literal.String.Double: 'green',
pygments.token.Literal.Number: 'aqua',
pygments.token.Keyword: 'orange',
# Other stuff
pygments.token.Text: 'primary',
pygments.token.Name.Attribute: 'primary',
pygments.token.Name.Builtin: 'blue',
pygments.token.Name.Builtin.Pseudo: 'blue',
pygments.token.Name.Class: 'blue',
pygments.token.Name.Constant: 'orange',
pygments.token.Name.Decorator: 'blue',
pygments.token.Name.Entity: 'orange',
pygments.token.Name.Exception: 'yellow',
pygments.token.Name.Function: 'blue',
pygments.token.Name.Variable: 'blue',
pygments.token.String: 'aqua',
pygments.token.String.Backtick: 'secondary',
pygments.token.String.Char: 'aqua',
pygments.token.String.Doc: 'aqua',
pygments.token.String.Escape: 'red',
pygments.token.String.Heredoc: 'aqua',
pygments.token.String.Regex: 'red',
pygments.token.Number: 'aqua',
pygments.token.Operator: 'primary',
pygments.token.Operator.Word: 'green',
pygments.token.Comment: 'secondary',
pygments.token.Comment.Preproc: 'green',
pygments.token.Comment.Special: 'green',
pygments.token.Generic.Deleted: 'aqua',
pygments.token.Generic.Emph: 'italic',
pygments.token.Generic.Error: 'red',
pygments.token.Generic.Heading: 'orange',
pygments.token.Generic.Inserted: 'green',
pygments.token.Generic.Strong: 'bold',
pygments.token.Generic.Subheading: 'orange',
pygments.token.Token: 'primary',
pygments.token.Token.Other: 'orange',
}
def make_style(name, raw_styles, shade):
def format_value(value):
return ' '.join(
get_color(part, shade) or part
for part in value.split()
)
bases = (pygments.style.Style,)
data = {
'styles': {
key: format_value(value)
for key, value in raw_styles.items()
}
}
return type(name, bases, data)
def make_styles():
styles = {}
for shade, name in SHADE_NAMES.items():
styles[name] = [
make_style(name, style_map, shade)
for style_name, style_map in [
(f'Pie{name}HeaderStyle', PIE_HEADER_STYLE),
(f'Pie{name}BodyStyle', PIE_BODY_STYLE),
]
]
return styles
PIE_STYLES = make_styles()
BUNDLED_STYLES |= PIE_STYLES.keys()

View File

@ -1,6 +1,70 @@
import re
import pygments
RE_STATUS_LINE = re.compile(r'(\d{3})( +)(.+)')
STATUS_TYPES = {
'1': pygments.token.Number.HTTP.INFO,
'2': pygments.token.Number.HTTP.OK,
'3': pygments.token.Number.HTTP.REDIRECT,
'4': pygments.token.Number.HTTP.CLIENT_ERR,
'5': pygments.token.Number.HTTP.SERVER_ERR,
}
RESPONSE_TYPES = {
'GET': pygments.token.Name.Function.HTTP.GET,
'HEAD': pygments.token.Name.Function.HTTP.HEAD,
'POST': pygments.token.Name.Function.HTTP.POST,
'PUT': pygments.token.Name.Function.HTTP.PUT,
'PATCH': pygments.token.Name.Function.HTTP.PATCH,
'DELETE': pygments.token.Name.Function.HTTP.DELETE,
}
def precise(lexer, precise_token, parent_token):
# Due to a pygments bug*, custom tokens will look bad
# on outside styles. Until it is fixed on upstream, we'll
# convey whether the client is using pie style or not
# through precise option and return more precise tokens
# depending on it's value.
#
# [0]: https://github.com/pygments/pygments/issues/1986
if precise_token is None or not lexer.options.get("precise"):
return parent_token
else:
return precise_token
def http_response_type(lexer, match, ctx):
status_match = RE_STATUS_LINE.match(match.group())
if status_match is None:
return None
status_code, text, reason = status_match.groups()
status_type = precise(
lexer,
STATUS_TYPES.get(status_code[0]),
pygments.token.Number
)
groups = pygments.lexer.bygroups(
status_type,
pygments.token.Text,
status_type
)
yield from groups(lexer, status_match, ctx)
def request_method(lexer, match, ctx):
response_type = precise(
lexer,
RESPONSE_TYPES.get(match.group()),
pygments.token.Name.Function
)
yield match.start(), response_type, match.group()
class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
"""Simplified HTTP lexer for Pygments.
@ -18,7 +82,7 @@ class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
# Request-Line
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
pygments.lexer.bygroups(
pygments.token.Name.Function,
request_method,
pygments.token.Text,
pygments.token.Name.Namespace,
pygments.token.Text,
@ -27,15 +91,13 @@ class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
pygments.token.Number
)),
# Response Status-Line
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
(r'(HTTP)(/)(\d+\.\d+)( +)(.+)',
pygments.lexer.bygroups(
pygments.token.Keyword.Reserved, # 'HTTP'
pygments.token.Operator, # '/'
pygments.token.Number, # Version
pygments.token.Text,
pygments.token.Number, # Status code
pygments.token.Text,
pygments.token.Name.Exception, # Reason
http_response_type, # Status code and Reason
)),
# Header
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(

View File

161
httpie/output/ui/palette.py Normal file
View File

@ -0,0 +1,161 @@
# Copy the brand palette
from typing import Optional
COLOR_PALETTE = {
'transparent': 'transparent',
'current': 'currentColor',
'white': '#F5F5F0',
'black': '#1C1818',
'grey': {
'50': '#F5F5F0',
'100': '#EDEDEB',
'200': '#D1D1CF',
'300': '#B5B5B2',
'400': '#999999',
'500': '#7D7D7D',
'600': '#666663',
'700': '#4F4D4D',
'800': '#363636',
'900': '#1C1818',
'DEFAULT': '#7D7D7D',
},
'aqua': {
'50': '#E8F0F5',
'100': '#D6E3ED',
'200': '#C4D9E5',
'300': '#B0CCDE',
'400': '#9EBFD6',
'500': '#8CB4CD',
'600': '#7A9EB5',
'700': '#698799',
'800': '#597082',
'900': '#455966',
'DEFAULT': '#8CB4CD',
},
'purple': {
'50': '#F0E0FC',
'100': '#E3C7FA',
'200': '#D9ADF7',
'300': '#CC96F5',
'400': '#BF7DF2',
'500': '#B464F0',
'600': '#9E54D6',
'700': '#8745BA',
'800': '#70389E',
'900': '#5C2982',
'DEFAULT': '#B464F0',
},
'orange': {
'50': '#FFEDDB',
'100': '#FFDEBF',
'200': '#FFCFA3',
'300': '#FFBF87',
'400': '#FFB06B',
'500': '#FFA24E',
'600': '#F2913D',
'700': '#E3822B',
'800': '#D6701C',
'900': '#C75E0A',
'DEFAULT': '#FFA24E',
},
'red': {
'50': '#FFE0DE',
'100': '#FFC7C4',
'200': '#FFB0AB',
'300': '#FF968F',
'400': '#FF8075',
'500': '#FF665B',
'600': '#E34F45',
'700': '#C7382E',
'800': '#AD2117',
'900': '#910A00',
'DEFAULT': '#FF665B',
},
'blue': {
'50': '#DBE3FA',
'100': '#BFCFF5',
'200': '#A1B8F2',
'300': '#85A3ED',
'400': '#698FEB',
'500': '#4B78E6',
'600': '#426BD1',
'700': '#3B5EBA',
'800': '#3354A6',
'900': '#2B478F',
'DEFAULT': '#4B78E6',
},
'pink': {
'50': '#FFEBFF',
'100': '#FCDBFC',
'200': '#FCCCFC',
'300': '#FCBAFC',
'400': '#FAABFA',
'500': '#FA9BFA',
'600': '#DE85DE',
'700': '#C26EC2',
'800': '#A854A6',
'900': '#8C3D8A',
'DEFAULT': '#FA9BFA',
},
'green': {
'50': '#E3F7E8',
'100': '#CCF2D6',
'200': '#B5EDC4',
'300': '#A1E8B0',
'400': '#8AE09E',
'500': '#73DC8C',
'600': '#63C27A',
'700': '#52AB66',
'800': '#429154',
'900': '#307842',
'DEFAULT': '#73DC8C',
},
'yellow': {
'50': '#F7F7DB',
'100': '#F2F2BF',
'200': '#EDEDA6',
'300': '#E5E88A',
'400': '#E0E36E',
'500': '#DBDE52',
'600': '#CCCC3D',
'700': '#BABA29',
'800': '#ABA614',
'900': '#999400',
'DEFAULT': '#DBDE52',
},
}
# Grey is the same no matter shade for the colors
COLOR_PALETTE['grey'] = {
shade: COLOR_PALETTE['grey']['500'] for shade in COLOR_PALETTE['grey'].keys()
}
COLOR_PALETTE['primary'] = {
'700': COLOR_PALETTE['black'],
'600': 'ansibrightblack',
'500': COLOR_PALETTE['white'],
}
COLOR_PALETTE['secondary'] = {'700': '#37523C', '600': '#6c6969', '500': '#6c6969'}
SHADE_NAMES = {
'500': 'pie-dark',
'600': 'pie',
'700': 'pie-light'
}
SHADES = [
'50',
*map(str, range(100, 1000, 100))
]
def get_color(color: str, shade: str) -> Optional[str]:
if color not in COLOR_PALETTE:
return None
color_code = COLOR_PALETTE[color]
if isinstance(color_code, dict) and shade in color_code:
return color_code[shade]
else:
return color_code