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.
@@ -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"
@@ -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