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 contextlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
@@ -11,10 +12,12 @@ import timeit
|
|||||||
import weakref
|
import weakref
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from contextlib import ExitStack, contextmanager
|
from contextlib import ExitStack, contextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO, Any
|
from typing import IO, Any
|
||||||
|
|
||||||
|
from clan_cli.colors import Color
|
||||||
from clan_cli.custom_logger import print_trace
|
from clan_cli.custom_logger import print_trace
|
||||||
from clan_cli.errors import ClanCmdError, ClanError, CmdOut, indent_command
|
from clan_cli.errors import ClanCmdError, ClanError, CmdOut, indent_command
|
||||||
|
|
||||||
@@ -43,16 +46,23 @@ class Log(Enum):
|
|||||||
NONE = 4
|
NONE = 4
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MsgColor:
|
||||||
|
stderr: Color | None = None
|
||||||
|
stdout: Color | None = None
|
||||||
|
|
||||||
|
|
||||||
def handle_io(
|
def handle_io(
|
||||||
process: subprocess.Popen,
|
process: subprocess.Popen,
|
||||||
log: Log,
|
log: Log,
|
||||||
cmdlog: logging.Logger,
|
cmdlog: logging.Logger,
|
||||||
prefix: str,
|
|
||||||
*,
|
*,
|
||||||
|
prefix: str | None,
|
||||||
input_bytes: bytes | None,
|
input_bytes: bytes | None,
|
||||||
stdout: IO[bytes] | None,
|
stdout: IO[bytes] | None,
|
||||||
stderr: IO[bytes] | None,
|
stderr: IO[bytes] | None,
|
||||||
timeout: float = math.inf,
|
timeout: float = math.inf,
|
||||||
|
msg_color: MsgColor | None = None,
|
||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
rlist = [process.stdout, process.stderr]
|
rlist = [process.stdout, process.stderr]
|
||||||
wlist = [process.stdin] if input_bytes is not None else []
|
wlist = [process.stdin] if input_bytes is not None else []
|
||||||
@@ -85,6 +95,12 @@ def handle_io(
|
|||||||
rlist.remove(fd)
|
rlist.remove(fd)
|
||||||
return b""
|
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
|
# Process stdout
|
||||||
#
|
#
|
||||||
@@ -92,7 +108,7 @@ def handle_io(
|
|||||||
if ret and log in [Log.STDOUT, Log.BOTH]:
|
if ret and log in [Log.STDOUT, Log.BOTH]:
|
||||||
lines = ret.decode("utf-8", "replace").rstrip("\n").split("\n")
|
lines = ret.decode("utf-8", "replace").rstrip("\n").split("\n")
|
||||||
for line in lines:
|
for line in lines:
|
||||||
cmdlog.info(line, extra={"command_prefix": prefix})
|
cmdlog.info(line, extra=extra)
|
||||||
if ret and stdout:
|
if ret and stdout:
|
||||||
stdout.write(ret)
|
stdout.write(ret)
|
||||||
stdout.flush()
|
stdout.flush()
|
||||||
@@ -106,7 +122,7 @@ def handle_io(
|
|||||||
if ret and log in [Log.STDERR, Log.BOTH]:
|
if ret and log in [Log.STDERR, Log.BOTH]:
|
||||||
lines = ret.decode("utf-8", "replace").rstrip("\n").split("\n")
|
lines = ret.decode("utf-8", "replace").rstrip("\n").split("\n")
|
||||||
for line in lines:
|
for line in lines:
|
||||||
cmdlog.error(line, extra={"command_prefix": prefix})
|
cmdlog.error(line, extra=extra)
|
||||||
if ret and stderr:
|
if ret and stderr:
|
||||||
stderr.write(ret)
|
stderr.write(ret)
|
||||||
stderr.flush()
|
stderr.flush()
|
||||||
@@ -224,6 +240,7 @@ def run(
|
|||||||
log: Log = Log.STDERR,
|
log: Log = Log.STDERR,
|
||||||
logger: logging.Logger = cmdlog,
|
logger: logging.Logger = cmdlog,
|
||||||
prefix: str | None = None,
|
prefix: str | None = None,
|
||||||
|
msg_color: MsgColor | None = None,
|
||||||
check: bool = True,
|
check: bool = True,
|
||||||
error_msg: str | None = None,
|
error_msg: str | None = None,
|
||||||
needs_user_terminal: bool = False,
|
needs_user_terminal: bool = False,
|
||||||
@@ -234,7 +251,7 @@ def run(
|
|||||||
cwd = Path.cwd()
|
cwd = Path.cwd()
|
||||||
|
|
||||||
if prefix is None:
|
if prefix is None:
|
||||||
prefix = "localhost"
|
prefix = "$"
|
||||||
|
|
||||||
if input:
|
if input:
|
||||||
if any(not ch.isprintable() for ch in input.decode("ascii", "replace")):
|
if any(not ch.isprintable() for ch in input.decode("ascii", "replace")):
|
||||||
@@ -273,6 +290,7 @@ def run(
|
|||||||
process,
|
process,
|
||||||
log,
|
log,
|
||||||
prefix=prefix,
|
prefix=prefix,
|
||||||
|
msg_color=msg_color,
|
||||||
cmdlog=logger,
|
cmdlog=logger,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
input_bytes=input,
|
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 inspect
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
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
|
# https://no-color.org
|
||||||
DISABLE_COLOR = not sys.stderr.isatty() or os.environ.get("NO_COLOR", "") != ""
|
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
|
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.default_prefix = default_prefix
|
||||||
self.trace_prints = trace_prints
|
self.trace_prints = trace_prints
|
||||||
|
|
||||||
@@ -37,22 +40,35 @@ class PrefixFormatter(logging.Formatter):
|
|||||||
filepath = _get_filepath(record)
|
filepath = _get_filepath(record)
|
||||||
|
|
||||||
if record.levelno == logging.DEBUG:
|
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:
|
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:
|
elif record.levelno == logging.WARNING:
|
||||||
color_str = "yellow"
|
ansi_color = AnsiColor.YELLOW.value
|
||||||
else:
|
else:
|
||||||
color_str = None
|
ansi_color = AnsiColor.DEFAULT.value
|
||||||
|
|
||||||
command_prefix = getattr(record, "command_prefix", self.default_prefix)
|
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)
|
prefix_color = self.hostname_colorcode(command_prefix)
|
||||||
format_str = color(f"[{command_prefix}]", fg=prefix_color)
|
format_str = color_by_tuple(f"[{command_prefix}]", fg=prefix_color)
|
||||||
format_str += color(" %(message)s", fg=color_str)
|
format_str += color_by_tuple(" %(message)s", fg=ansi_color)
|
||||||
else:
|
else:
|
||||||
format_str = f"[{command_prefix}] %(message)s"
|
format_str = color_by_tuple("%(message)s", fg=ansi_color)
|
||||||
|
|
||||||
if self.trace_prints:
|
if self.trace_prints:
|
||||||
format_str += f"\nSource: {filepath}:%(lineno)d::%(funcName)s\n"
|
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)
|
return logging.Formatter(format_str).format(record)
|
||||||
|
|
||||||
def hostname_colorcode(self, hostname: str) -> tuple[int, int, int]:
|
def hostname_colorcode(self, hostname: str) -> tuple[int, int, int]:
|
||||||
|
colorcodes = RgbColor.list_values()
|
||||||
try:
|
try:
|
||||||
index = self.hostnames.index(hostname)
|
index = self.hostnames.index(hostname)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.hostnames += [hostname]
|
self.hostnames += [hostname]
|
||||||
index = self.hostnames.index(hostname)
|
index = self.hostnames.index(hostname)
|
||||||
coloroffset = (index + self.hostname_color_offset) % len(css_colors)
|
coloroffset = (index + self.hostname_color_offset) % len(colorcodes)
|
||||||
colorcode = list(css_colors.values())[coloroffset]
|
colorcode = colorcodes[coloroffset]
|
||||||
|
|
||||||
return colorcode
|
return colorcode
|
||||||
|
|
||||||
@@ -116,7 +133,7 @@ def get_callers(start: int = 2, end: int = 2) -> list[str]:
|
|||||||
return callers
|
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"))
|
trace_depth = int(os.environ.get("TRACE_DEPTH", "0"))
|
||||||
callers = get_callers(3, 4 + trace_depth)
|
callers = get_callers(3, 4 + trace_depth)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import sys
|
|||||||
|
|
||||||
from clan_cli.api import API
|
from clan_cli.api import API
|
||||||
from clan_cli.clan_uri import FlakeId
|
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 (
|
from clan_cli.completions import (
|
||||||
add_dynamic_completer,
|
add_dynamic_completer,
|
||||||
complete_machines,
|
complete_machines,
|
||||||
@@ -147,17 +148,26 @@ def deploy_machine(machines: MachineGroup) -> None:
|
|||||||
test_cmd.extend(["--target-host", target_host.target])
|
test_cmd.extend(["--target-host", target_host.target])
|
||||||
|
|
||||||
env = host.nix_ssh_env(None)
|
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
|
# if the machine is mobile, we retry to deploy with the quirk method
|
||||||
is_mobile = machine.deployment.get("nixosMobileWorkaround", False)
|
is_mobile = machine.deployment.get("nixosMobileWorkaround", False)
|
||||||
if is_mobile and ret.returncode != 0:
|
if is_mobile and ret.returncode != 0:
|
||||||
log.info("Mobile machine detected, applying quirk deployment method")
|
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
|
# retry nixos-rebuild switch if the first attempt failed
|
||||||
elif ret.returncode != 0:
|
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:
|
if len(machines.group.hosts) > 1:
|
||||||
machines.run_function(deploy)
|
machines.run_function(deploy)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
|||||||
from shlex import quote
|
from shlex import quote
|
||||||
from typing import IO, Any
|
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.cmd import run as local_run
|
||||||
from clan_cli.ssh.host_key import HostKeyCheck
|
from clan_cli.ssh.host_key import HostKeyCheck
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ class Host:
|
|||||||
check: bool = True,
|
check: bool = True,
|
||||||
error_msg: str | None = None,
|
error_msg: str | None = None,
|
||||||
needs_user_terminal: bool = False,
|
needs_user_terminal: bool = False,
|
||||||
|
msg_color: MsgColor | None = None,
|
||||||
shell: bool = False,
|
shell: bool = False,
|
||||||
timeout: float = math.inf,
|
timeout: float = math.inf,
|
||||||
) -> subprocess.CompletedProcess[str]:
|
) -> subprocess.CompletedProcess[str]:
|
||||||
@@ -85,6 +86,7 @@ class Host:
|
|||||||
logger=cmdlog,
|
logger=cmdlog,
|
||||||
check=check,
|
check=check,
|
||||||
error_msg=error_msg,
|
error_msg=error_msg,
|
||||||
|
msg_color=msg_color,
|
||||||
needs_user_terminal=needs_user_terminal,
|
needs_user_terminal=needs_user_terminal,
|
||||||
)
|
)
|
||||||
return subprocess.CompletedProcess(
|
return subprocess.CompletedProcess(
|
||||||
@@ -141,6 +143,7 @@ class Host:
|
|||||||
timeout: float = math.inf,
|
timeout: float = math.inf,
|
||||||
verbose_ssh: bool = False,
|
verbose_ssh: bool = False,
|
||||||
tty: bool = False,
|
tty: bool = False,
|
||||||
|
msg_color: MsgColor | None = None,
|
||||||
shell: bool = False,
|
shell: bool = False,
|
||||||
log: Log = Log.BOTH,
|
log: Log = Log.BOTH,
|
||||||
) -> subprocess.CompletedProcess[str]:
|
) -> subprocess.CompletedProcess[str]:
|
||||||
@@ -192,6 +195,7 @@ class Host:
|
|||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
check=check,
|
check=check,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
msg_color=msg_color,
|
||||||
needs_user_terminal=True, # ssh asks for a password
|
needs_user_terminal=True, # ssh asks for a password
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class HostGroup:
|
|||||||
errors += 1
|
errors += 1
|
||||||
if errors > 0:
|
if errors > 0:
|
||||||
msg = f"{errors} hosts failed with an error. Check the logs above"
|
msg = f"{errors} hosts failed with an error. Check the logs above"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg) from e
|
||||||
|
|
||||||
def _run(
|
def _run(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ let
|
|||||||
rope
|
rope
|
||||||
setuptools
|
setuptools
|
||||||
wheel
|
wheel
|
||||||
|
webcolors
|
||||||
pip
|
pip
|
||||||
]);
|
]);
|
||||||
in
|
in
|
||||||
|
|||||||
Reference in New Issue
Block a user