clan-cli: Refactor colors to a subset of colors that work in light and dark mode
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
@@ -11,10 +12,12 @@ import timeit
|
||||
import weakref
|
||||
from collections.abc import Iterator
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import IO, Any
|
||||
|
||||
from clan_cli.colors import Color
|
||||
from clan_cli.custom_logger import print_trace
|
||||
from clan_cli.errors import ClanCmdError, ClanError, CmdOut, indent_command
|
||||
|
||||
@@ -43,16 +46,23 @@ class Log(Enum):
|
||||
NONE = 4
|
||||
|
||||
|
||||
@dataclass
|
||||
class MsgColor:
|
||||
stderr: Color | None = None
|
||||
stdout: Color | None = None
|
||||
|
||||
|
||||
def handle_io(
|
||||
process: subprocess.Popen,
|
||||
log: Log,
|
||||
cmdlog: logging.Logger,
|
||||
prefix: str,
|
||||
*,
|
||||
prefix: str | None,
|
||||
input_bytes: bytes | None,
|
||||
stdout: IO[bytes] | None,
|
||||
stderr: IO[bytes] | None,
|
||||
timeout: float = math.inf,
|
||||
msg_color: MsgColor | None = None,
|
||||
) -> tuple[str, str]:
|
||||
rlist = [process.stdout, process.stderr]
|
||||
wlist = [process.stdin] if input_bytes is not None else []
|
||||
@@ -85,6 +95,12 @@ def handle_io(
|
||||
rlist.remove(fd)
|
||||
return b""
|
||||
|
||||
extra = {"command_prefix": prefix}
|
||||
if msg_color and msg_color.stderr:
|
||||
extra["stderr_color"] = json.dumps(msg_color.stderr.value)
|
||||
if msg_color and msg_color.stdout:
|
||||
extra["stdout_color"] = json.dumps(msg_color.stdout.value)
|
||||
|
||||
#
|
||||
# Process stdout
|
||||
#
|
||||
@@ -92,7 +108,7 @@ def handle_io(
|
||||
if ret and log in [Log.STDOUT, Log.BOTH]:
|
||||
lines = ret.decode("utf-8", "replace").rstrip("\n").split("\n")
|
||||
for line in lines:
|
||||
cmdlog.info(line, extra={"command_prefix": prefix})
|
||||
cmdlog.info(line, extra=extra)
|
||||
if ret and stdout:
|
||||
stdout.write(ret)
|
||||
stdout.flush()
|
||||
@@ -106,7 +122,7 @@ def handle_io(
|
||||
if ret and log in [Log.STDERR, Log.BOTH]:
|
||||
lines = ret.decode("utf-8", "replace").rstrip("\n").split("\n")
|
||||
for line in lines:
|
||||
cmdlog.error(line, extra={"command_prefix": prefix})
|
||||
cmdlog.error(line, extra=extra)
|
||||
if ret and stderr:
|
||||
stderr.write(ret)
|
||||
stderr.flush()
|
||||
@@ -224,6 +240,7 @@ def run(
|
||||
log: Log = Log.STDERR,
|
||||
logger: logging.Logger = cmdlog,
|
||||
prefix: str | None = None,
|
||||
msg_color: MsgColor | None = None,
|
||||
check: bool = True,
|
||||
error_msg: str | None = None,
|
||||
needs_user_terminal: bool = False,
|
||||
@@ -234,7 +251,7 @@ def run(
|
||||
cwd = Path.cwd()
|
||||
|
||||
if prefix is None:
|
||||
prefix = "localhost"
|
||||
prefix = "$"
|
||||
|
||||
if input:
|
||||
if any(not ch.isprintable() for ch in input.decode("ascii", "replace")):
|
||||
@@ -273,6 +290,7 @@ def run(
|
||||
process,
|
||||
log,
|
||||
prefix=prefix,
|
||||
msg_color=msg_color,
|
||||
cmdlog=logger,
|
||||
timeout=timeout,
|
||||
input_bytes=input,
|
||||
|
||||
172
pkgs/clan-cli/clan_cli/colors.py
Normal file
172
pkgs/clan-cli/clan_cli/colors.py
Normal file
@@ -0,0 +1,172 @@
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
ANSI16_MARKER = 300
|
||||
ANSI256_MARKER = 301
|
||||
DEFAULT_MARKER = 302
|
||||
|
||||
|
||||
class RgbColor(Enum):
|
||||
"""
|
||||
A subset of CSS colors with RGB values that work well in Dark and Light mode.
|
||||
"""
|
||||
|
||||
TEAL = (0, 130, 128)
|
||||
OLIVEDRAB = (113, 122, 57)
|
||||
CHOCOLATE = (198, 77, 45)
|
||||
MEDIUMORCHID = (170, 74, 198)
|
||||
SEAGREEN = (49, 128, 125)
|
||||
SLATEBLUE = (127, 97, 206)
|
||||
DARKCYAN = (25, 128, 145)
|
||||
STEELBLUE = (51, 118, 193)
|
||||
MEDIUMPURPLE = (149, 82, 220)
|
||||
INDIANRED = (199, 74, 77)
|
||||
FORESTGREEN = (42, 134, 44)
|
||||
SLATEGRAY = (75, 123, 140)
|
||||
LIGHTSLATEGRAY = (125, 106, 170)
|
||||
MEDIUMSLATEBLUE = (100, 92, 255)
|
||||
GRAY = (144, 102, 146)
|
||||
DARKORCHID = (188, 36, 228)
|
||||
SIENNA = (178, 94, 30)
|
||||
OLIVE = (133, 116, 33)
|
||||
PALEVIOLETRED = (186, 77, 136)
|
||||
DARKGOLDENROD = (180, 93, 0)
|
||||
MEDIUMVIOLETRED = (212, 2, 184)
|
||||
BLUEVIOLET = (165, 50, 255)
|
||||
DIMGRAY = (95, 122, 115)
|
||||
DARKVIOLET = (202, 12, 211)
|
||||
DODGERBLUE = (0, 106, 255)
|
||||
DARKOLIVEGREEN = (88, 128, 41)
|
||||
|
||||
@classmethod
|
||||
def get_by_name(cls: Any, name: str) -> "RgbColor":
|
||||
try:
|
||||
return cls[name.upper()]
|
||||
except KeyError as ex:
|
||||
msg = f"Color '{name}' is not a valid color name"
|
||||
raise ValueError(msg) from ex
|
||||
|
||||
@classmethod
|
||||
def list_values(cls: Any) -> list[tuple[int, int, int]]:
|
||||
return [color.value for color in cls]
|
||||
|
||||
|
||||
class AnsiColor(Enum):
|
||||
"""Enum representing ANSI colors."""
|
||||
|
||||
# Standard 16-bit colors
|
||||
BLACK = (ANSI16_MARKER, 0, 0)
|
||||
RED = (ANSI16_MARKER, 1, 0)
|
||||
GREEN = (ANSI16_MARKER, 2, 0)
|
||||
YELLOW = (ANSI16_MARKER, 3, 0)
|
||||
BLUE = (ANSI16_MARKER, 4, 0)
|
||||
MAGENTA = (ANSI16_MARKER, 5, 0)
|
||||
CYAN = (ANSI16_MARKER, 6, 0)
|
||||
WHITE = (ANSI16_MARKER, 7, 0)
|
||||
DEFAULT = (DEFAULT_MARKER, 9, 0)
|
||||
|
||||
# Subset of 256-bit colors
|
||||
BRIGHT_BLACK = (ANSI256_MARKER, 8, 0)
|
||||
BRIGHT_RED = (ANSI256_MARKER, 9, 0)
|
||||
BRIGHT_GREEN = (ANSI256_MARKER, 10, 0)
|
||||
BRIGHT_YELLOW = (ANSI256_MARKER, 11, 0)
|
||||
BRIGHT_BLUE = (ANSI256_MARKER, 12, 0)
|
||||
BRIGHT_MAGENTA = (ANSI256_MARKER, 13, 0)
|
||||
BRIGHT_CYAN = (ANSI256_MARKER, 14, 0)
|
||||
BRIGHT_WHITE = (ANSI256_MARKER, 15, 0)
|
||||
|
||||
|
||||
Color = AnsiColor | RgbColor
|
||||
|
||||
|
||||
class ColorType(Enum):
|
||||
BG = 40
|
||||
FG = 30
|
||||
|
||||
|
||||
def _join(*values: int | str) -> str:
|
||||
"""
|
||||
Join a series of values with semicolons. The values
|
||||
are either integers or strings, so stringify each for
|
||||
good measure. Worth breaking out as its own function
|
||||
because semicolon-joined lists are core to ANSI coding.
|
||||
"""
|
||||
return ";".join(str(v) for v in values)
|
||||
|
||||
|
||||
def color_code(spec: tuple[int, int, int], base: ColorType) -> str:
|
||||
"""
|
||||
Workhorse of encoding a color. Give preference to named colors from
|
||||
ANSI, then to specific numeric or tuple specs. If those don't work,
|
||||
try looking up CSS color names or parsing CSS color specifications
|
||||
(hex or rgb).
|
||||
"""
|
||||
red = spec[0]
|
||||
green = spec[1]
|
||||
blue = spec[2]
|
||||
val = None
|
||||
if red == ANSI16_MARKER:
|
||||
val = _join(base.value + green)
|
||||
elif red == ANSI256_MARKER:
|
||||
val = _join(base.value + 8, 5, green)
|
||||
elif red == DEFAULT_MARKER:
|
||||
val = _join(base.value + 9)
|
||||
elif 0 <= red <= 255 and 0 <= green <= 255 and 0 <= blue <= 255:
|
||||
val = _join(base.value + 8, 2, red, green, blue)
|
||||
else:
|
||||
msg = f"Invalid color specification: {spec}"
|
||||
raise ValueError(msg)
|
||||
|
||||
return val
|
||||
|
||||
|
||||
def color_by_tuple(
|
||||
message: str,
|
||||
fg: tuple[int, int, int] = AnsiColor.DEFAULT.value,
|
||||
bg: tuple[int, int, int] = AnsiColor.DEFAULT.value,
|
||||
) -> str:
|
||||
codes: list[str] = []
|
||||
if fg[0] != DEFAULT_MARKER:
|
||||
codes.append(color_code(fg, ColorType.FG))
|
||||
|
||||
if bg[0] != DEFAULT_MARKER:
|
||||
codes.append(color_code(bg, ColorType.BG))
|
||||
|
||||
if codes:
|
||||
template = "\x1b[{0}m{1}\x1b[0m"
|
||||
return template.format(_join(*codes), message)
|
||||
return message
|
||||
|
||||
|
||||
def color(
|
||||
message: str,
|
||||
fg: Color = AnsiColor.DEFAULT,
|
||||
bg: Color = AnsiColor.DEFAULT,
|
||||
) -> str:
|
||||
"""
|
||||
Add ANSI colors and styles to a string.
|
||||
"""
|
||||
return color_by_tuple(message, fg.value, bg.value)
|
||||
|
||||
|
||||
def strip_color(message: str) -> str:
|
||||
"""
|
||||
Remove ANSI color/style sequences from a string. The set of all possible
|
||||
ANSI sequences is large, so does not try to strip every possible one. But
|
||||
does strip some outliers seen not just in text generated by this module, but
|
||||
by other ANSI colorizers in the wild. Those include `\x1b[K` (aka EL or
|
||||
erase to end of line) and `\x1b[m`, a terse version of the more common
|
||||
`\x1b[0m`.
|
||||
"""
|
||||
return re.sub("\x1b\\[(K|.*?m)", "", message)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("====ANSI Colors====")
|
||||
for _, value in AnsiColor.__members__.items():
|
||||
print(color_by_tuple(f"{value}", fg=value.value))
|
||||
|
||||
print("====CSS Colors====")
|
||||
for _, cs_value in RgbColor.__members__.items():
|
||||
print(color_by_tuple(f"{cs_value}", fg=cs_value.value))
|
||||
@@ -1,2 +0,0 @@
|
||||
from .colors import * # noqa
|
||||
from .csscolors import * # noqa
|
||||
@@ -1,180 +0,0 @@
|
||||
# Copyright (c) 2012 Giorgos Verigakis <verigak@gmail.com>
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import re
|
||||
from functools import partial
|
||||
|
||||
from .csscolors import parse_rgb
|
||||
|
||||
# ANSI color names. There is also a "default"
|
||||
COLORS: tuple[str, ...] = (
|
||||
"black",
|
||||
"red",
|
||||
"green",
|
||||
"yellow",
|
||||
"blue",
|
||||
"magenta",
|
||||
"cyan",
|
||||
"white",
|
||||
)
|
||||
|
||||
# ANSI style names
|
||||
STYLES: tuple[str, ...] = (
|
||||
"none",
|
||||
"bold",
|
||||
"faint",
|
||||
"italic",
|
||||
"underline",
|
||||
"blink",
|
||||
"blink2",
|
||||
"negative",
|
||||
"concealed",
|
||||
"crossed",
|
||||
)
|
||||
|
||||
|
||||
def is_string(obj: str | bytes) -> bool:
|
||||
"""
|
||||
Is the given object a string?
|
||||
"""
|
||||
return isinstance(obj, str)
|
||||
|
||||
|
||||
def _join(*values: int | str) -> str:
|
||||
"""
|
||||
Join a series of values with semicolons. The values
|
||||
are either integers or strings, so stringify each for
|
||||
good measure. Worth breaking out as its own function
|
||||
because semicolon-joined lists are core to ANSI coding.
|
||||
"""
|
||||
return ";".join(str(v) for v in values)
|
||||
|
||||
|
||||
def color_code(spec: str | int | tuple[int, int, int] | list, base: int) -> str:
|
||||
"""
|
||||
Workhorse of encoding a color. Give preference to named colors from
|
||||
ANSI, then to specific numeric or tuple specs. If those don't work,
|
||||
try looking up CSS color names or parsing CSS color specifications
|
||||
(hex or rgb).
|
||||
|
||||
:param str|int|tuple|list spec: Unparsed color specification
|
||||
:param int base: Either 30 or 40, signifying the base value
|
||||
for color encoding (foreground and background respectively).
|
||||
Low values are added directly to the base. Higher values use `
|
||||
base + 8` (i.e. 38 or 48) then extended codes.
|
||||
:returns: Discovered ANSI color encoding.
|
||||
:rtype: str
|
||||
:raises: ValueError if cannot parse the color spec.
|
||||
"""
|
||||
if isinstance(spec, str | bytes):
|
||||
spec = spec.strip().lower()
|
||||
|
||||
if spec == "default":
|
||||
return _join(base + 9)
|
||||
if spec in COLORS:
|
||||
return _join(base + COLORS.index(spec))
|
||||
if isinstance(spec, int) and 0 <= spec <= 255:
|
||||
return _join(base + 8, 5, spec)
|
||||
if isinstance(spec, tuple | list):
|
||||
return _join(base + 8, 2, _join(*spec))
|
||||
rgb = parse_rgb(str(spec))
|
||||
# parse_rgb raises ValueError if cannot parse spec
|
||||
# or returns an rgb tuple if it can
|
||||
return _join(base + 8, 2, _join(*rgb))
|
||||
|
||||
|
||||
def color(
|
||||
s: str | None = None,
|
||||
fg: str | int | tuple[int, int, int] | None = None,
|
||||
bg: str | int | tuple[int, int, int] | None = None,
|
||||
style: str | None = None,
|
||||
reset: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Add ANSI colors and styles to a string.
|
||||
|
||||
:param str s: String to format.
|
||||
:param str|int|tuple fg: Foreground color specification.
|
||||
:param str|int|tuple bg: Background color specification.
|
||||
:param str: Style names, separated by '+'
|
||||
:returns: Formatted string.
|
||||
:rtype: str (or unicode in Python 2, if s is unicode)
|
||||
"""
|
||||
codes: list[int | str] = []
|
||||
|
||||
if fg:
|
||||
codes.append(color_code(fg, 30))
|
||||
if bg:
|
||||
codes.append(color_code(bg, 40))
|
||||
if style:
|
||||
for style_part in style.split("+"):
|
||||
if style_part in STYLES:
|
||||
codes.append(STYLES.index(style_part))
|
||||
else:
|
||||
msg = f'Invalid style "{style_part}"'
|
||||
raise ValueError(msg)
|
||||
|
||||
if not s:
|
||||
s = ""
|
||||
|
||||
if codes:
|
||||
if reset:
|
||||
template = "\x1b[{0}m{1}\x1b[0m"
|
||||
else:
|
||||
template = "\x1b[{0}m{1}"
|
||||
return template.format(_join(*codes), s)
|
||||
return s
|
||||
|
||||
|
||||
def strip_color(s: str) -> str:
|
||||
"""
|
||||
Remove ANSI color/style sequences from a string. The set of all possible
|
||||
ANSI sequences is large, so does not try to strip every possible one. But
|
||||
does strip some outliers seen not just in text generated by this module, but
|
||||
by other ANSI colorizers in the wild. Those include `\x1b[K` (aka EL or
|
||||
erase to end of line) and `\x1b[m`, a terse version of the more common
|
||||
`\x1b[0m`.
|
||||
"""
|
||||
return re.sub("\x1b\\[(K|.*?m)", "", s)
|
||||
|
||||
|
||||
def ansilen(s: str) -> int:
|
||||
"""
|
||||
Given a string with embedded ANSI codes, what would its
|
||||
length be without those codes?
|
||||
"""
|
||||
return len(strip_color(s))
|
||||
|
||||
|
||||
# Foreground color shortcuts
|
||||
black = partial(color, fg="black")
|
||||
red = partial(color, fg="red")
|
||||
green = partial(color, fg="green")
|
||||
yellow = partial(color, fg="yellow")
|
||||
blue = partial(color, fg="blue")
|
||||
magenta = partial(color, fg="magenta")
|
||||
cyan = partial(color, fg="cyan")
|
||||
white = partial(color, fg="white")
|
||||
|
||||
# Style shortcuts
|
||||
bold = partial(color, style="bold")
|
||||
none = partial(color, style="none")
|
||||
faint = partial(color, style="faint")
|
||||
italic = partial(color, style="italic")
|
||||
underline = partial(color, style="underline")
|
||||
blink = partial(color, style="blink")
|
||||
blink2 = partial(color, style="blink2")
|
||||
negative = partial(color, style="negative")
|
||||
concealed = partial(color, style="concealed")
|
||||
crossed = partial(color, style="crossed")
|
||||
@@ -1,183 +0,0 @@
|
||||
"""
|
||||
Map of CSS color names to RGB integer values.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
css_colors: dict[str, tuple[int, int, int]] = {
|
||||
"aliceblue": (240, 248, 255),
|
||||
"antiquewhite": (250, 235, 215),
|
||||
"aqua": (0, 255, 255),
|
||||
"aquamarine": (127, 255, 212),
|
||||
"azure": (240, 255, 255),
|
||||
"beige": (245, 245, 220),
|
||||
"bisque": (255, 228, 196),
|
||||
"black": (0, 0, 0),
|
||||
"blanchedalmond": (255, 235, 205),
|
||||
"blue": (0, 0, 255),
|
||||
"blueviolet": (138, 43, 226),
|
||||
"brown": (165, 42, 42),
|
||||
"burlywood": (222, 184, 135),
|
||||
"cadetblue": (95, 158, 160),
|
||||
"chartreuse": (127, 255, 0),
|
||||
"chocolate": (210, 105, 30),
|
||||
"coral": (255, 127, 80),
|
||||
"cornflowerblue": (100, 149, 237),
|
||||
"cornsilk": (255, 248, 220),
|
||||
"crimson": (220, 20, 60),
|
||||
"cyan": (0, 255, 255),
|
||||
"darkblue": (0, 0, 139),
|
||||
"darkcyan": (0, 139, 139),
|
||||
"darkgoldenrod": (184, 134, 11),
|
||||
"darkgray": (169, 169, 169),
|
||||
"darkgreen": (0, 100, 0),
|
||||
"darkgrey": (169, 169, 169),
|
||||
"darkkhaki": (189, 183, 107),
|
||||
"darkmagenta": (139, 0, 139),
|
||||
"darkolivegreen": (85, 107, 47),
|
||||
"darkorange": (255, 140, 0),
|
||||
"darkorchid": (153, 50, 204),
|
||||
"darkred": (139, 0, 0),
|
||||
"darksalmon": (233, 150, 122),
|
||||
"darkseagreen": (143, 188, 143),
|
||||
"darkslateblue": (72, 61, 139),
|
||||
"darkslategray": (47, 79, 79),
|
||||
"darkslategrey": (47, 79, 79),
|
||||
"darkturquoise": (0, 206, 209),
|
||||
"darkviolet": (148, 0, 211),
|
||||
"deeppink": (255, 20, 147),
|
||||
"deepskyblue": (0, 191, 255),
|
||||
"dimgray": (105, 105, 105),
|
||||
"dimgrey": (105, 105, 105),
|
||||
"dodgerblue": (30, 144, 255),
|
||||
"firebrick": (178, 34, 34),
|
||||
"floralwhite": (255, 250, 240),
|
||||
"forestgreen": (34, 139, 34),
|
||||
"fuchsia": (255, 0, 255),
|
||||
"gainsboro": (220, 220, 220),
|
||||
"ghostwhite": (248, 248, 255),
|
||||
"gold": (255, 215, 0),
|
||||
"goldenrod": (218, 165, 32),
|
||||
"gray": (128, 128, 128),
|
||||
"green": (0, 128, 0),
|
||||
"greenyellow": (173, 255, 47),
|
||||
"grey": (128, 128, 128),
|
||||
"honeydew": (240, 255, 240),
|
||||
"hotpink": (255, 105, 180),
|
||||
"indianred": (205, 92, 92),
|
||||
"indigo": (75, 0, 130),
|
||||
"ivory": (255, 255, 240),
|
||||
"khaki": (240, 230, 140),
|
||||
"lavender": (230, 230, 250),
|
||||
"lavenderblush": (255, 240, 245),
|
||||
"lawngreen": (124, 252, 0),
|
||||
"lemonchiffon": (255, 250, 205),
|
||||
"lightblue": (173, 216, 230),
|
||||
"lightcoral": (240, 128, 128),
|
||||
"lightcyan": (224, 255, 255),
|
||||
"lightgoldenrodyellow": (250, 250, 210),
|
||||
"lightgray": (211, 211, 211),
|
||||
"lightgreen": (144, 238, 144),
|
||||
"lightgrey": (211, 211, 211),
|
||||
"lightpink": (255, 182, 193),
|
||||
"lightsalmon": (255, 160, 122),
|
||||
"lightseagreen": (32, 178, 170),
|
||||
"lightskyblue": (135, 206, 250),
|
||||
"lightslategray": (119, 136, 153),
|
||||
"lightslategrey": (119, 136, 153),
|
||||
"lightsteelblue": (176, 196, 222),
|
||||
"lightyellow": (255, 255, 224),
|
||||
"lime": (0, 255, 0),
|
||||
"limegreen": (50, 205, 50),
|
||||
"linen": (250, 240, 230),
|
||||
"magenta": (255, 0, 255),
|
||||
"maroon": (128, 0, 0),
|
||||
"mediumaquamarine": (102, 205, 170),
|
||||
"mediumblue": (0, 0, 205),
|
||||
"mediumorchid": (186, 85, 211),
|
||||
"mediumpurple": (147, 112, 219),
|
||||
"mediumseagreen": (60, 179, 113),
|
||||
"mediumslateblue": (123, 104, 238),
|
||||
"mediumspringgreen": (0, 250, 154),
|
||||
"mediumturquoise": (72, 209, 204),
|
||||
"mediumvioletred": (199, 21, 133),
|
||||
"midnightblue": (25, 25, 112),
|
||||
"mintcream": (245, 255, 250),
|
||||
"mistyrose": (255, 228, 225),
|
||||
"moccasin": (255, 228, 181),
|
||||
"navajowhite": (255, 222, 173),
|
||||
"navy": (0, 0, 128),
|
||||
"oldlace": (253, 245, 230),
|
||||
"olive": (128, 128, 0),
|
||||
"olivedrab": (107, 142, 35),
|
||||
"orange": (255, 165, 0),
|
||||
"orangered": (255, 69, 0),
|
||||
"orchid": (218, 112, 214),
|
||||
"palegoldenrod": (238, 232, 170),
|
||||
"palegreen": (152, 251, 152),
|
||||
"paleturquoise": (175, 238, 238),
|
||||
"palevioletred": (219, 112, 147),
|
||||
"papayawhip": (255, 239, 213),
|
||||
"peachpuff": (255, 218, 185),
|
||||
"peru": (205, 133, 63),
|
||||
"pink": (255, 192, 203),
|
||||
"plum": (221, 160, 221),
|
||||
"powderblue": (176, 224, 230),
|
||||
"purple": (128, 0, 128),
|
||||
"rebeccapurple": (102, 51, 153),
|
||||
"red": (255, 0, 0),
|
||||
"rosybrown": (188, 143, 143),
|
||||
"royalblue": (65, 105, 225),
|
||||
"saddlebrown": (139, 69, 19),
|
||||
"salmon": (250, 128, 114),
|
||||
"sandybrown": (244, 164, 96),
|
||||
"seagreen": (46, 139, 87),
|
||||
"seashell": (255, 245, 238),
|
||||
"sienna": (160, 82, 45),
|
||||
"silver": (192, 192, 192),
|
||||
"skyblue": (135, 206, 235),
|
||||
"slateblue": (106, 90, 205),
|
||||
"slategray": (112, 128, 144),
|
||||
"slategrey": (112, 128, 144),
|
||||
"snow": (255, 250, 250),
|
||||
"springgreen": (0, 255, 127),
|
||||
"steelblue": (70, 130, 180),
|
||||
"tan": (210, 180, 140),
|
||||
"teal": (0, 128, 128),
|
||||
"thistle": (216, 191, 216),
|
||||
"tomato": (255, 99, 71),
|
||||
"turquoise": (64, 224, 208),
|
||||
"violet": (238, 130, 238),
|
||||
"wheat": (245, 222, 179),
|
||||
"white": (255, 255, 255),
|
||||
"whitesmoke": (245, 245, 245),
|
||||
"yellow": (255, 255, 0),
|
||||
"yellowgreen": (154, 205, 50),
|
||||
}
|
||||
|
||||
|
||||
def parse_rgb(s: str) -> tuple[int, ...]:
|
||||
s = s.strip().replace(" ", "").lower()
|
||||
# simple lookup
|
||||
rgb = css_colors.get(s)
|
||||
if rgb is not None:
|
||||
return rgb
|
||||
|
||||
# 6-digit hex
|
||||
match = re.match("#([a-f0-9]{6})$", s)
|
||||
if match:
|
||||
core = match.group(1)
|
||||
return tuple(int(core[i : i + 2], 16) for i in range(0, 6, 2))
|
||||
|
||||
# 3-digit hex
|
||||
match = re.match("#([a-f0-9]{3})$", s)
|
||||
if match:
|
||||
return tuple(int(c * 2, 16) for c in match.group(1))
|
||||
|
||||
# rgb(x,y,z)
|
||||
match = re.match(r"rgb\((\d+,\d+,\d+)\)", s)
|
||||
if match:
|
||||
return tuple(int(v) for v in match.group(1).split(","))
|
||||
|
||||
msg = f"Could not parse color '{s}'"
|
||||
raise ValueError(msg)
|
||||
@@ -1,11 +1,12 @@
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.colors import color, css_colors
|
||||
from clan_cli.colors import AnsiColor, RgbColor, color_by_tuple
|
||||
|
||||
# https://no-color.org
|
||||
DISABLE_COLOR = not sys.stderr.isatty() or os.environ.get("NO_COLOR", "") != ""
|
||||
@@ -25,7 +26,9 @@ class PrefixFormatter(logging.Formatter):
|
||||
print errors in red and warnings in yellow
|
||||
"""
|
||||
|
||||
def __init__(self, trace_prints: bool = False, default_prefix: str = "") -> None:
|
||||
def __init__(
|
||||
self, trace_prints: bool = False, default_prefix: str | None = None
|
||||
) -> None:
|
||||
self.default_prefix = default_prefix
|
||||
self.trace_prints = trace_prints
|
||||
|
||||
@@ -37,22 +40,35 @@ class PrefixFormatter(logging.Formatter):
|
||||
filepath = _get_filepath(record)
|
||||
|
||||
if record.levelno == logging.DEBUG:
|
||||
color_str = "blue"
|
||||
ansi_color_str = getattr(record, "stdout_color", None)
|
||||
if ansi_color_str is not None:
|
||||
ansi_color = json.loads(ansi_color_str)
|
||||
else:
|
||||
ansi_color = AnsiColor.BLUE.value
|
||||
elif record.levelno == logging.ERROR:
|
||||
color_str = "red"
|
||||
ansi_color_str = getattr(record, "stderr_color", None)
|
||||
if ansi_color_str is not None:
|
||||
ansi_color = json.loads(ansi_color_str)
|
||||
else:
|
||||
ansi_color = AnsiColor.RED.value
|
||||
elif record.levelno == logging.WARNING:
|
||||
color_str = "yellow"
|
||||
ansi_color = AnsiColor.YELLOW.value
|
||||
else:
|
||||
color_str = None
|
||||
ansi_color = AnsiColor.DEFAULT.value
|
||||
|
||||
command_prefix = getattr(record, "command_prefix", self.default_prefix)
|
||||
|
||||
if not DISABLE_COLOR:
|
||||
if DISABLE_COLOR:
|
||||
if command_prefix:
|
||||
format_str = f"[{command_prefix}] %(message)s"
|
||||
else:
|
||||
format_str = "%(message)s"
|
||||
elif command_prefix:
|
||||
prefix_color = self.hostname_colorcode(command_prefix)
|
||||
format_str = color(f"[{command_prefix}]", fg=prefix_color)
|
||||
format_str += color(" %(message)s", fg=color_str)
|
||||
format_str = color_by_tuple(f"[{command_prefix}]", fg=prefix_color)
|
||||
format_str += color_by_tuple(" %(message)s", fg=ansi_color)
|
||||
else:
|
||||
format_str = f"[{command_prefix}] %(message)s"
|
||||
format_str = color_by_tuple("%(message)s", fg=ansi_color)
|
||||
|
||||
if self.trace_prints:
|
||||
format_str += f"\nSource: {filepath}:%(lineno)d::%(funcName)s\n"
|
||||
@@ -60,13 +76,14 @@ class PrefixFormatter(logging.Formatter):
|
||||
return logging.Formatter(format_str).format(record)
|
||||
|
||||
def hostname_colorcode(self, hostname: str) -> tuple[int, int, int]:
|
||||
colorcodes = RgbColor.list_values()
|
||||
try:
|
||||
index = self.hostnames.index(hostname)
|
||||
except ValueError:
|
||||
self.hostnames += [hostname]
|
||||
index = self.hostnames.index(hostname)
|
||||
coloroffset = (index + self.hostname_color_offset) % len(css_colors)
|
||||
colorcode = list(css_colors.values())[coloroffset]
|
||||
coloroffset = (index + self.hostname_color_offset) % len(colorcodes)
|
||||
colorcode = colorcodes[coloroffset]
|
||||
|
||||
return colorcode
|
||||
|
||||
@@ -116,7 +133,7 @@ def get_callers(start: int = 2, end: int = 2) -> list[str]:
|
||||
return callers
|
||||
|
||||
|
||||
def print_trace(msg: str, logger: logging.Logger, prefix: str) -> None:
|
||||
def print_trace(msg: str, logger: logging.Logger, prefix: str | None) -> None:
|
||||
trace_depth = int(os.environ.get("TRACE_DEPTH", "0"))
|
||||
callers = get_callers(3, 4 + trace_depth)
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import sys
|
||||
|
||||
from clan_cli.api import API
|
||||
from clan_cli.clan_uri import FlakeId
|
||||
from clan_cli.cmd import run
|
||||
from clan_cli.cmd import MsgColor, run
|
||||
from clan_cli.colors import AnsiColor
|
||||
from clan_cli.completions import (
|
||||
add_dynamic_completer,
|
||||
complete_machines,
|
||||
@@ -147,17 +148,26 @@ def deploy_machine(machines: MachineGroup) -> None:
|
||||
test_cmd.extend(["--target-host", target_host.target])
|
||||
|
||||
env = host.nix_ssh_env(None)
|
||||
ret = host.run(switch_cmd, extra_env=env, check=False)
|
||||
ret = host.run(
|
||||
switch_cmd,
|
||||
extra_env=env,
|
||||
check=False,
|
||||
msg_color=MsgColor(stderr=AnsiColor.DEFAULT),
|
||||
)
|
||||
|
||||
# if the machine is mobile, we retry to deploy with the quirk method
|
||||
is_mobile = machine.deployment.get("nixosMobileWorkaround", False)
|
||||
if is_mobile and ret.returncode != 0:
|
||||
log.info("Mobile machine detected, applying quirk deployment method")
|
||||
ret = host.run(test_cmd, extra_env=env)
|
||||
ret = host.run(
|
||||
test_cmd, extra_env=env, msg_color=MsgColor(stderr=AnsiColor.DEFAULT)
|
||||
)
|
||||
|
||||
# retry nixos-rebuild switch if the first attempt failed
|
||||
elif ret.returncode != 0:
|
||||
ret = host.run(switch_cmd, extra_env=env)
|
||||
ret = host.run(
|
||||
switch_cmd, extra_env=env, msg_color=MsgColor(stderr=AnsiColor.DEFAULT)
|
||||
)
|
||||
|
||||
if len(machines.group.hosts) > 1:
|
||||
machines.run_function(deploy)
|
||||
|
||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
from shlex import quote
|
||||
from typing import IO, Any
|
||||
|
||||
from clan_cli.cmd import Log
|
||||
from clan_cli.cmd import Log, MsgColor
|
||||
from clan_cli.cmd import run as local_run
|
||||
from clan_cli.ssh.host_key import HostKeyCheck
|
||||
|
||||
@@ -68,6 +68,7 @@ class Host:
|
||||
check: bool = True,
|
||||
error_msg: str | None = None,
|
||||
needs_user_terminal: bool = False,
|
||||
msg_color: MsgColor | None = None,
|
||||
shell: bool = False,
|
||||
timeout: float = math.inf,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
@@ -85,6 +86,7 @@ class Host:
|
||||
logger=cmdlog,
|
||||
check=check,
|
||||
error_msg=error_msg,
|
||||
msg_color=msg_color,
|
||||
needs_user_terminal=needs_user_terminal,
|
||||
)
|
||||
return subprocess.CompletedProcess(
|
||||
@@ -141,6 +143,7 @@ class Host:
|
||||
timeout: float = math.inf,
|
||||
verbose_ssh: bool = False,
|
||||
tty: bool = False,
|
||||
msg_color: MsgColor | None = None,
|
||||
shell: bool = False,
|
||||
log: Log = Log.BOTH,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
@@ -192,6 +195,7 @@ class Host:
|
||||
cwd=cwd,
|
||||
check=check,
|
||||
timeout=timeout,
|
||||
msg_color=msg_color,
|
||||
needs_user_terminal=True, # ssh asks for a password
|
||||
)
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class HostGroup:
|
||||
errors += 1
|
||||
if errors > 0:
|
||||
msg = f"{errors} hosts failed with an error. Check the logs above"
|
||||
raise ClanError(msg)
|
||||
raise ClanError(msg) from e
|
||||
|
||||
def _run(
|
||||
self,
|
||||
|
||||
@@ -15,6 +15,7 @@ let
|
||||
rope
|
||||
setuptools
|
||||
wheel
|
||||
webcolors
|
||||
pip
|
||||
]);
|
||||
in
|
||||
|
||||
Reference in New Issue
Block a user