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 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