bittty 0.0.1__tar.gz
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-0.0.1/LICENSE.md +7 -0
- bittty-0.0.1/PKG-INFO +84 -0
- bittty-0.0.1/README.md +65 -0
- bittty-0.0.1/pyproject.toml +36 -0
- bittty-0.0.1/src/bittty/__init__.py +34 -0
- bittty-0.0.1/src/bittty/buffer.py +272 -0
- bittty-0.0.1/src/bittty/color.py +225 -0
- bittty-0.0.1/src/bittty/constants.py +152 -0
- bittty-0.0.1/src/bittty/parser.py +582 -0
- bittty-0.0.1/src/bittty/pty_base.py +64 -0
- bittty-0.0.1/src/bittty/pty_unix.py +187 -0
- bittty-0.0.1/src/bittty/pty_windows.py +170 -0
- bittty-0.0.1/src/bittty/style.py +322 -0
- bittty-0.0.1/src/bittty/tcaps.py +123 -0
- bittty-0.0.1/src/bittty/terminal.py +685 -0
bittty-0.0.1/LICENSE.md
ADDED
bittty-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bittty
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: a pure python tty
|
|
5
|
+
Author-email: Gareth Davidson <gaz@bitplane.net>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE.md
|
|
9
|
+
Requires-Dist: pre-commit ; extra == "dev"
|
|
10
|
+
Requires-Dist: pytest ; extra == "dev"
|
|
11
|
+
Requires-Dist: coverage ; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-cov ; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-asyncio ; extra == "dev"
|
|
14
|
+
Requires-Dist: build ; extra == "dev"
|
|
15
|
+
Requires-Dist: twine ; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff ; extra == "dev"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
|
|
19
|
+
# bittty
|
|
20
|
+
|
|
21
|
+
A pure Python terminal emulator.
|
|
22
|
+
|
|
23
|
+
Currently buggy and a bit slow, but it's still somewhat usable.
|
|
24
|
+
|
|
25
|
+
## Demo
|
|
26
|
+
|
|
27
|
+
Run the standalone demo:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python ./demo/terminal.py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or use the textual demo to see it in a TUI:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uvx textual-tty
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
There's 3 main classes:
|
|
42
|
+
|
|
43
|
+
1. `Terminal`, a standalone terminal that doesn't need Textual.
|
|
44
|
+
2. `TextualTerminal`, a tty widget subclass.
|
|
45
|
+
3. `TerminalApp`, a terminal emulator in a window.
|
|
46
|
+
|
|
47
|
+
Read the demo code for more info.
|
|
48
|
+
|
|
49
|
+
## Links
|
|
50
|
+
|
|
51
|
+
* [🏠 home](https://bitplane.net/dev/python/bittty)
|
|
52
|
+
* [🐍 pypi](https://pypi.org/project/bittty)
|
|
53
|
+
* [🐱 github](https://github.com/bitplane/bittty)
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
WTFPL with one additional clause
|
|
58
|
+
|
|
59
|
+
1. Don't blame me
|
|
60
|
+
|
|
61
|
+
Do wtf you want, but don't blame me when it rips a hole in your trousers.
|
|
62
|
+
|
|
63
|
+
## todo / ideas
|
|
64
|
+
|
|
65
|
+
- [ ] split pty out into a cross platform package
|
|
66
|
+
- [x] break terminal project out from Textual deps
|
|
67
|
+
- [x] write a minimal demo that doesn't need textual
|
|
68
|
+
- [ ] gui
|
|
69
|
+
- [ ] make `framebuffer.py`
|
|
70
|
+
- [ ] choose a backend
|
|
71
|
+
- [ ] performance improvements
|
|
72
|
+
- [ ] parse with regex over large buffer sizes
|
|
73
|
+
- [ ] scrollback buffer
|
|
74
|
+
- [ ] implement `logloglog` for scrollback with wrapping
|
|
75
|
+
- [ ] bugs
|
|
76
|
+
- [ ] blank background to end of line
|
|
77
|
+
- [ ] corruption in stream - debug it
|
|
78
|
+
- [ ] scroll region: scroll up in `vim` corrupts outside scroll region
|
|
79
|
+
- [ ] reduce redundancy redundancy of repeated repeated code code
|
|
80
|
+
- [ ] code code of of redundancy redundancy
|
|
81
|
+
- [ ] add terminal visuals
|
|
82
|
+
- [ ] bell flash effect
|
|
83
|
+
- [ ] Support themes
|
|
84
|
+
|
bittty-0.0.1/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# bittty
|
|
2
|
+
|
|
3
|
+
A pure Python terminal emulator.
|
|
4
|
+
|
|
5
|
+
Currently buggy and a bit slow, but it's still somewhat usable.
|
|
6
|
+
|
|
7
|
+
## Demo
|
|
8
|
+
|
|
9
|
+
Run the standalone demo:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python ./demo/terminal.py
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or use the textual demo to see it in a TUI:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uvx textual-tty
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
There's 3 main classes:
|
|
24
|
+
|
|
25
|
+
1. `Terminal`, a standalone terminal that doesn't need Textual.
|
|
26
|
+
2. `TextualTerminal`, a tty widget subclass.
|
|
27
|
+
3. `TerminalApp`, a terminal emulator in a window.
|
|
28
|
+
|
|
29
|
+
Read the demo code for more info.
|
|
30
|
+
|
|
31
|
+
## Links
|
|
32
|
+
|
|
33
|
+
* [🏠 home](https://bitplane.net/dev/python/bittty)
|
|
34
|
+
* [🐍 pypi](https://pypi.org/project/bittty)
|
|
35
|
+
* [🐱 github](https://github.com/bitplane/bittty)
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
WTFPL with one additional clause
|
|
40
|
+
|
|
41
|
+
1. Don't blame me
|
|
42
|
+
|
|
43
|
+
Do wtf you want, but don't blame me when it rips a hole in your trousers.
|
|
44
|
+
|
|
45
|
+
## todo / ideas
|
|
46
|
+
|
|
47
|
+
- [ ] split pty out into a cross platform package
|
|
48
|
+
- [x] break terminal project out from Textual deps
|
|
49
|
+
- [x] write a minimal demo that doesn't need textual
|
|
50
|
+
- [ ] gui
|
|
51
|
+
- [ ] make `framebuffer.py`
|
|
52
|
+
- [ ] choose a backend
|
|
53
|
+
- [ ] performance improvements
|
|
54
|
+
- [ ] parse with regex over large buffer sizes
|
|
55
|
+
- [ ] scrollback buffer
|
|
56
|
+
- [ ] implement `logloglog` for scrollback with wrapping
|
|
57
|
+
- [ ] bugs
|
|
58
|
+
- [ ] blank background to end of line
|
|
59
|
+
- [ ] corruption in stream - debug it
|
|
60
|
+
- [ ] scroll region: scroll up in `vim` corrupts outside scroll region
|
|
61
|
+
- [ ] reduce redundancy redundancy of repeated repeated code code
|
|
62
|
+
- [ ] code code of of redundancy redundancy
|
|
63
|
+
- [ ] add terminal visuals
|
|
64
|
+
- [ ] bell flash effect
|
|
65
|
+
- [ ] Support themes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bittty"
|
|
3
|
+
description = "a pure python tty"
|
|
4
|
+
version = "0.0.1"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Gareth Davidson", email = "gaz@bitplane.net" }
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"pre-commit",
|
|
18
|
+
"pytest",
|
|
19
|
+
"coverage",
|
|
20
|
+
"pytest-cov",
|
|
21
|
+
"pytest-asyncio",
|
|
22
|
+
"build",
|
|
23
|
+
"twine",
|
|
24
|
+
"ruff"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
build-backend = "flit_core.buildapi"
|
|
29
|
+
requires = ["flit_core >=3.2,<4"]
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 120
|
|
33
|
+
target-version = "py310"
|
|
34
|
+
|
|
35
|
+
[tool.ruff.format]
|
|
36
|
+
docstring-code-format = true
|
|
@@ -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
|
+
]
|
|
@@ -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)
|