From 442d18c5db15269e1f8e323057694b9624a19807 Mon Sep 17 00:00:00 2001 From: symphony-dbcli Date: Thu, 18 Jun 2026 17:44:28 +0000 Subject: [PATCH] Conversation #1: Add solarized theme --- CHANGELOG.md | 4 + litecli/clistyle.py | 175 ++++++++++++++++++++++++++++++++++++----- litecli/liteclirc | 2 +- tests/liteclirc | 2 +- tests/test_clistyle.py | 20 ++++- 5 files changed, 181 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e429af4..91d98a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +### Features + +- Add Solarized syntax highlighting styles. + ### Bug Fixes - Expand `~` in configured log file paths before opening the log. diff --git a/litecli/clistyle.py b/litecli/clistyle.py index b364872..6a51afb 100644 --- a/litecli/clistyle.py +++ b/litecli/clistyle.py @@ -1,14 +1,25 @@ from __future__ import annotations import logging -from typing import cast import pygments.styles from prompt_toolkit.styles import Style, merge_styles from prompt_toolkit.styles.pygments import style_from_pygments_cls from prompt_toolkit.styles.style import _MergedStyle from pygments.style import Style as PygmentsStyle -from pygments.token import Token, _TokenType, string_to_tokentype +from pygments.token import ( + Comment, + Error, + Generic, + Keyword, + Name, + Number, + Operator, + String, + Token, + _TokenType, + string_to_tokentype, +) from pygments.util import ClassNotFound logger = logging.getLogger(__name__) @@ -46,7 +57,135 @@ PROMPT_STYLE_TO_TOKEN: dict[str, _TokenType] = {v: k for k, v in TOKEN_TO_PROMPT_STYLE.items()} -def parse_pygments_style(token_name: str, style_object: PygmentsStyle | dict, style_dict: dict[str, str]) -> tuple[_TokenType, str]: +def _make_solarized_style(colors: dict[str, str]) -> dict[_TokenType, str]: + return { + Token: colors["base0"], + Comment: "italic " + colors["base01"], + Comment.Hashbang: colors["base01"], + Comment.Multiline: colors["base01"], + Comment.Preproc: "noitalic " + colors["magenta"], + Comment.PreprocFile: "noitalic " + colors["base01"], + Keyword: colors["green"], + Keyword.Constant: colors["cyan"], + Keyword.Declaration: colors["cyan"], + Keyword.Namespace: colors["orange"], + Keyword.Type: colors["yellow"], + Operator: colors["base01"], + Operator.Word: colors["green"], + Name.Builtin: colors["blue"], + Name.Builtin.Pseudo: colors["blue"], + Name.Class: colors["blue"], + Name.Constant: colors["blue"], + Name.Decorator: colors["blue"], + Name.Entity: colors["blue"], + Name.Exception: colors["blue"], + Name.Function: colors["blue"], + Name.Function.Magic: colors["blue"], + Name.Label: colors["blue"], + Name.Namespace: colors["blue"], + Name.Tag: colors["blue"], + Name.Variable: colors["blue"], + Name.Variable.Global: colors["blue"], + Name.Variable.Magic: colors["blue"], + String: colors["cyan"], + String.Doc: colors["base01"], + String.Regex: colors["orange"], + Number: colors["cyan"], + Generic: colors["base0"], + Generic.Deleted: colors["red"], + Generic.Emph: "italic", + Generic.Error: colors["red"], + Generic.Heading: "bold", + Generic.Subheading: "underline", + Generic.Inserted: colors["green"], + Generic.Output: colors["base0"], + Generic.Prompt: "bold " + colors["blue"], + Generic.Strong: "bold", + Generic.EmphStrong: "bold italic", + Generic.Traceback: colors["blue"], + Error: "bg:" + colors["red"], + } + + +SOLARIZED_DARK_COLORS = { + "base03": "#002b36", + "base02": "#073642", + "base01": "#586e75", + "base00": "#657b83", + "base0": "#839496", + "base1": "#93a1a1", + "base2": "#eee8d5", + "base3": "#fdf6e3", + "yellow": "#b58900", + "orange": "#cb4b16", + "red": "#dc322f", + "magenta": "#d33682", + "violet": "#6c71c4", + "blue": "#268bd2", + "cyan": "#2aa198", + "green": "#859900", +} + +SOLARIZED_LIGHT_COLORS = { + "base3": "#002b36", + "base2": "#073642", + "base1": "#586e75", + "base0": "#657b83", + "base00": "#839496", + "base01": "#93a1a1", + "base02": "#eee8d5", + "base03": "#fdf6e3", + "yellow": "#b58900", + "orange": "#cb4b16", + "red": "#dc322f", + "magenta": "#d33682", + "violet": "#6c71c4", + "blue": "#268bd2", + "cyan": "#2aa198", + "green": "#859900", +} + + +class SolarizedDarkStyle(PygmentsStyle): + name = "solarized-dark" + background_color = SOLARIZED_DARK_COLORS["base03"] + highlight_color = SOLARIZED_DARK_COLORS["base02"] + line_number_color = SOLARIZED_DARK_COLORS["base01"] + line_number_background_color = SOLARIZED_DARK_COLORS["base02"] + styles = _make_solarized_style(SOLARIZED_DARK_COLORS) + + +class SolarizedLightStyle(PygmentsStyle): + name = "solarized-light" + background_color = SOLARIZED_LIGHT_COLORS["base03"] + highlight_color = SOLARIZED_LIGHT_COLORS["base02"] + line_number_color = SOLARIZED_LIGHT_COLORS["base01"] + line_number_background_color = SOLARIZED_LIGHT_COLORS["base02"] + styles = _make_solarized_style(SOLARIZED_LIGHT_COLORS) + + +CUSTOM_STYLES: dict[str, type[PygmentsStyle]] = { + "solarized": SolarizedDarkStyle, + "solarized-dark": SolarizedDarkStyle, + "solarized-light": SolarizedLightStyle, +} + + +def get_style(name: str) -> type[PygmentsStyle]: + if name in CUSTOM_STYLES: + return CUSTOM_STYLES[name] + + try: + return pygments.styles.get_style_by_name(name) + except ClassNotFound: + return pygments.styles.get_style_by_name("native") + + +def parse_pygments_style( + token_name: str, + style_object: type[PygmentsStyle] | dict[_TokenType, str], + style_dict: dict[str, str], +) -> tuple[_TokenType, str]: """Parse token type and style string. :param token_name: str name of Pygments token. Example: "Token.String" @@ -55,18 +194,16 @@ def parse_pygments_style(token_name: str, style_object: PygmentsStyle | dict, st """ token_type = string_to_tokentype(token_name) - if isinstance(style_object, PygmentsStyle): - other_token_type = string_to_tokentype(style_dict[token_name]) + style_value = style_dict[token_name] + if isinstance(style_object, type) and issubclass(style_object, PygmentsStyle) and style_value.startswith("Token."): + other_token_type = string_to_tokentype(style_value) return token_type, style_object.styles[other_token_type] else: - return token_type, style_dict[token_name] + return token_type, style_value def style_factory(name: str, cli_style: dict[str, str]) -> _MergedStyle: - try: - style = pygments.styles.get_style_by_name(name) - except ClassNotFound: - style = pygments.styles.get_style_by_name("native") + style = get_style(name) prompt_styles: list[tuple[str, str]] = [] # prompt-toolkit used pygments tokens for styling before, switched to style @@ -90,11 +227,9 @@ def style_factory(name: str, cli_style: dict[str, str]) -> _MergedStyle: return merge_styles([style_from_pygments_cls(style), override_style, Style(prompt_styles)]) -def style_factory_output(name: str, cli_style: dict[str, str]) -> PygmentsStyle: - try: - style = pygments.styles.get_style_by_name(name).styles - except ClassNotFound: - style = pygments.styles.get_style_by_name("native").styles +def style_factory_output(name: str, cli_style: dict[str, str]) -> type[PygmentsStyle]: + style_cls = get_style(name) + style = dict(style_cls.styles) for token in cli_style: if token.startswith("Token."): @@ -109,7 +244,11 @@ def style_factory_output(name: str, cli_style: dict[str, str]) -> PygmentsStyle: class OutputStyle(PygmentsStyle): default_style = "" - styles = style - # mypy does not complain but ty complains: error[invalid-return-type]: Return type does not match returned value. Hence added cast. - return cast(OutputStyle, PygmentsStyle) + OutputStyle.background_color = style_cls.background_color + OutputStyle.highlight_color = style_cls.highlight_color + OutputStyle.line_number_color = style_cls.line_number_color + OutputStyle.line_number_background_color = style_cls.line_number_background_color + OutputStyle.styles = style + + return OutputStyle diff --git a/litecli/liteclirc b/litecli/liteclirc index ec162d3..a48648b 100644 --- a/litecli/liteclirc +++ b/litecli/liteclirc @@ -46,7 +46,7 @@ table_format = ascii # Syntax coloring style. Possible values (many support the "-dark" suffix): # manni, igor, xcode, vim, autumn, vs, rrt, native, perldoc, borland, tango, emacs, # friendly, monokai, paraiso, colorful, murphy, bw, pastie, paraiso, trac, default, -# fruity. +# fruity, solarized, solarized-dark, solarized-light. # See the LiteCLI README for syntax examples syntax_style = default diff --git a/tests/liteclirc b/tests/liteclirc index 91f7df9..183a200 100644 --- a/tests/liteclirc +++ b/tests/liteclirc @@ -39,7 +39,7 @@ table_format = ascii # Syntax coloring style. Possible values (many support the "-dark" suffix): # manni, igor, xcode, vim, autumn, vs, rrt, native, perldoc, borland, tango, emacs, # friendly, monokai, paraiso, colorful, murphy, bw, pastie, paraiso, trac, default, -# fruity. +# fruity, solarized, solarized-dark, solarized-light. # Screenshots at http://mycli.net/syntax syntax_style = default diff --git a/tests/test_clistyle.py b/tests/test_clistyle.py index 00d6f5a..312766b 100644 --- a/tests/test_clistyle.py +++ b/tests/test_clistyle.py @@ -4,9 +4,9 @@ import pytest from pygments.style import Style -from pygments.token import Token +from pygments.token import Keyword, String, Token -from litecli.clistyle import style_factory +from litecli.clistyle import SolarizedDarkStyle, SolarizedLightStyle, style_factory, style_factory_output @pytest.mark.skip(reason="incompatible with new prompt toolkit") @@ -27,3 +27,19 @@ def test_style_factory_unknown_name(): style = style_factory("foobar", {}) assert isinstance(style, Style) + + +@pytest.mark.parametrize( + ("name", "style", "background"), + [ + ("solarized", SolarizedDarkStyle, "#002b36"), + ("solarized-dark", SolarizedDarkStyle, "#002b36"), + ("solarized-light", SolarizedLightStyle, "#fdf6e3"), + ], +) +def test_style_factory_output_solarized(name, style, background): + output_style = style_factory_output(name, {}) + + assert output_style.styles[Keyword] == style.styles[Keyword] + assert output_style.styles[String] == style.styles[String] + assert output_style.background_color == background