updates
This commit is contained in:
parent
a463c62d69
commit
56344910a1
3 changed files with 144 additions and 25 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
__pycache__
|
||||
/result*
|
||||
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue