codehs-utils 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codehs_utils/__init__.py +78 -0
- codehs_utils/_buffer.py +51 -0
- codehs_utils/_pixelbuf.py +10 -0
- codehs_utils/_platform.py +156 -0
- codehs_utils/colors.py +510 -0
- codehs_utils/drawing.py +337 -0
- codehs_utils/geometry.py +70 -0
- codehs_utils/terminal.py +573 -0
- codehs_utils/text.py +199 -0
- codehs_utils-1.0.0.dist-info/METADATA +510 -0
- codehs_utils-1.0.0.dist-info/RECORD +13 -0
- codehs_utils-1.0.0.dist-info/WHEEL +4 -0
- codehs_utils-1.0.0.dist-info/licenses/LICENSE +21 -0
codehs_utils/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""codehs_utils: a small terminal toolkit (colors, gradients, banners,
|
|
2
|
+
keyboard/mouse input, drawing) originally written as one flat script for
|
|
3
|
+
CodeHS's Python environment, now split into a regular package.
|
|
4
|
+
|
|
5
|
+
Everything importable from the original flat module is re-exported here,
|
|
6
|
+
so ``import codehs_utils`` (or ``from codehs_utils import *``) behaves the
|
|
7
|
+
same as before.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ._buffer import frame, write
|
|
11
|
+
from .geometry import Rect
|
|
12
|
+
from .terminal import (
|
|
13
|
+
MouseEvent,
|
|
14
|
+
KeyEvent,
|
|
15
|
+
restore_terminal,
|
|
16
|
+
get_key,
|
|
17
|
+
get_keys,
|
|
18
|
+
get_key_nonblocking,
|
|
19
|
+
get_mouse_event,
|
|
20
|
+
get_mouse_event_nonblocking,
|
|
21
|
+
get_event,
|
|
22
|
+
events,
|
|
23
|
+
get_cursor_position,
|
|
24
|
+
gcp,
|
|
25
|
+
get_terminal_size,
|
|
26
|
+
clear_screen,
|
|
27
|
+
set_cursor_col,
|
|
28
|
+
set_cursor_row,
|
|
29
|
+
set_cursor_pos,
|
|
30
|
+
move_cursor,
|
|
31
|
+
save_cursor_position,
|
|
32
|
+
restore_cursor_position,
|
|
33
|
+
hide_cursor,
|
|
34
|
+
show_cursor,
|
|
35
|
+
enter_alt_screen,
|
|
36
|
+
leave_alt_screen,
|
|
37
|
+
clear_line,
|
|
38
|
+
enable_mouse_tracking,
|
|
39
|
+
disable_mouse_tracking,
|
|
40
|
+
print_at,
|
|
41
|
+
app,
|
|
42
|
+
)
|
|
43
|
+
from .text import wrap_text, align_text
|
|
44
|
+
from .colors import ColorLike, StyledText, ColorText, GradientText, ColorSpec, GradientColors
|
|
45
|
+
from .drawing import (
|
|
46
|
+
Banner,
|
|
47
|
+
BannerRow,
|
|
48
|
+
banner,
|
|
49
|
+
fill_rect,
|
|
50
|
+
get_pixel_size,
|
|
51
|
+
set_pixel,
|
|
52
|
+
clear_pixel,
|
|
53
|
+
clear_pixels,
|
|
54
|
+
Button,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"frame", "write",
|
|
59
|
+
"Rect",
|
|
60
|
+
"MouseEvent", "KeyEvent", "restore_terminal",
|
|
61
|
+
"get_key", "get_keys", "get_key_nonblocking",
|
|
62
|
+
"get_mouse_event", "get_mouse_event_nonblocking",
|
|
63
|
+
"get_event", "events",
|
|
64
|
+
"get_cursor_position", "gcp", "get_terminal_size",
|
|
65
|
+
"clear_screen", "set_cursor_col", "set_cursor_row", "set_cursor_pos",
|
|
66
|
+
"move_cursor", "save_cursor_position", "restore_cursor_position",
|
|
67
|
+
"hide_cursor", "show_cursor", "enter_alt_screen", "leave_alt_screen",
|
|
68
|
+
"clear_line", "enable_mouse_tracking", "disable_mouse_tracking",
|
|
69
|
+
"print_at", "app",
|
|
70
|
+
"wrap_text", "align_text",
|
|
71
|
+
"ColorLike", "StyledText", "ColorText", "GradientText",
|
|
72
|
+
"ColorSpec", "GradientColors",
|
|
73
|
+
"Banner", "BannerRow", "banner",
|
|
74
|
+
"fill_rect", "get_pixel_size", "set_pixel", "clear_pixel", "clear_pixels",
|
|
75
|
+
"Button",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
__version__ = "1.0.0"
|
codehs_utils/_buffer.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Output write buffering: batches writes made inside a `frame()` block into
|
|
2
|
+
a single flush to stdout, so multi-line draws don't tear or flicker.
|
|
3
|
+
|
|
4
|
+
A private implementation detail of :mod:`codehs_utils`, used by
|
|
5
|
+
:mod:`codehs_utils.terminal` and :mod:`codehs_utils.geometry`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import contextlib
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
_frame_depth = 0
|
|
13
|
+
_frame_buf: List[str] = []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _flush_pending_frame():
|
|
17
|
+
if _frame_buf:
|
|
18
|
+
data = "".join(_frame_buf)
|
|
19
|
+
_frame_buf.clear()
|
|
20
|
+
sys.stdout.write(data)
|
|
21
|
+
sys.stdout.flush()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _write(s: str, flush: bool = True):
|
|
25
|
+
if _frame_depth > 0:
|
|
26
|
+
_frame_buf.append(s)
|
|
27
|
+
else:
|
|
28
|
+
sys.stdout.write(s)
|
|
29
|
+
if flush:
|
|
30
|
+
sys.stdout.flush()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@contextlib.contextmanager
|
|
34
|
+
def frame(sync: bool = False):
|
|
35
|
+
global _frame_depth
|
|
36
|
+
_frame_depth += 1
|
|
37
|
+
try:
|
|
38
|
+
yield
|
|
39
|
+
finally:
|
|
40
|
+
_frame_depth -= 1
|
|
41
|
+
if _frame_depth == 0 and _frame_buf:
|
|
42
|
+
data = "".join(_frame_buf)
|
|
43
|
+
_frame_buf.clear()
|
|
44
|
+
if sync:
|
|
45
|
+
data = "\033[?2026h" + data + "\033[?2026l"
|
|
46
|
+
sys.stdout.write(data)
|
|
47
|
+
sys.stdout.flush()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def write(*parts, sep: str = "", flush: bool = True):
|
|
51
|
+
_write(sep.join(str(p) for p in parts), flush)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Shared half-block pixel-canvas state.
|
|
2
|
+
|
|
3
|
+
Kept in its own module (rather than in `.terminal` or `.drawing`) because
|
|
4
|
+
both of those modules touch it: `.terminal` clears it on `clear_screen`,
|
|
5
|
+
`enter_alt_screen`, and `leave_alt_screen`, while `.drawing` reads and
|
|
6
|
+
writes it for `set_pixel`/`clear_pixel`/`clear_pixels`. Keeping it here
|
|
7
|
+
avoids either module needing to import the other just for this dict.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
_pixel_buf: dict = {}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Windows console API bindings, used internally to provide VT-mode output,
|
|
2
|
+
raw key reads, cursor position, and terminal size on Windows.
|
|
3
|
+
|
|
4
|
+
Everything here is a private implementation detail of :mod:`codehs_utils`;
|
|
5
|
+
the public API lives in :mod:`codehs_utils.terminal`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
import shutil
|
|
11
|
+
import ctypes
|
|
12
|
+
import ctypes.wintypes
|
|
13
|
+
from typing import Optional, Tuple
|
|
14
|
+
|
|
15
|
+
_IS_WINDOWS = os.name == "nt"
|
|
16
|
+
|
|
17
|
+
if _IS_WINDOWS:
|
|
18
|
+
import msvcrt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _WinCOORD(ctypes.Structure):
|
|
22
|
+
_fields_ = [("X", ctypes.c_short), ("Y", ctypes.c_short)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _WinSMALL_RECT(ctypes.Structure):
|
|
26
|
+
_fields_ = [("Left", ctypes.c_short), ("Top", ctypes.c_short),
|
|
27
|
+
("Right", ctypes.c_short), ("Bottom", ctypes.c_short)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _WinCONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
|
|
31
|
+
_fields_ = [("dwSize", _WinCOORD),
|
|
32
|
+
("dwCursorPosition", _WinCOORD),
|
|
33
|
+
("wAttributes", ctypes.c_ushort),
|
|
34
|
+
("srWindow", _WinSMALL_RECT),
|
|
35
|
+
("dwMaximumWindowSize", _WinCOORD)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_STD_INPUT, _STD_OUTPUT = -10, -11
|
|
39
|
+
_WIN_VT_OUTPUT = 0x0004
|
|
40
|
+
_WIN_VT_INPUT = 0x0200
|
|
41
|
+
_WIN_QUICK_EDIT = 0x0040
|
|
42
|
+
_WIN_EXT_FLAGS = 0x0080
|
|
43
|
+
_win_saved_input_mode = None
|
|
44
|
+
|
|
45
|
+
if _IS_WINDOWS:
|
|
46
|
+
_k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
47
|
+
_k32.GetStdHandle.restype = ctypes.wintypes.HANDLE
|
|
48
|
+
_k32.GetStdHandle.argtypes = [ctypes.wintypes.DWORD]
|
|
49
|
+
_k32.GetConsoleMode.restype = ctypes.wintypes.BOOL
|
|
50
|
+
_k32.GetConsoleMode.argtypes = [ctypes.wintypes.HANDLE, ctypes.POINTER(ctypes.wintypes.DWORD)]
|
|
51
|
+
_k32.SetConsoleMode.restype = ctypes.wintypes.BOOL
|
|
52
|
+
_k32.SetConsoleMode.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD]
|
|
53
|
+
_k32.GetConsoleScreenBufferInfo.restype = ctypes.wintypes.BOOL
|
|
54
|
+
_k32.GetConsoleScreenBufferInfo.argtypes = [ctypes.wintypes.HANDLE,
|
|
55
|
+
ctypes.POINTER(_WinCONSOLE_SCREEN_BUFFER_INFO)]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _win_get_mode(handle) -> Optional[int]:
|
|
59
|
+
mode = ctypes.wintypes.DWORD()
|
|
60
|
+
if _k32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
61
|
+
return mode.value
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _win_enable_vt_output():
|
|
66
|
+
handle = _k32.GetStdHandle(_STD_OUTPUT)
|
|
67
|
+
mode = _win_get_mode(handle)
|
|
68
|
+
if mode is not None and not mode & _WIN_VT_OUTPUT:
|
|
69
|
+
_k32.SetConsoleMode(handle, mode | _WIN_VT_OUTPUT)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _win_enable_vt_input():
|
|
73
|
+
global _win_saved_input_mode
|
|
74
|
+
handle = _k32.GetStdHandle(_STD_INPUT)
|
|
75
|
+
mode = _win_get_mode(handle)
|
|
76
|
+
if mode is None:
|
|
77
|
+
return
|
|
78
|
+
if _win_saved_input_mode is None:
|
|
79
|
+
_win_saved_input_mode = mode
|
|
80
|
+
_k32.SetConsoleMode(handle, (mode | _WIN_VT_INPUT | _WIN_EXT_FLAGS) & ~_WIN_QUICK_EDIT)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _win_restore_input_mode():
|
|
84
|
+
global _win_saved_input_mode
|
|
85
|
+
if _win_saved_input_mode is not None:
|
|
86
|
+
try:
|
|
87
|
+
_k32.SetConsoleMode(_k32.GetStdHandle(_STD_INPUT), _win_saved_input_mode)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
_win_saved_input_mode = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _win_console_info():
|
|
94
|
+
handle = _k32.GetStdHandle(_STD_OUTPUT)
|
|
95
|
+
info = _WinCONSOLE_SCREEN_BUFFER_INFO()
|
|
96
|
+
if not _k32.GetConsoleScreenBufferInfo(handle, ctypes.byref(info)):
|
|
97
|
+
return None
|
|
98
|
+
return info
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _win_get_cursor_position() -> Tuple[int, int]:
|
|
102
|
+
info = _win_console_info()
|
|
103
|
+
if info is None:
|
|
104
|
+
raise OSError("Cannot read the cursor position: stdout is not a console.")
|
|
105
|
+
row = info.dwCursorPosition.Y - info.srWindow.Top + 1
|
|
106
|
+
col = info.dwCursorPosition.X - info.srWindow.Left + 1
|
|
107
|
+
return row, col
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _win_get_terminal_size() -> Tuple[int, int]:
|
|
111
|
+
info = _win_console_info()
|
|
112
|
+
if info is not None:
|
|
113
|
+
width = info.srWindow.Right - info.srWindow.Left + 1
|
|
114
|
+
height = info.srWindow.Bottom - info.srWindow.Top + 1
|
|
115
|
+
if width > 0 and height > 0:
|
|
116
|
+
return width, height
|
|
117
|
+
size = shutil.get_terminal_size(fallback=(80, 24))
|
|
118
|
+
return size.columns, size.lines
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_WIN_EXT_MAP = {
|
|
122
|
+
"H": b"\x1b[A", "P": b"\x1b[B", "M": b"\x1b[C", "K": b"\x1b[D",
|
|
123
|
+
"G": b"\x1b[H", "O": b"\x1b[F", "R": b"\x1b[2~", "S": b"\x1b[3~",
|
|
124
|
+
"I": b"\x1b[5~", "Q": b"\x1b[6~",
|
|
125
|
+
";": b"\x1bOP", "<": b"\x1bOQ", "=": b"\x1bOR", ">": b"\x1bOS",
|
|
126
|
+
"?": b"\x1b[15~", "@": b"\x1b[17~", "A": b"\x1b[18~", "B": b"\x1b[19~",
|
|
127
|
+
"C": b"\x1b[20~", "D": b"\x1b[21~", "\x85": b"\x1b[23~", "\x86": b"\x1b[24~",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _win_read_bytes(timeout=None):
|
|
132
|
+
start = time.monotonic()
|
|
133
|
+
while not msvcrt.kbhit():
|
|
134
|
+
if timeout is not None and (time.monotonic() - start) >= timeout:
|
|
135
|
+
return b""
|
|
136
|
+
time.sleep(0.005)
|
|
137
|
+
out = b""
|
|
138
|
+
while msvcrt.kbhit() and len(out) < 1024:
|
|
139
|
+
ch = msvcrt.getwch()
|
|
140
|
+
if ch in ("\x00", "\xe0"):
|
|
141
|
+
out += _WIN_EXT_MAP.get(msvcrt.getwch(), b"")
|
|
142
|
+
continue
|
|
143
|
+
if "\ud800" <= ch <= "\udbff":
|
|
144
|
+
try:
|
|
145
|
+
ch = (ch + msvcrt.getwch()).encode("utf-16", "surrogatepass").decode("utf-16")
|
|
146
|
+
except Exception:
|
|
147
|
+
continue
|
|
148
|
+
out += ch.encode("utf-8", errors="ignore")
|
|
149
|
+
return out
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if _IS_WINDOWS:
|
|
153
|
+
try:
|
|
154
|
+
_win_enable_vt_output()
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|