bittty 0.0.1__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.
- bittty/__init__.py +34 -0
- bittty/buffer.py +272 -0
- bittty/color.py +225 -0
- bittty/constants.py +152 -0
- bittty/parser.py +582 -0
- bittty/pty_base.py +64 -0
- bittty/pty_unix.py +187 -0
- bittty/pty_windows.py +170 -0
- bittty/style.py +322 -0
- bittty/tcaps.py +123 -0
- bittty/terminal.py +685 -0
- bittty-0.0.1.dist-info/METADATA +84 -0
- bittty-0.0.1.dist-info/RECORD +15 -0
- bittty-0.0.1.dist-info/WHEEL +4 -0
- bittty-0.0.1.dist-info/licenses/LICENSE.md +7 -0
bittty/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bittty: A fast, pure Python terminal emulator library.
|
|
3
|
+
|
|
4
|
+
bittty (bitplane-tty) is a high-performance terminal emulator engine
|
|
5
|
+
that provides comprehensive ANSI sequence parsing and terminal state management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .terminal import Terminal
|
|
9
|
+
from .buffer import Buffer
|
|
10
|
+
from .parser import Parser
|
|
11
|
+
from .color import (
|
|
12
|
+
get_color_code,
|
|
13
|
+
get_rgb_code,
|
|
14
|
+
get_style_code,
|
|
15
|
+
get_combined_code,
|
|
16
|
+
reset_code,
|
|
17
|
+
get_cursor_code,
|
|
18
|
+
get_clear_line_code,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Terminal",
|
|
25
|
+
"Buffer",
|
|
26
|
+
"Parser",
|
|
27
|
+
"get_color_code",
|
|
28
|
+
"get_rgb_code",
|
|
29
|
+
"get_style_code",
|
|
30
|
+
"get_combined_code",
|
|
31
|
+
"reset_code",
|
|
32
|
+
"get_cursor_code",
|
|
33
|
+
"get_clear_line_code",
|
|
34
|
+
]
|
bittty/buffer.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terminal Buffer: Grid-based terminal content storage.
|
|
3
|
+
|
|
4
|
+
This module provides the Buffer class that manages terminal screen content
|
|
5
|
+
as a 2D grid of (ansi_code, character) tuples.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import List, Tuple
|
|
11
|
+
|
|
12
|
+
from . import constants
|
|
13
|
+
from .color import get_cursor_code, reset_code
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Type alias for a cell: (ANSI code, character)
|
|
17
|
+
Cell = Tuple[str, str]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Buffer:
|
|
21
|
+
"""
|
|
22
|
+
A buffer that stores terminal content as a 2D grid.
|
|
23
|
+
|
|
24
|
+
Each cell contains a tuple of (ansi_code, character) where:
|
|
25
|
+
- ansi_code: ANSI escape sequence for styling (empty string for no styling)
|
|
26
|
+
- character: The actual character to display
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, width: int, height: int) -> None:
|
|
30
|
+
"""Initialize buffer with given dimensions."""
|
|
31
|
+
self.width = width
|
|
32
|
+
self.height = height
|
|
33
|
+
# Initialize grid with empty cells
|
|
34
|
+
self.grid: List[List[Cell]] = [[("", " ") for _ in range(width)] for _ in range(height)]
|
|
35
|
+
|
|
36
|
+
def get_content(self) -> List[List[Cell]]:
|
|
37
|
+
"""Get buffer content as a 2D grid."""
|
|
38
|
+
return [row[:] for row in self.grid]
|
|
39
|
+
|
|
40
|
+
def get_cell(self, x: int, y: int) -> Cell:
|
|
41
|
+
"""Get cell at position."""
|
|
42
|
+
if 0 <= y < self.height and 0 <= x < self.width:
|
|
43
|
+
return self.grid[y][x]
|
|
44
|
+
return ("", " ")
|
|
45
|
+
|
|
46
|
+
def set_cell(self, x: int, y: int, char: str, ansi_code: str = "") -> None:
|
|
47
|
+
"""Set a single cell at position."""
|
|
48
|
+
if 0 <= y < self.height and 0 <= x < self.width:
|
|
49
|
+
self.grid[y][x] = (ansi_code, char)
|
|
50
|
+
|
|
51
|
+
def set(self, x: int, y: int, text: str, ansi_code: str = "") -> None:
|
|
52
|
+
"""Set text at position, overwriting existing content."""
|
|
53
|
+
if not (0 <= y < self.height):
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
for i, char in enumerate(text):
|
|
57
|
+
if x + i >= self.width:
|
|
58
|
+
break
|
|
59
|
+
self.grid[y][x + i] = (ansi_code, char)
|
|
60
|
+
|
|
61
|
+
def insert(self, x: int, y: int, text: str, ansi_code: str = "") -> None:
|
|
62
|
+
"""Insert text at position, shifting existing content right."""
|
|
63
|
+
if not (0 <= y < self.height) or x >= self.width:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# Get the current row
|
|
67
|
+
row = self.grid[y]
|
|
68
|
+
|
|
69
|
+
# Create new cells for the inserted text
|
|
70
|
+
new_cells = [(ansi_code, char) for char in text]
|
|
71
|
+
|
|
72
|
+
# Insert at position
|
|
73
|
+
if x < len(row):
|
|
74
|
+
# Split row and insert
|
|
75
|
+
new_row = row[:x] + new_cells + row[x:]
|
|
76
|
+
# Truncate to width
|
|
77
|
+
self.grid[y] = new_row[: self.width]
|
|
78
|
+
else:
|
|
79
|
+
# Pad with spaces if needed
|
|
80
|
+
padding_needed = x - len(row)
|
|
81
|
+
if padding_needed > 0:
|
|
82
|
+
row.extend([("", " ")] * padding_needed)
|
|
83
|
+
row.extend(new_cells)
|
|
84
|
+
# Truncate to width
|
|
85
|
+
self.grid[y] = row[: self.width]
|
|
86
|
+
|
|
87
|
+
def delete(self, x: int, y: int, count: int = 1) -> None:
|
|
88
|
+
"""Delete characters at position."""
|
|
89
|
+
if not (0 <= y < self.height) or x >= self.width:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
row = self.grid[y]
|
|
93
|
+
|
|
94
|
+
# Delete characters and shift left
|
|
95
|
+
if x < len(row):
|
|
96
|
+
end_pos = min(x + count, len(row))
|
|
97
|
+
new_row = row[:x] + row[end_pos:]
|
|
98
|
+
# Pad with spaces to maintain width
|
|
99
|
+
while len(new_row) < self.width:
|
|
100
|
+
new_row.append(("", " "))
|
|
101
|
+
self.grid[y] = new_row
|
|
102
|
+
|
|
103
|
+
def clear_region(self, x1: int, y1: int, x2: int, y2: int, ansi_code: str = "") -> None:
|
|
104
|
+
"""Clear a rectangular region."""
|
|
105
|
+
for y in range(max(0, y1), min(self.height, y2 + 1)):
|
|
106
|
+
for x in range(max(0, x1), min(self.width, x2 + 1)):
|
|
107
|
+
self.grid[y][x] = (ansi_code, " ")
|
|
108
|
+
|
|
109
|
+
def clear_line(
|
|
110
|
+
self, y: int, mode: int = constants.ERASE_FROM_CURSOR_TO_END, cursor_x: int = 0, ansi_code: str = ""
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Clear line content."""
|
|
113
|
+
if not (0 <= y < self.height):
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if mode == constants.ERASE_FROM_CURSOR_TO_END:
|
|
117
|
+
# Clear from cursor to end of line
|
|
118
|
+
for x in range(cursor_x, self.width):
|
|
119
|
+
self.grid[y][x] = (ansi_code, " ")
|
|
120
|
+
elif mode == constants.ERASE_FROM_START_TO_CURSOR:
|
|
121
|
+
# Clear from start to cursor
|
|
122
|
+
for x in range(0, min(cursor_x + 1, self.width)):
|
|
123
|
+
self.grid[y][x] = (ansi_code, " ")
|
|
124
|
+
elif mode == constants.ERASE_ALL:
|
|
125
|
+
# Clear entire line
|
|
126
|
+
self.grid[y] = [(ansi_code, " ") for _ in range(self.width)]
|
|
127
|
+
|
|
128
|
+
def scroll_up(self, count: int) -> None:
|
|
129
|
+
"""Scroll content up, removing top lines and adding blank lines at bottom."""
|
|
130
|
+
for _ in range(count):
|
|
131
|
+
self.grid.pop(0)
|
|
132
|
+
self.grid.append([("", " ") for _ in range(self.width)])
|
|
133
|
+
|
|
134
|
+
def scroll_down(self, count: int) -> None:
|
|
135
|
+
"""Scroll content down, removing bottom lines and adding blank lines at top."""
|
|
136
|
+
for _ in range(count):
|
|
137
|
+
self.grid.pop()
|
|
138
|
+
self.grid.insert(0, [("", " ") for _ in range(self.width)])
|
|
139
|
+
|
|
140
|
+
def resize(self, width: int, height: int) -> None:
|
|
141
|
+
"""Resize buffer to new dimensions."""
|
|
142
|
+
# Adjust number of rows
|
|
143
|
+
if len(self.grid) < height:
|
|
144
|
+
# Add new rows
|
|
145
|
+
for _ in range(height - len(self.grid)):
|
|
146
|
+
self.grid.append([("", " ") for _ in range(width)])
|
|
147
|
+
elif len(self.grid) > height:
|
|
148
|
+
# Remove excess rows
|
|
149
|
+
self.grid = self.grid[:height]
|
|
150
|
+
|
|
151
|
+
# Adjust width of each row
|
|
152
|
+
for y in range(len(self.grid)):
|
|
153
|
+
row = self.grid[y]
|
|
154
|
+
if len(row) < width:
|
|
155
|
+
# Extend row
|
|
156
|
+
row.extend([("", " ")] * (width - len(row)))
|
|
157
|
+
elif len(row) > width:
|
|
158
|
+
# Truncate row
|
|
159
|
+
self.grid[y] = row[:width]
|
|
160
|
+
|
|
161
|
+
# Update dimensions
|
|
162
|
+
self.width = width
|
|
163
|
+
self.height = height
|
|
164
|
+
|
|
165
|
+
def get_line_text(self, y: int) -> str:
|
|
166
|
+
"""Get plain text content of a line (for debugging/testing)."""
|
|
167
|
+
if 0 <= y < self.height:
|
|
168
|
+
return "".join(cell[1] for cell in self.grid[y])
|
|
169
|
+
return ""
|
|
170
|
+
|
|
171
|
+
def get_line(
|
|
172
|
+
self,
|
|
173
|
+
y: int,
|
|
174
|
+
width: int = None,
|
|
175
|
+
cursor_x: int = -1,
|
|
176
|
+
cursor_y: int = -1,
|
|
177
|
+
show_cursor: bool = False,
|
|
178
|
+
mouse_x: int = -1,
|
|
179
|
+
mouse_y: int = -1,
|
|
180
|
+
show_mouse: bool = False,
|
|
181
|
+
) -> str:
|
|
182
|
+
"""Get full ANSI sequence for a line (like tmux capture-pane)."""
|
|
183
|
+
if not (0 <= y < self.height):
|
|
184
|
+
return ""
|
|
185
|
+
|
|
186
|
+
# Use buffer width if not specified
|
|
187
|
+
if width is None:
|
|
188
|
+
width = self.width
|
|
189
|
+
|
|
190
|
+
parts = []
|
|
191
|
+
row = self.grid[y]
|
|
192
|
+
|
|
193
|
+
# Process each cell up to specified width
|
|
194
|
+
for x in range(min(len(row), width)):
|
|
195
|
+
ansi_code, char = row[x]
|
|
196
|
+
|
|
197
|
+
# Handle mouse cursor (convert to 0-based, as original code does mouse_x - 1)
|
|
198
|
+
if show_mouse and x == (mouse_x - 1) and y == (mouse_y - 1):
|
|
199
|
+
char = "↖"
|
|
200
|
+
|
|
201
|
+
# Handle text cursor position
|
|
202
|
+
if show_cursor and x == cursor_x and y == cursor_y:
|
|
203
|
+
# Add cursor style
|
|
204
|
+
parts.append(ansi_code)
|
|
205
|
+
parts.append(get_cursor_code())
|
|
206
|
+
parts.append(char)
|
|
207
|
+
parts.append("\033[27m") # Turn off reverse video only
|
|
208
|
+
else:
|
|
209
|
+
# Normal cell
|
|
210
|
+
parts.append(ansi_code)
|
|
211
|
+
parts.append(char)
|
|
212
|
+
|
|
213
|
+
# Pad to width if needed
|
|
214
|
+
current_width = min(len(row), width)
|
|
215
|
+
if current_width < width:
|
|
216
|
+
# Reset all attributes for padding (including background)
|
|
217
|
+
parts.append(reset_code())
|
|
218
|
+
parts.append(" " * (width - current_width))
|
|
219
|
+
|
|
220
|
+
# Always end with a reset to prevent bleeding to next line
|
|
221
|
+
parts.append(reset_code())
|
|
222
|
+
|
|
223
|
+
return "".join(parts)
|
|
224
|
+
|
|
225
|
+
def get_line_tuple(
|
|
226
|
+
self,
|
|
227
|
+
y: int,
|
|
228
|
+
width: int = None,
|
|
229
|
+
cursor_x: int = -1,
|
|
230
|
+
cursor_y: int = -1,
|
|
231
|
+
show_cursor: bool = False,
|
|
232
|
+
mouse_x: int = -1,
|
|
233
|
+
mouse_y: int = -1,
|
|
234
|
+
show_mouse: bool = False,
|
|
235
|
+
) -> tuple:
|
|
236
|
+
"""Get line as hashable tuple for caching: (ansi_code, char, ansi_code, char, ...)"""
|
|
237
|
+
if not (0 <= y < self.height):
|
|
238
|
+
return tuple()
|
|
239
|
+
|
|
240
|
+
# Use buffer width if not specified
|
|
241
|
+
if width is None:
|
|
242
|
+
width = self.width
|
|
243
|
+
|
|
244
|
+
parts = []
|
|
245
|
+
row = self.grid[y]
|
|
246
|
+
|
|
247
|
+
# Process each cell up to specified width
|
|
248
|
+
for x in range(min(len(row), width)):
|
|
249
|
+
ansi_code, char = row[x]
|
|
250
|
+
|
|
251
|
+
# Handle mouse cursor (convert to 0-based, as original code does mouse_x - 1)
|
|
252
|
+
if show_mouse and x == (mouse_x - 1) and y == (mouse_y - 1):
|
|
253
|
+
char = "↖"
|
|
254
|
+
|
|
255
|
+
# Handle text cursor position
|
|
256
|
+
if show_cursor and x == cursor_x and y == cursor_y:
|
|
257
|
+
# Add cursor style
|
|
258
|
+
parts.extend(("ansi", ansi_code, "cursor", get_cursor_code(), "char", char, "cursor_end", "\033[27m"))
|
|
259
|
+
else:
|
|
260
|
+
# Normal cell
|
|
261
|
+
parts.extend(("ansi", ansi_code, "char", char))
|
|
262
|
+
|
|
263
|
+
# Pad to width if needed
|
|
264
|
+
current_width = min(len(row), width)
|
|
265
|
+
if current_width < width:
|
|
266
|
+
# Reset all attributes for padding (including background)
|
|
267
|
+
parts.extend(("reset", reset_code(), "pad", " " * (width - current_width)))
|
|
268
|
+
|
|
269
|
+
# Always end with a reset to prevent bleeding to next line
|
|
270
|
+
parts.extend(("final_reset", reset_code()))
|
|
271
|
+
|
|
272
|
+
return tuple(parts)
|
bittty/color.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ANSI Sequence Cache: LRU-cached functions for generating ANSI escape sequences.
|
|
3
|
+
|
|
4
|
+
This module provides high-performance ANSI sequence generation by caching
|
|
5
|
+
commonly used combinations of colors and styles.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from typing import Optional, Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@lru_cache(maxsize=1024)
|
|
15
|
+
def get_color_code(fg: Optional[int] = None, bg: Optional[int] = None) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Generate ANSI color code for 256-color palette.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
fg: Foreground color (0-255) or None
|
|
21
|
+
bg: Background color (0-255) or None
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
ANSI escape sequence for the colors
|
|
25
|
+
"""
|
|
26
|
+
if fg is None and bg is None:
|
|
27
|
+
return ""
|
|
28
|
+
|
|
29
|
+
codes = []
|
|
30
|
+
if fg is not None:
|
|
31
|
+
codes.append(f"38;5;{fg}")
|
|
32
|
+
if bg is not None:
|
|
33
|
+
codes.append(f"48;5;{bg}")
|
|
34
|
+
|
|
35
|
+
return f"\033[{';'.join(codes)}m"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@lru_cache(maxsize=512)
|
|
39
|
+
def get_rgb_code(fg_rgb: Optional[Tuple[int, int, int]] = None, bg_rgb: Optional[Tuple[int, int, int]] = None) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Generate ANSI color code for RGB colors.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
fg_rgb: Foreground RGB tuple (r, g, b) or None
|
|
45
|
+
bg_rgb: Background RGB tuple (r, g, b) or None
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
ANSI escape sequence for the RGB colors
|
|
49
|
+
"""
|
|
50
|
+
if fg_rgb is None and bg_rgb is None:
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
codes = []
|
|
54
|
+
if fg_rgb is not None:
|
|
55
|
+
r, g, b = fg_rgb
|
|
56
|
+
codes.append(f"38;2;{r};{g};{b}")
|
|
57
|
+
if bg_rgb is not None:
|
|
58
|
+
r, g, b = bg_rgb
|
|
59
|
+
codes.append(f"48;2;{r};{g};{b}")
|
|
60
|
+
|
|
61
|
+
return f"\033[{';'.join(codes)}m"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@lru_cache(maxsize=256)
|
|
65
|
+
def get_style_code(
|
|
66
|
+
bold: bool = False,
|
|
67
|
+
dim: bool = False,
|
|
68
|
+
italic: bool = False,
|
|
69
|
+
underline: bool = False,
|
|
70
|
+
blink: bool = False,
|
|
71
|
+
reverse: bool = False,
|
|
72
|
+
strike: bool = False,
|
|
73
|
+
conceal: bool = False,
|
|
74
|
+
) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Generate ANSI style code for text attributes.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
ANSI escape sequence for the styles
|
|
80
|
+
"""
|
|
81
|
+
codes = []
|
|
82
|
+
if bold:
|
|
83
|
+
codes.append("1")
|
|
84
|
+
if dim:
|
|
85
|
+
codes.append("2")
|
|
86
|
+
if italic:
|
|
87
|
+
codes.append("3")
|
|
88
|
+
if underline:
|
|
89
|
+
codes.append("4")
|
|
90
|
+
if blink:
|
|
91
|
+
codes.append("5")
|
|
92
|
+
if reverse:
|
|
93
|
+
codes.append("7")
|
|
94
|
+
if conceal:
|
|
95
|
+
codes.append("8")
|
|
96
|
+
if strike:
|
|
97
|
+
codes.append("9")
|
|
98
|
+
|
|
99
|
+
if not codes:
|
|
100
|
+
return ""
|
|
101
|
+
|
|
102
|
+
return f"\033[{';'.join(codes)}m"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@lru_cache(maxsize=2048)
|
|
106
|
+
def get_combined_code(
|
|
107
|
+
fg: Optional[int] = None,
|
|
108
|
+
bg: Optional[int] = None,
|
|
109
|
+
fg_rgb: Optional[Tuple[int, int, int]] = None,
|
|
110
|
+
bg_rgb: Optional[Tuple[int, int, int]] = None,
|
|
111
|
+
bold: bool = False,
|
|
112
|
+
dim: bool = False,
|
|
113
|
+
italic: bool = False,
|
|
114
|
+
underline: bool = False,
|
|
115
|
+
blink: bool = False,
|
|
116
|
+
reverse: bool = False,
|
|
117
|
+
strike: bool = False,
|
|
118
|
+
conceal: bool = False,
|
|
119
|
+
) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Generate a combined ANSI code for colors and styles.
|
|
122
|
+
|
|
123
|
+
This is the main function to use for complete styling.
|
|
124
|
+
RGB colors take precedence over palette colors.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Complete ANSI escape sequence or empty string
|
|
128
|
+
"""
|
|
129
|
+
codes = []
|
|
130
|
+
|
|
131
|
+
# Style attributes first
|
|
132
|
+
if bold:
|
|
133
|
+
codes.append("1")
|
|
134
|
+
if dim:
|
|
135
|
+
codes.append("2")
|
|
136
|
+
if italic:
|
|
137
|
+
codes.append("3")
|
|
138
|
+
if underline:
|
|
139
|
+
codes.append("4")
|
|
140
|
+
if blink:
|
|
141
|
+
codes.append("5")
|
|
142
|
+
if reverse:
|
|
143
|
+
codes.append("7")
|
|
144
|
+
if conceal:
|
|
145
|
+
codes.append("8")
|
|
146
|
+
if strike:
|
|
147
|
+
codes.append("9")
|
|
148
|
+
|
|
149
|
+
# Colors (RGB takes precedence)
|
|
150
|
+
if fg_rgb is not None:
|
|
151
|
+
r, g, b = fg_rgb
|
|
152
|
+
codes.append(f"38;2;{r};{g};{b}")
|
|
153
|
+
elif fg is not None:
|
|
154
|
+
codes.append(f"38;5;{fg}")
|
|
155
|
+
|
|
156
|
+
if bg_rgb is not None:
|
|
157
|
+
r, g, b = bg_rgb
|
|
158
|
+
codes.append(f"48;2;{r};{g};{b}")
|
|
159
|
+
elif bg is not None:
|
|
160
|
+
codes.append(f"48;5;{bg}")
|
|
161
|
+
|
|
162
|
+
if not codes:
|
|
163
|
+
return ""
|
|
164
|
+
|
|
165
|
+
return f"\033[{';'.join(codes)}m"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@lru_cache(maxsize=1)
|
|
169
|
+
def reset_code() -> str:
|
|
170
|
+
"""Get the ANSI reset code."""
|
|
171
|
+
return "\033[0m"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@lru_cache(maxsize=16)
|
|
175
|
+
def get_basic_color_code(color: int, is_bg: bool = False) -> str:
|
|
176
|
+
"""
|
|
177
|
+
Generate ANSI code for basic 16 colors (0-15).
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
color: Color index (0-7 for normal, 8-15 for bright)
|
|
181
|
+
is_bg: True for background, False for foreground
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
ANSI escape sequence
|
|
185
|
+
"""
|
|
186
|
+
if 0 <= color <= 7:
|
|
187
|
+
# Normal colors
|
|
188
|
+
code = 40 + color if is_bg else 30 + color
|
|
189
|
+
elif 8 <= color <= 15:
|
|
190
|
+
# Bright colors
|
|
191
|
+
code = 100 + (color - 8) if is_bg else 90 + (color - 8)
|
|
192
|
+
else:
|
|
193
|
+
return ""
|
|
194
|
+
|
|
195
|
+
return f"\033[{code}m"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@lru_cache(maxsize=1)
|
|
199
|
+
def get_cursor_code() -> str:
|
|
200
|
+
"""Get ANSI code for cursor display (reverse video)."""
|
|
201
|
+
return "\033[7m"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@lru_cache(maxsize=1)
|
|
205
|
+
def get_clear_line_code() -> str:
|
|
206
|
+
"""Get ANSI code to clear to end of line."""
|
|
207
|
+
return "\033[K"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@lru_cache(maxsize=1)
|
|
211
|
+
def reset_foreground_code() -> str:
|
|
212
|
+
"""Get ANSI code to reset foreground color only."""
|
|
213
|
+
return "\033[39m"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@lru_cache(maxsize=1)
|
|
217
|
+
def reset_background_code() -> str:
|
|
218
|
+
"""Get ANSI code to reset background color only."""
|
|
219
|
+
return "\033[49m"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@lru_cache(maxsize=1)
|
|
223
|
+
def reset_text_attributes() -> str:
|
|
224
|
+
"""Reset text attributes but preserve background color."""
|
|
225
|
+
return "\033[0;22;23;24;25;27;28;29;39m"
|
bittty/constants.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants for the terminal parser and emulator.
|
|
3
|
+
|
|
4
|
+
This module contains constants used in the terminal parsing and emulation logic,
|
|
5
|
+
following standards like VT100, VT220, and xterm.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# --- Parser States ---
|
|
9
|
+
GROUND = "GROUND"
|
|
10
|
+
ESCAPE = "ESCAPE"
|
|
11
|
+
CSI_ENTRY = "CSI_ENTRY"
|
|
12
|
+
CSI_PARAM = "CSI_PARAM"
|
|
13
|
+
CSI_INTERMEDIATE = "CSI_INTERMEDIATE"
|
|
14
|
+
OSC_STRING = "OSC_STRING"
|
|
15
|
+
OSC_ESC = "OSC_ESC"
|
|
16
|
+
DCS_STRING = "DCS_STRING"
|
|
17
|
+
DCS_ESC = "DCS_ESC"
|
|
18
|
+
CHARSET_G0 = "CHARSET_G0"
|
|
19
|
+
CHARSET_G1 = "CHARSET_G1"
|
|
20
|
+
|
|
21
|
+
# --- C0 Control Characters ---
|
|
22
|
+
BEL = "\x07" # Bell
|
|
23
|
+
BS = "\x08" # Backspace
|
|
24
|
+
HT = "\x09" # Horizontal Tab
|
|
25
|
+
LF = "\x0a" # Line Feed
|
|
26
|
+
CR = "\x0d" # Carriage Return
|
|
27
|
+
ESC = "\x1b" # Escape
|
|
28
|
+
DEL = "\x7f" # Delete
|
|
29
|
+
|
|
30
|
+
# --- Erase Modes (for ED and EL) ---
|
|
31
|
+
ERASE_FROM_CURSOR_TO_END = 0
|
|
32
|
+
ERASE_FROM_START_TO_CURSOR = 1
|
|
33
|
+
ERASE_ALL = 2
|
|
34
|
+
|
|
35
|
+
# --- Private Modes (DECSET/DECRST) ---
|
|
36
|
+
# Used with CSI ? ... h/l
|
|
37
|
+
DECCKM_CURSOR_KEYS_APPLICATION = 1
|
|
38
|
+
DECAWM_AUTOWRAP = 7
|
|
39
|
+
DECTCEM_SHOW_CURSOR = 25
|
|
40
|
+
ALT_SCREEN_BUFFER_OLDER = 47
|
|
41
|
+
ALT_SCREEN_BUFFER = 1049
|
|
42
|
+
MOUSE_TRACKING_BASIC = 1000
|
|
43
|
+
MOUSE_TRACKING_BUTTON_EVENT = 1002
|
|
44
|
+
MOUSE_TRACKING_ANY_EVENT = 1003
|
|
45
|
+
MOUSE_SGR_MODE = 1006
|
|
46
|
+
MOUSE_EXTENDED_MODE = 1015
|
|
47
|
+
BRACKETED_PASTE = 2004
|
|
48
|
+
|
|
49
|
+
# --- ANSI Modes (SM/RM) ---
|
|
50
|
+
# Used with CSI ... h/l
|
|
51
|
+
DECKPAM_APPLICATION_KEYPAD = 1
|
|
52
|
+
IRM_INSERT_REPLACE = 4
|
|
53
|
+
|
|
54
|
+
# --- SGR (Select Graphic Rendition) Parameters ---
|
|
55
|
+
SGR_RESET = 0
|
|
56
|
+
SGR_BOLD = 1
|
|
57
|
+
SGR_DIM = 2
|
|
58
|
+
SGR_ITALIC = 3
|
|
59
|
+
SGR_UNDERLINE = 4
|
|
60
|
+
SGR_BLINK = 5
|
|
61
|
+
SGR_REVERSE = 7
|
|
62
|
+
SGR_CONCEAL = 8
|
|
63
|
+
SGR_STRIKE = 9
|
|
64
|
+
SGR_NOT_BOLD_OR_DOUBLE_UNDERLINE = 21
|
|
65
|
+
SGR_NOT_BOLD_NOR_FAINT = 22
|
|
66
|
+
SGR_NOT_ITALIC = 23
|
|
67
|
+
SGR_NOT_UNDERLINED = 24
|
|
68
|
+
SGR_NOT_BLINKING = 25
|
|
69
|
+
SGR_NOT_REVERSED = 27
|
|
70
|
+
SGR_NOT_CONCEALED = 28
|
|
71
|
+
SGR_NOT_STRIKETHROUGH = 29
|
|
72
|
+
SGR_FG_BLACK = 30
|
|
73
|
+
SGR_FG_RED = 31
|
|
74
|
+
SGR_FG_GREEN = 32
|
|
75
|
+
SGR_FG_YELLOW = 33
|
|
76
|
+
SGR_FG_BLUE = 34
|
|
77
|
+
SGR_FG_MAGENTA = 35
|
|
78
|
+
SGR_FG_CYAN = 36
|
|
79
|
+
SGR_FG_WHITE = 37
|
|
80
|
+
SGR_EXTENDED_COLOR = 38
|
|
81
|
+
SGR_DEFAULT_FG = 39
|
|
82
|
+
SGR_BG_BLACK = 40
|
|
83
|
+
SGR_BG_RED = 41
|
|
84
|
+
SGR_BG_GREEN = 42
|
|
85
|
+
SGR_BG_YELLOW = 43
|
|
86
|
+
SGR_BG_BLUE = 44
|
|
87
|
+
SGR_BG_MAGENTA = 45
|
|
88
|
+
SGR_BG_CYAN = 46
|
|
89
|
+
SGR_BG_WHITE = 47
|
|
90
|
+
SGR_EXTENDED_BG_COLOR = 48
|
|
91
|
+
SGR_DEFAULT_BG = 49
|
|
92
|
+
SGR_BRIGHT_FG_BLACK = 90
|
|
93
|
+
SGR_BRIGHT_FG_RED = 91
|
|
94
|
+
SGR_BRIGHT_FG_GREEN = 92
|
|
95
|
+
SGR_BRIGHT_FG_YELLOW = 93
|
|
96
|
+
SGR_BRIGHT_FG_BLUE = 94
|
|
97
|
+
SGR_BRIGHT_FG_MAGENTA = 95
|
|
98
|
+
SGR_BRIGHT_FG_CYAN = 96
|
|
99
|
+
SGR_BRIGHT_FG_WHITE = 97
|
|
100
|
+
SGR_BRIGHT_BG_BLACK = 100
|
|
101
|
+
SGR_BRIGHT_BG_RED = 101
|
|
102
|
+
SGR_BRIGHT_BG_GREEN = 102
|
|
103
|
+
SGR_BRIGHT_BG_YELLOW = 103
|
|
104
|
+
SGR_BRIGHT_BG_BLUE = 104
|
|
105
|
+
SGR_BRIGHT_BG_MAGENTA = 105
|
|
106
|
+
SGR_BRIGHT_BG_CYAN = 106
|
|
107
|
+
SGR_BRIGHT_BG_WHITE = 107
|
|
108
|
+
|
|
109
|
+
# --- OSC (Operating System Command) Commands ---
|
|
110
|
+
OSC_SET_TITLE_AND_ICON = 0
|
|
111
|
+
OSC_SET_ICON_TITLE = 1
|
|
112
|
+
OSC_SET_TITLE = 2
|
|
113
|
+
|
|
114
|
+
# --- Mouse Button Modifiers ---
|
|
115
|
+
MOUSE_MOD_SHIFT = 4
|
|
116
|
+
MOUSE_MOD_META = 8
|
|
117
|
+
MOUSE_MOD_CTRL = 16
|
|
118
|
+
|
|
119
|
+
# --- Mouse Buttons ---
|
|
120
|
+
MOUSE_BUTTON_LEFT = 0
|
|
121
|
+
MOUSE_BUTTON_MIDDLE = 1
|
|
122
|
+
MOUSE_BUTTON_RIGHT = 2
|
|
123
|
+
MOUSE_BUTTON_WHEEL_UP = 64
|
|
124
|
+
MOUSE_BUTTON_WHEEL_DOWN = 65
|
|
125
|
+
MOUSE_BUTTON_MOVEMENT = 35
|
|
126
|
+
|
|
127
|
+
# --- Keyboard Modifier Codes (for CSI sequences) ---
|
|
128
|
+
KEY_MOD_NONE = 1
|
|
129
|
+
KEY_MOD_SHIFT = 2
|
|
130
|
+
KEY_MOD_ALT = 3
|
|
131
|
+
KEY_MOD_SHIFT_ALT = 4
|
|
132
|
+
KEY_MOD_CTRL = 5
|
|
133
|
+
KEY_MOD_SHIFT_CTRL = 6
|
|
134
|
+
KEY_MOD_ALT_CTRL = 7
|
|
135
|
+
KEY_MOD_SHIFT_ALT_CTRL = 8
|
|
136
|
+
|
|
137
|
+
# --- Cursor Key Mappings ---
|
|
138
|
+
CURSOR_KEYS = {"up": "A", "down": "B", "right": "C", "left": "D"}
|
|
139
|
+
|
|
140
|
+
# --- Navigation Key Mappings ---
|
|
141
|
+
NAV_KEYS = {"home": "H", "end": "F"}
|
|
142
|
+
|
|
143
|
+
# --- PTY and System Constants ---
|
|
144
|
+
DEFAULT_TERMINAL_WIDTH = 80
|
|
145
|
+
DEFAULT_TERMINAL_HEIGHT = 24
|
|
146
|
+
DEFAULT_PTY_BUFFER_SIZE = 4096
|
|
147
|
+
PTY_POLL_INTERVAL = 0.1
|
|
148
|
+
DEFAULT_EXIT_CODE = 0
|
|
149
|
+
|
|
150
|
+
# --- Error Numbers ---
|
|
151
|
+
EBADF = 9 # Bad file descriptor
|
|
152
|
+
EINVAL = 22 # Invalid argument
|