Coverage for openhcs/textual_tui/services/terminal_enhancements.py: 0.0%
169 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""
2Terminal Enhancements for textual-terminal
4Extracted useful features from Gate One terminal.py to enhance textual-terminal:
5- Better ANSI escape sequence parsing
6- Enhanced color handling
7- Improved cursor positioning
8- Advanced scrollback management
10This is a focused extraction of the most valuable parts without the bloat.
11"""
13import re
14import logging
15from typing import Dict, List, Tuple, Optional, Callable
16from collections import defaultdict
18logger = logging.getLogger(__name__)
21class TerminalEnhancements:
22 """
23 Enhanced terminal features extracted from Gate One terminal.py
25 This class provides the most useful enhancements that can be integrated
26 into textual-terminal without the massive bloat of the full Gate One implementation.
27 """
29 # Enhanced regex patterns for better escape sequence parsing
30 RE_CSI_ESC_SEQ = re.compile(r'\x1B\[([?A-Za-z0-9>;@:\!]*?)([A-Za-z@_])')
31 RE_ESC_SEQ = re.compile(r'\x1b(.*\x1b\\|[ABCDEFGHIJKLMNOQRSTUVWXYZa-z0-9=<>]|[()# %*+].)')
32 RE_TITLE_SEQ = re.compile(r'\x1b\][0-2]\;(.*?)(\x07|\x1b\\)')
33 RE_OPT_SEQ = re.compile(r'\x1b\]_\;(.+?)(\x07|\x1b\\)')
34 RE_NUMBERS = re.compile(r'\d*')
36 # Enhanced color mappings
37 COLORS_256 = {
38 # Standard 16 colors
39 0: (0, 0, 0), # Black
40 1: (128, 0, 0), # Dark Red
41 2: (0, 128, 0), # Dark Green
42 3: (128, 128, 0), # Dark Yellow
43 4: (0, 0, 128), # Dark Blue
44 5: (128, 0, 128), # Dark Magenta
45 6: (0, 128, 128), # Dark Cyan
46 7: (192, 192, 192), # Light Gray
47 8: (128, 128, 128), # Dark Gray
48 9: (255, 0, 0), # Red
49 10: (0, 255, 0), # Green
50 11: (255, 255, 0), # Yellow
51 12: (0, 0, 255), # Blue
52 13: (255, 0, 255), # Magenta
53 14: (0, 255, 255), # Cyan
54 15: (255, 255, 255), # White
55 }
57 def __init__(self):
58 """Initialize terminal enhancements."""
59 self.callbacks = defaultdict(dict)
60 self.enhanced_colors = self._generate_256_colors()
62 # Enhanced escape sequence handlers
63 self.csi_handlers = self._setup_csi_handlers()
64 self.esc_handlers = self._setup_esc_handlers()
66 logger.info("Terminal enhancements initialized")
68 def _generate_256_colors(self) -> Dict[int, Tuple[int, int, int]]:
69 """Generate the full 256-color palette."""
70 colors = self.COLORS_256.copy()
72 # Colors 16-231: 6x6x6 color cube
73 for i in range(216):
74 r = (i // 36) * 51
75 g = ((i % 36) // 6) * 51
76 b = (i % 6) * 51
77 colors[16 + i] = (r, g, b)
79 # Colors 232-255: Grayscale
80 for i in range(24):
81 gray = 8 + i * 10
82 colors[232 + i] = (gray, gray, gray)
84 return colors
86 def _setup_csi_handlers(self) -> Dict[str, Callable]:
87 """Setup enhanced CSI escape sequence handlers."""
88 return {
89 'A': self._cursor_up,
90 'B': self._cursor_down,
91 'C': self._cursor_right,
92 'D': self._cursor_left,
93 'H': self._cursor_position,
94 'f': self._cursor_position,
95 'J': self._erase_display,
96 'K': self._erase_line,
97 'm': self._set_rendition,
98 'r': self._set_scroll_region,
99 's': self._save_cursor,
100 'u': self._restore_cursor,
101 'l': self._reset_mode,
102 'h': self._set_mode,
103 }
105 def _setup_esc_handlers(self) -> Dict[str, Callable]:
106 """Setup enhanced ESC sequence handlers."""
107 return {
108 'c': self._reset_terminal,
109 'D': self._index,
110 'E': self._next_line,
111 'H': self._set_tab_stop,
112 'M': self._reverse_index,
113 '7': self._save_cursor_and_attrs,
114 '8': self._restore_cursor_and_attrs,
115 '=': self._application_keypad,
116 '>': self._normal_keypad,
117 }
119 def parse_enhanced_escape_sequences(self, text: str) -> List[Tuple[str, str, Dict]]:
120 """
121 Parse escape sequences with enhanced Gate One logic.
123 Returns:
124 List of (text_part, sequence_type, params) tuples
125 """
126 parts = []
127 pos = 0
129 while pos < len(text):
130 # Look for CSI sequences first
131 csi_match = self.RE_CSI_ESC_SEQ.search(text, pos)
132 esc_match = self.RE_ESC_SEQ.search(text, pos)
133 title_match = self.RE_TITLE_SEQ.search(text, pos)
135 # Find the earliest match
136 matches = []
137 if csi_match:
138 matches.append(('csi', csi_match))
139 if esc_match:
140 matches.append(('esc', esc_match))
141 if title_match:
142 matches.append(('title', title_match))
144 if not matches:
145 # No more escape sequences, add remaining text
146 if pos < len(text):
147 parts.append((text[pos:], 'text', {}))
148 break
150 # Sort by position to get earliest match
151 matches.sort(key=lambda x: x[1].start())
152 seq_type, match = matches[0]
154 # Add text before the sequence
155 if match.start() > pos:
156 parts.append((text[pos:match.start()], 'text', {}))
158 # Add the sequence
159 if seq_type == 'csi':
160 params = self._parse_csi_params(match.group(1))
161 command = match.group(2)
162 parts.append((match.group(0), 'csi', {'params': params, 'command': command}))
163 elif seq_type == 'esc':
164 parts.append((match.group(0), 'esc', {'sequence': match.group(1)}))
165 elif seq_type == 'title':
166 parts.append((match.group(0), 'title', {'title': match.group(1)}))
168 pos = match.end()
170 return parts
172 def _parse_csi_params(self, param_str: str) -> List[int]:
173 """Parse CSI parameters into a list of integers."""
174 if not param_str:
175 return []
177 params = []
178 for param in param_str.split(';'):
179 if param.isdigit():
180 params.append(int(param))
181 else:
182 params.append(0) # Default value
184 return params
186 def get_enhanced_color(self, color_code: int) -> Optional[Tuple[int, int, int]]:
187 """Get RGB values for enhanced 256-color palette."""
188 return self.enhanced_colors.get(color_code)
190 def parse_color_sequence(self, params: List[int]) -> Dict[str, any]:
191 """
192 Parse SGR (Select Graphic Rendition) color sequences.
194 Returns:
195 Dictionary with color and style information
196 """
197 result = {
198 'fg_color': None,
199 'bg_color': None,
200 'bold': False,
201 'italic': False,
202 'underline': False,
203 'reverse': False,
204 'strikethrough': False,
205 }
207 i = 0
208 while i < len(params):
209 param = params[i]
211 if param == 0: # Reset
212 result = {k: False if isinstance(v, bool) else None for k, v in result.items()}
213 elif param == 1: # Bold
214 result['bold'] = True
215 elif param == 3: # Italic
216 result['italic'] = True
217 elif param == 4: # Underline
218 result['underline'] = True
219 elif param == 7: # Reverse
220 result['reverse'] = True
221 elif param == 9: # Strikethrough
222 result['strikethrough'] = True
223 elif param == 22: # Normal intensity
224 result['bold'] = False
225 elif param == 23: # Not italic
226 result['italic'] = False
227 elif param == 24: # Not underlined
228 result['underline'] = False
229 elif param == 27: # Not reversed
230 result['reverse'] = False
231 elif param == 29: # Not strikethrough
232 result['strikethrough'] = False
233 elif 30 <= param <= 37: # Foreground colors
234 result['fg_color'] = param - 30
235 elif param == 38: # Extended foreground color
236 if i + 2 < len(params) and params[i + 1] == 5:
237 result['fg_color'] = params[i + 2]
238 i += 2
239 elif i + 4 < len(params) and params[i + 1] == 2:
240 # RGB color
241 r, g, b = params[i + 2], params[i + 3], params[i + 4]
242 result['fg_color'] = (r, g, b)
243 i += 4
244 elif param == 39: # Default foreground
245 result['fg_color'] = None
246 elif 40 <= param <= 47: # Background colors
247 result['bg_color'] = param - 40
248 elif param == 48: # Extended background color
249 if i + 2 < len(params) and params[i + 1] == 5:
250 result['bg_color'] = params[i + 2]
251 i += 2
252 elif i + 4 < len(params) and params[i + 1] == 2:
253 # RGB color
254 r, g, b = params[i + 2], params[i + 3], params[i + 4]
255 result['bg_color'] = (r, g, b)
256 i += 4
257 elif param == 49: # Default background
258 result['bg_color'] = None
259 elif 90 <= param <= 97: # Bright foreground colors
260 result['fg_color'] = param - 90 + 8
261 elif 100 <= param <= 107: # Bright background colors
262 result['bg_color'] = param - 100 + 8
264 i += 1
266 return result
268 def add_callback(self, event_type: str, callback: Callable, identifier: str = None):
269 """Add a callback for terminal events."""
270 if identifier is None:
271 identifier = str(hash(callback))
272 self.callbacks[event_type][identifier] = callback
274 def remove_callback(self, event_type: str, identifier: str):
275 """Remove a callback."""
276 if event_type in self.callbacks and identifier in self.callbacks[event_type]:
277 del self.callbacks[event_type][identifier]
279 def trigger_callbacks(self, event_type: str, *args, **kwargs):
280 """Trigger all callbacks for an event type."""
281 for callback in self.callbacks[event_type].values():
282 try:
283 callback(*args, **kwargs)
284 except Exception as e:
285 logger.error(f"Error in callback {callback}: {e}")
287 # Placeholder methods for escape sequence handlers
288 # These would be implemented to actually modify terminal state
289 def _cursor_up(self, params): pass
290 def _cursor_down(self, params): pass
291 def _cursor_right(self, params): pass
292 def _cursor_left(self, params): pass
293 def _cursor_position(self, params): pass
294 def _erase_display(self, params): pass
295 def _erase_line(self, params): pass
296 def _set_rendition(self, params): pass
297 def _set_scroll_region(self, params): pass
298 def _save_cursor(self, params): pass
299 def _restore_cursor(self, params): pass
300 def _reset_mode(self, params): pass
301 def _set_mode(self, params): pass
302 def _reset_terminal(self, params): pass
303 def _index(self, params): pass
304 def _next_line(self, params): pass
305 def _set_tab_stop(self, params): pass
306 def _reverse_index(self, params): pass
307 def _save_cursor_and_attrs(self, params): pass
308 def _restore_cursor_and_attrs(self, params): pass
309 def _application_keypad(self, params): pass
310 def _normal_keypad(self, params): pass
313# Global instance for easy access
314terminal_enhancements = TerminalEnhancements()