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

1""" 

2Terminal Enhancements for textual-terminal 

3 

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 

9 

10This is a focused extraction of the most valuable parts without the bloat. 

11""" 

12 

13import re 

14import logging 

15from typing import Dict, List, Tuple, Optional, Callable 

16from collections import defaultdict 

17 

18logger = logging.getLogger(__name__) 

19 

20 

21class TerminalEnhancements: 

22 """ 

23 Enhanced terminal features extracted from Gate One terminal.py 

24  

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

28 

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*') 

35 

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 } 

56 

57 def __init__(self): 

58 """Initialize terminal enhancements.""" 

59 self.callbacks = defaultdict(dict) 

60 self.enhanced_colors = self._generate_256_colors() 

61 

62 # Enhanced escape sequence handlers 

63 self.csi_handlers = self._setup_csi_handlers() 

64 self.esc_handlers = self._setup_esc_handlers() 

65 

66 logger.info("Terminal enhancements initialized") 

67 

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

71 

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) 

78 

79 # Colors 232-255: Grayscale 

80 for i in range(24): 

81 gray = 8 + i * 10 

82 colors[232 + i] = (gray, gray, gray) 

83 

84 return colors 

85 

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 } 

104 

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 } 

118 

119 def parse_enhanced_escape_sequences(self, text: str) -> List[Tuple[str, str, Dict]]: 

120 """ 

121 Parse escape sequences with enhanced Gate One logic. 

122  

123 Returns: 

124 List of (text_part, sequence_type, params) tuples 

125 """ 

126 parts = [] 

127 pos = 0 

128 

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) 

134 

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

143 

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 

149 

150 # Sort by position to get earliest match 

151 matches.sort(key=lambda x: x[1].start()) 

152 seq_type, match = matches[0] 

153 

154 # Add text before the sequence 

155 if match.start() > pos: 

156 parts.append((text[pos:match.start()], 'text', {})) 

157 

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

167 

168 pos = match.end() 

169 

170 return parts 

171 

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

176 

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 

183 

184 return params 

185 

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) 

189 

190 def parse_color_sequence(self, params: List[int]) -> Dict[str, any]: 

191 """ 

192 Parse SGR (Select Graphic Rendition) color sequences. 

193  

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 } 

206 

207 i = 0 

208 while i < len(params): 

209 param = params[i] 

210 

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 

263 

264 i += 1 

265 

266 return result 

267 

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 

273 

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] 

278 

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}") 

286 

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 

311 

312 

313# Global instance for easy access 

314terminal_enhancements = TerminalEnhancements()