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 enum import Enum
from enum import IntEnum
from enum import StrEnum
from enum import auto
from functools import wraps
import re
from typing import Annotated
from typing import Callable
from typing import Concatenate
from typing import ParamSpec
from typing import Self
from typing import get_args
from typing import get_origin
@ -18,6 +22,11 @@ class ValueRange:
high: int
@dataclass
class RegexMatch:
pattern: re.Pattern
def check_annotations(func: Callable) -> Callable:
@wraps(func)
def wrapped(*args, **kwargs):
@ -28,12 +37,20 @@ def check_annotations(func: Callable) -> Callable:
hint_type, *hint_args = get_args(hint)
if hint_type is list or get_origin(hint_type) is list:
for arg in hint_args:
if isinstance(arg, ValueRange):
value = kwargs[param]
if value < arg.low or value > arg.high:
raise ValueError(
f"Parameter '{param}' must be >= {arg.low} and <= {arg.high}"
)
match arg:
case ValueRange():
value = kwargs[param]
if value < arg.low or value > 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 wrapped
@ -172,22 +189,49 @@ class DesktopNotificationDone(IntEnum):
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:
mode: Mode
wrap: bool
@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"""
@wraps(func)
def wrapped(ansi: Self, *args, **kwargs):
def wrapped(ansi: ANSI, *args: P.args, **kwargs: P.kwargs) -> str:
result = ""
if ansi.wrap:
result += ansi.SOH
result += func(ansi, *args, **kwargs)
if ansi.wrap:
result += ansi.STX
return result
return wrapped
@ -195,8 +239,16 @@ class ANSI:
self.mode = mode
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
def SOH(self: Self) -> str:
"""start of heading"""
match self.mode:
case Mode.C0 | Mode.C1:
return "\x01"
@ -205,17 +257,13 @@ class ANSI:
@property
def STX(self: Self) -> str:
"""start of text"""
match self.mode:
case Mode.C0 | Mode.C1:
return "\x02"
case Mode.Bash:
return "\\]"
def _wrap(self: Self, data: str) -> str:
if self.wrap:
return f"{self.SOH}{data}{self.STX}"
return data
@property
def RL_PROMPT_START_IGNORE(self: Self) -> str:
if self.wrap:
@ -228,6 +276,11 @@ class ANSI:
return self.STX
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
def BEL(self: Self) -> str:
match self.mode:
@ -236,23 +289,43 @@ class ANSI:
case Mode.Bash:
return "\\a"
BS = "\x08"
HT = "\x09"
LF = "\x0a"
VT = "\x0b"
FF = "\x0c"
CR = "\x0d"
BS: Annotated[str, "backspace"] = "\x08"
HT: Annotated[str, "horizontal tab"] = "\x09"
LF: Annotated[str, "linefeed"] = "\x0a"
VT: Annotated[str, "vertical tab"] = "\x0b"
FF: Annotated[str, "form feed"] = "\x0c"
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
def ESC(self) -> str:
"""Escape"""
def ESC(self: Self) -> str:
"""escape"""
match self.mode:
case Mode.C0 | Mode.C1:
return "\x1b"
case Mode.Bash:
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
def DCS(self: Self) -> str:
@ -401,7 +474,6 @@ class ANSI:
def RestoreCursorPosition(self: Self) -> str:
return f"{self.CSI}u"
@readline_wrap
def Title(
self: Self, text: str, icon_name: bool = True, window_title: bool = True
) -> str:
@ -415,7 +487,7 @@ class ANSI:
code = 1
case (True, True):
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:
return (
@ -426,12 +498,47 @@ class ANSI:
@readline_wrap
def Notification(self: Self, text: str) -> str:
"""https://iterm2.com/documentation-escape-codes.html"""
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
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}"
@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
def ActiveStatusDisplay(self: Self, mode: ActiveStatusDisplayMode) -> str:
return f"{self.CSI}{mode}$}}"
@ -440,8 +547,14 @@ class ANSI:
def StatusLineType(self: Self, mode: StatusLineType) -> str:
return f"{self.CSI}{mode}$~"
@check_annotations
@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(
part: DesktopNotificationPart, text: str, done: DesktopNotificationDone
) -> str:

View file

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