This commit is contained in:
Jeffrey C. Ollie 2023-10-29 21:31:54 -05:00
parent a463c62d69
commit 56344910a1
Signed by: jeff
GPG key ID: 6F86035A6D97044E
3 changed files with 144 additions and 25 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
__pycache__
/result*

View file

@ -1,11 +1,15 @@
from base64 import b64encode
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from enum import IntEnum from enum import IntEnum
from enum import StrEnum from enum import StrEnum
from enum import auto from enum import auto
from functools import wraps from functools import wraps
import re
from typing import Annotated from typing import Annotated
from typing import Callable from typing import Callable
from typing import Concatenate
from typing import ParamSpec
from typing import Self from typing import Self
from typing import get_args from typing import get_args
from typing import get_origin from typing import get_origin
@ -18,6 +22,11 @@ class ValueRange:
high: int high: int
@dataclass
class RegexMatch:
pattern: re.Pattern
def check_annotations(func: Callable) -> Callable: def check_annotations(func: Callable) -> Callable:
@wraps(func) @wraps(func)
def wrapped(*args, **kwargs): def wrapped(*args, **kwargs):
@ -28,12 +37,20 @@ def check_annotations(func: Callable) -> Callable:
hint_type, *hint_args = get_args(hint) hint_type, *hint_args = get_args(hint)
if hint_type is list or get_origin(hint_type) is list: if hint_type is list or get_origin(hint_type) is list:
for arg in hint_args: for arg in hint_args:
if isinstance(arg, ValueRange): match arg:
value = kwargs[param] case ValueRange():
if value < arg.low or value > arg.high: value = kwargs[param]
raise ValueError( if value < arg.low or value > arg.high:
f"Parameter '{param}' must be >= {arg.low} and <= {arg.high}" raise ValueError(
) f"Parameter '{param}' must be >= {arg.low} and <= {arg.high}"
)
case RegexMatch():
value = kwargs[param]
if not arg.pattern.match(value):
raise ValueError(
f"Parameter '{param}' must match pattern '{arg.pattern.pattern}'"
)
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapped return wrapped
@ -172,22 +189,49 @@ class DesktopNotificationDone(IntEnum):
DONE = 1 DONE = 1
class ClipboardType(StrEnum):
CLIPBOARD = "c"
PRIMARY = "p"
SELECT = "s"
CUT_BUFFER_0 = "0"
CUT_BUFFER_1 = "1"
CUT_BUFFER_2 = "2"
CUT_BUFFER_3 = "3"
CUT_BUFFER_4 = "4"
CUT_BUFFER_5 = "5"
CUT_BUFFER_6 = "6"
CUT_BUFFER_7 = "7"
CUT_BUFFER_8 = "8"
class CursorShape(IntEnum):
BLOCK = 0
VERTICAL_BAR = 1
UNDERLINE = 2
P = ParamSpec("P")
class ANSI: class ANSI:
mode: Mode mode: Mode
wrap: bool wrap: bool
@staticmethod @staticmethod
def readline_wrap(func: Callable) -> Callable: def readline_wrap(
func: Callable[Concatenate["ANSI", P], str]
) -> Callable[Concatenate["ANSI", P], str]:
"""wrap output in SOH/STX so that readline handles prompts properly""" """wrap output in SOH/STX so that readline handles prompts properly"""
@wraps(func) @wraps(func)
def wrapped(ansi: Self, *args, **kwargs): def wrapped(ansi: ANSI, *args: P.args, **kwargs: P.kwargs) -> str:
result = "" result = ""
if ansi.wrap: if ansi.wrap:
result += ansi.SOH result += ansi.SOH
result += func(ansi, *args, **kwargs) result += func(ansi, *args, **kwargs)
if ansi.wrap: if ansi.wrap:
result += ansi.STX result += ansi.STX
return result
return wrapped return wrapped
@ -195,8 +239,16 @@ class ANSI:
self.mode = mode self.mode = mode
self.wrap = wrap self.wrap = wrap
def _wrap(self: Self, data: str) -> str:
if self.wrap:
return f"{self.SOH}{data}{self.STX}"
return data
NUL: Annotated[str, "null"] = "\x00"
@property @property
def SOH(self: Self) -> str: def SOH(self: Self) -> str:
"""start of heading"""
match self.mode: match self.mode:
case Mode.C0 | Mode.C1: case Mode.C0 | Mode.C1:
return "\x01" return "\x01"
@ -205,17 +257,13 @@ class ANSI:
@property @property
def STX(self: Self) -> str: def STX(self: Self) -> str:
"""start of text"""
match self.mode: match self.mode:
case Mode.C0 | Mode.C1: case Mode.C0 | Mode.C1:
return "\x02" return "\x02"
case Mode.Bash: case Mode.Bash:
return "\\]" return "\\]"
def _wrap(self: Self, data: str) -> str:
if self.wrap:
return f"{self.SOH}{data}{self.STX}"
return data
@property @property
def RL_PROMPT_START_IGNORE(self: Self) -> str: def RL_PROMPT_START_IGNORE(self: Self) -> str:
if self.wrap: if self.wrap:
@ -228,6 +276,11 @@ class ANSI:
return self.STX return self.STX
return "" return ""
ETX: Annotated[str, "end of text"] = "\x03"
EOT: Annotated[str, "end of transmission"] = "\x04"
ENQ: Annotated[str, "enquiry"] = "\x05"
ACK: Annotated[str, "acknowlege"] = "\x06"
@property @property
def BEL(self: Self) -> str: def BEL(self: Self) -> str:
match self.mode: match self.mode:
@ -236,23 +289,43 @@ class ANSI:
case Mode.Bash: case Mode.Bash:
return "\\a" return "\\a"
BS = "\x08" BS: Annotated[str, "backspace"] = "\x08"
HT = "\x09" HT: Annotated[str, "horizontal tab"] = "\x09"
LF = "\x0a" LF: Annotated[str, "linefeed"] = "\x0a"
VT = "\x0b" VT: Annotated[str, "vertical tab"] = "\x0b"
FF = "\x0c" FF: Annotated[str, "form feed"] = "\x0c"
CR = "\x0d" CR: Annotated[str, "carriage return"] = "\x0d"
SO: Annotated[str, "shift out"] = "\x0e"
SI: Annotated[str, "shift in"] = "\x0f"
DLE: Annotated[str, "data link escape"] = "\x10"
DC1: Annotated[str, "device control one"] = "\x11"
XON = DC1
DC2: Annotated[str, "device control two"] = "\x12"
DC3: Annotated[str, "device control three"] = "\x13"
XOFF = DC3
DC4: Annotated[str, "device control four"] = "\x14"
NAK: Annotated[str, "negative acknowledge"] = "\x15"
SYN: Annotated[str, "synchronous idle"] = "\x16"
ETB: Annotated[str, "end of transmission block"] = "\x17"
CAN: Annotated[str, "cancel"] = "\x18"
EM: Annotated[str, "end of medium"] = "\x19"
SUB: Annotated[str, "substitute"] = "\x1a"
@property @property
def ESC(self) -> str: def ESC(self: Self) -> str:
"""Escape""" """escape"""
match self.mode: match self.mode:
case Mode.C0 | Mode.C1: case Mode.C0 | Mode.C1:
return "\x1b" return "\x1b"
case Mode.Bash: case Mode.Bash:
return "\\e" return "\\e"
DEL = "\x7f" FS: Annotated[str, "file separator"] = "\x1c"
GS: Annotated[str, "group separator"] = "\x1d"
RS: Annotated[str, "record separator"] = "\x1e"
US: Annotated[str, "unit separator"] = "\x1f"
DEL: Annotated[str, "delete"] = "\x7f"
@property @property
def DCS(self: Self) -> str: def DCS(self: Self) -> str:
@ -401,7 +474,6 @@ class ANSI:
def RestoreCursorPosition(self: Self) -> str: def RestoreCursorPosition(self: Self) -> str:
return f"{self.CSI}u" return f"{self.CSI}u"
@readline_wrap
def Title( def Title(
self: Self, text: str, icon_name: bool = True, window_title: bool = True self: Self, text: str, icon_name: bool = True, window_title: bool = True
) -> str: ) -> str:
@ -415,7 +487,7 @@ class ANSI:
code = 1 code = 1
case (True, True): case (True, True):
code = 0 code = 0
return f"{self.OSC}{code};{text}{self.ST}" return self._wrap(f"{self.OSC}{code};{text}{self.ST}")
def Hyperlink(self: Self, link: str, text: str) -> str: def Hyperlink(self: Self, link: str, text: str) -> str:
return ( return (
@ -426,12 +498,47 @@ class ANSI:
@readline_wrap @readline_wrap
def Notification(self: Self, text: str) -> str: def Notification(self: Self, text: str) -> str:
"""https://iterm2.com/documentation-escape-codes.html"""
return f"{self.OSC}9;{text}{self.ST}" return f"{self.OSC}9;{text}{self.ST}"
@readline_wrap
def SetMark(self: Self) -> str:
"""https://iterm2.com/documentation-escape-codes.html"""
return f"{self.OSC}1337;SetMark{self.ST}"
@readline_wrap
def StealFocus(self: Self) -> str:
"""https://iterm2.com/documentation-escape-codes.html"""
return f"{self.OSC}1337;StealFocus{self.ST}"
@readline_wrap @readline_wrap
def CurrentDirectory(self: Self, text: str) -> str: def CurrentDirectory(self: Self, text: str) -> str:
"""
Set the current directory
https://iterm2.com/documentation-escape-codes.html
"""
return f"{self.OSC}1337;CurrentDir={text}{self.ST}" return f"{self.OSC}1337;CurrentDir={text}{self.ST}"
@readline_wrap
def CopyToClipboard(self: Self, text: str) -> str:
"""https://iterm2.com/documentation-escape-codes.html"""
data = b64encode(text.encode("utf-8")).decode("utf-8")
return f"{self.OSC}52;Pc;{data}{self.ST}"
@readline_wrap
def ChangeProfile(self: Self, profile: str) -> str:
"""https://iterm2.com/documentation-escape-codes.html"""
return f"{self.OSC}1337;SetProfile={profile}{self.ST}"
@readline_wrap
def CursorShape(self: Self, shape: CursorShape) -> str:
"""Set the cursor shape
https://iterm2.com/documentation-escape-codes.html
"""
return f"{self.OSC}1337;CursorShape={shape}{self.ST}"
@readline_wrap @readline_wrap
def ActiveStatusDisplay(self: Self, mode: ActiveStatusDisplayMode) -> str: def ActiveStatusDisplay(self: Self, mode: ActiveStatusDisplayMode) -> str:
return f"{self.CSI}{mode}$}}" return f"{self.CSI}{mode}$}}"
@ -440,8 +547,14 @@ class ANSI:
def StatusLineType(self: Self, mode: StatusLineType) -> str: def StatusLineType(self: Self, mode: StatusLineType) -> str:
return f"{self.CSI}{mode}$~" return f"{self.CSI}{mode}$~"
@check_annotations
@readline_wrap @readline_wrap
def DesktopNotification(self: Self, identifier: str, title: str, body: str) -> str: def DesktopNotification(
self: Self,
identifier: Annotated[str, RegexMatch(re.compile(r"\A[a-zA-Z0-9-_+.]\z"))],
title: str,
body: str,
) -> str:
def format( def format(
part: DesktopNotificationPart, text: str, done: DesktopNotificationDone part: DesktopNotificationPart, text: str, done: DesktopNotificationDone
) -> str: ) -> str:

View file

@ -21,6 +21,9 @@ force_sort_within_sections = true
[tool.ruff] [tool.ruff]
line-length = 120 line-length = 120
[tool.pyright]
pythonVersion = "3.12"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"