diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e3b9a5f4f..eb8a917864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) ## [2.6.0](https://github.com/httpie/httpie/compare/2.5.0...2.6.0) (2021-10-14) diff --git a/docs/README.md b/docs/README.md index 5fd320db97..9a81a72677 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 don’t 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` | HTTPie’s 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 it’s 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: diff --git a/httpie/output/formatters/colors.py b/httpie/output/formatters/colors.py index b2db70ff37..757163f84b 100644 --- a/httpie/output/formatters/colors.py +++ b/httpie/output/formatters/colors.py @@ -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.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 @@ 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 @@ def __init__( 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 @@ def format_body(self, body: str, mime: str) -> str: body = pygments.highlight( code=body, lexer=lexer, - formatter=self.formatter, + formatter=self.body_formatter, ) return body @@ -104,6 +108,25 @@ def get_lexer_for_body( 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() diff --git a/httpie/output/lexers/http.py b/httpie/output/lexers/http.py index 4c2b00d252..0b8b612a0e 100644 --- a/httpie/output/lexers/http.py +++ b/httpie/output/lexers/http.py @@ -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( diff --git a/httpie/output/ui/__init__.py b/httpie/output/ui/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/httpie/output/ui/palette.py b/httpie/output/ui/palette.py new file mode 100644 index 0000000000..a13aef058c --- /dev/null +++ b/httpie/output/ui/palette.py @@ -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