wafer-core 0.1.37__py3-none-any.whl → 0.1.39__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.
Files changed (32) hide show
  1. wafer_core/lib/trace_compare/fusion_analyzer.py +2 -0
  2. wafer_core/rollouts/_logging/__init__.py +5 -1
  3. wafer_core/rollouts/_logging/logging_config.py +95 -3
  4. wafer_core/rollouts/_logging/sample_handler.py +66 -0
  5. wafer_core/rollouts/_pytui/__init__.py +114 -0
  6. wafer_core/rollouts/_pytui/app.py +809 -0
  7. wafer_core/rollouts/_pytui/console.py +291 -0
  8. wafer_core/rollouts/_pytui/renderer.py +210 -0
  9. wafer_core/rollouts/_pytui/spinner.py +73 -0
  10. wafer_core/rollouts/_pytui/terminal.py +489 -0
  11. wafer_core/rollouts/_pytui/text.py +470 -0
  12. wafer_core/rollouts/_pytui/theme.py +241 -0
  13. wafer_core/rollouts/evaluation.py +142 -177
  14. wafer_core/rollouts/progress_app.py +395 -0
  15. wafer_core/rollouts/tui/DESIGN.md +251 -115
  16. wafer_core/rollouts/tui/monitor.py +64 -20
  17. wafer_core/tools/compile/__init__.py +30 -0
  18. wafer_core/tools/compile/compiler.py +314 -0
  19. wafer_core/tools/compile/modal_compile.py +359 -0
  20. wafer_core/tools/compile/tests/__init__.py +1 -0
  21. wafer_core/tools/compile/tests/test_compiler.py +675 -0
  22. wafer_core/tools/compile/tests/test_data/utils.cuh +10 -0
  23. wafer_core/tools/compile/tests/test_data/vector_add.cu +7 -0
  24. wafer_core/tools/compile/tests/test_data/with_header.cu +9 -0
  25. wafer_core/tools/compile/tests/test_modal_integration.py +326 -0
  26. wafer_core/tools/compile/types.py +117 -0
  27. {wafer_core-0.1.37.dist-info → wafer_core-0.1.39.dist-info}/METADATA +1 -1
  28. {wafer_core-0.1.37.dist-info → wafer_core-0.1.39.dist-info}/RECORD +29 -12
  29. wafer_core/rollouts/events.py +0 -240
  30. wafer_core/rollouts/progress_display.py +0 -476
  31. wafer_core/utils/event_streaming.py +0 -63
  32. {wafer_core-0.1.37.dist-info → wafer_core-0.1.39.dist-info}/WHEEL +0 -0
@@ -0,0 +1,489 @@
1
+ """Terminal abstraction - raw mode, cursor control, input handling.
2
+
3
+ Unified terminal layer for all TUI apps. Handles:
4
+ - Raw mode (termios) with /dev/tty fallback
5
+ - Non-blocking keyboard input with escape sequence buffering
6
+ - SIGWINCH resize handling
7
+ - Alternate screen buffer (optional)
8
+ - Bracketed paste mode (optional)
9
+ - Atexit cleanup for crash safety
10
+ - External editor support (temporarily restore cooked mode)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import atexit
16
+ import os
17
+ import select
18
+ import signal
19
+ import sys
20
+ import termios
21
+ import time
22
+ import tty
23
+ from collections.abc import Callable
24
+ from types import FrameType
25
+ from typing import Any, Protocol
26
+
27
+ # ANSI escape sequences — named for readability and grep-ability
28
+ ALT_SCREEN_ON = "\x1b[?1049h"
29
+ ALT_SCREEN_OFF = "\x1b[?1049l"
30
+ BRACKETED_PASTE_ON = "\x1b[?2004h"
31
+ BRACKETED_PASTE_OFF = "\x1b[?2004l"
32
+ SYNC_OUTPUT_ON = "\x1b[?2026h"
33
+ SYNC_OUTPUT_OFF = "\x1b[?2026l"
34
+ CURSOR_SHOW = "\x1b[?25h"
35
+ CURSOR_HIDE = "\x1b[?25l"
36
+ CLEAR_SCREEN_HOME = "\x1b[2J\x1b[H"
37
+ CLEAR_LINE = "\x1b[K"
38
+ CLEAR_LINE_FULL = "\x1b[2K"
39
+ CLEAR_TO_END = "\x1b[J"
40
+ RESET_ATTRS = "\x1b[0m"
41
+
42
+ # Mouse tracking (SGR extended mode - handles coordinates > 223)
43
+ MOUSE_ON = "\x1b[?1000h\x1b[?1006h" # Enable button events + SGR encoding
44
+ MOUSE_OFF = "\x1b[?1000l\x1b[?1006l"
45
+
46
+ # Focus reporting
47
+ FOCUS_ON = "\x1b[?1004h"
48
+ FOCUS_OFF = "\x1b[?1004l"
49
+
50
+ # Known escape sequences - ported from bubbletea/key.go
51
+ # Maps sequence -> key name for reliable detection
52
+ KNOWN_SEQUENCES: dict[str, str] = {
53
+ # Arrow keys
54
+ "\x1b[A": "up", "\x1b[B": "down", "\x1b[C": "right", "\x1b[D": "left",
55
+ "\x1b[1;2A": "shift+up", "\x1b[1;2B": "shift+down",
56
+ "\x1b[1;2C": "shift+right", "\x1b[1;2D": "shift+left",
57
+ "\x1b[OA": "shift+up", "\x1b[OB": "shift+down", # DECCKM
58
+ "\x1b[OC": "shift+right", "\x1b[OD": "shift+left",
59
+ "\x1b[a": "shift+up", "\x1b[b": "shift+down", # urxvt
60
+ "\x1b[c": "shift+right", "\x1b[d": "shift+left",
61
+ "\x1b[1;3A": "alt+up", "\x1b[1;3B": "alt+down",
62
+ "\x1b[1;3C": "alt+right", "\x1b[1;3D": "alt+left",
63
+ "\x1b[1;4A": "alt+shift+up", "\x1b[1;4B": "alt+shift+down",
64
+ "\x1b[1;4C": "alt+shift+right", "\x1b[1;4D": "alt+shift+left",
65
+ "\x1b[1;5A": "ctrl+up", "\x1b[1;5B": "ctrl+down",
66
+ "\x1b[1;5C": "ctrl+right", "\x1b[1;5D": "ctrl+left",
67
+ "\x1b[Oa": "alt+ctrl+up", "\x1b[Ob": "alt+ctrl+down", # urxvt
68
+ "\x1b[Oc": "alt+ctrl+right", "\x1b[Od": "alt+ctrl+left",
69
+ "\x1b[1;6A": "ctrl+shift+up", "\x1b[1;6B": "ctrl+shift+down",
70
+ "\x1b[1;6C": "ctrl+shift+right", "\x1b[1;6D": "ctrl+shift+left",
71
+ "\x1b[1;7A": "alt+ctrl+up", "\x1b[1;7B": "alt+ctrl+down",
72
+ "\x1b[1;7C": "alt+ctrl+right", "\x1b[1;7D": "alt+ctrl+left",
73
+ "\x1b[1;8A": "alt+ctrl+shift+up", "\x1b[1;8B": "alt+ctrl+shift+down",
74
+ "\x1b[1;8C": "alt+ctrl+shift+right", "\x1b[1;8D": "alt+ctrl+shift+left",
75
+ # Application mode arrows (powershell, etc)
76
+ "\x1bOA": "up", "\x1bOB": "down", "\x1bOC": "right", "\x1bOD": "left",
77
+ # Misc keys
78
+ "\x1b[Z": "shift+tab",
79
+ "\x1b[2~": "insert", "\x1b[3;2~": "alt+insert",
80
+ "\x1b[3~": "delete", "\x1b[3;3~": "alt+delete",
81
+ # Page Up/Down
82
+ "\x1b[5~": "pageup", "\x1b[5;3~": "alt+pageup",
83
+ "\x1b[5;5~": "ctrl+pageup", "\x1b[5^": "ctrl+pageup", # urxvt
84
+ "\x1b[5;7~": "alt+ctrl+pageup",
85
+ "\x1b[6~": "pagedown", "\x1b[6;3~": "alt+pagedown",
86
+ "\x1b[6;5~": "ctrl+pagedown", "\x1b[6^": "ctrl+pagedown", # urxvt
87
+ "\x1b[6;7~": "alt+ctrl+pagedown",
88
+ # Home/End - xterm style
89
+ "\x1b[1~": "home", "\x1b[H": "home",
90
+ "\x1b[1;3H": "alt+home", "\x1b[1;5H": "ctrl+home",
91
+ "\x1b[1;7H": "alt+ctrl+home", "\x1b[1;2H": "shift+home",
92
+ "\x1b[1;4H": "alt+shift+home", "\x1b[1;6H": "ctrl+shift+home",
93
+ "\x1b[1;8H": "alt+ctrl+shift+home",
94
+ "\x1b[4~": "end", "\x1b[F": "end",
95
+ "\x1b[1;3F": "alt+end", "\x1b[1;5F": "ctrl+end",
96
+ "\x1b[1;7F": "alt+ctrl+end", "\x1b[1;2F": "shift+end",
97
+ "\x1b[1;4F": "alt+shift+end", "\x1b[1;6F": "ctrl+shift+end",
98
+ "\x1b[1;8F": "alt+ctrl+shift+end",
99
+ # Home/End - urxvt style
100
+ "\x1b[7~": "home", "\x1b[7^": "ctrl+home",
101
+ "\x1b[7$": "shift+home", "\x1b[7@": "ctrl+shift+home",
102
+ "\x1b[8~": "end", "\x1b[8^": "ctrl+end",
103
+ "\x1b[8$": "shift+end", "\x1b[8@": "ctrl+shift+end",
104
+ # Function keys - linux console
105
+ "\x1b[[A": "f1", "\x1b[[B": "f2", "\x1b[[C": "f3",
106
+ "\x1b[[D": "f4", "\x1b[[E": "f5",
107
+ # Function keys - vt100/xterm
108
+ "\x1bOP": "f1", "\x1bOQ": "f2", "\x1bOR": "f3", "\x1bOS": "f4",
109
+ "\x1b[1;3P": "alt+f1", "\x1b[1;3Q": "alt+f2",
110
+ "\x1b[1;3R": "alt+f3", "\x1b[1;3S": "alt+f4",
111
+ # Function keys - urxvt style
112
+ "\x1b[11~": "f1", "\x1b[12~": "f2", "\x1b[13~": "f3", "\x1b[14~": "f4",
113
+ "\x1b[15~": "f5", "\x1b[15;3~": "alt+f5",
114
+ "\x1b[17~": "f6", "\x1b[18~": "f7", "\x1b[19~": "f8",
115
+ "\x1b[20~": "f9", "\x1b[21~": "f10",
116
+ "\x1b[17;3~": "alt+f6", "\x1b[18;3~": "alt+f7",
117
+ "\x1b[19;3~": "alt+f8", "\x1b[20;3~": "alt+f9", "\x1b[21;3~": "alt+f10",
118
+ "\x1b[23~": "f11", "\x1b[24~": "f12",
119
+ "\x1b[23;3~": "alt+f11", "\x1b[24;3~": "alt+f12",
120
+ # F13-F20
121
+ "\x1b[1;2P": "f13", "\x1b[1;2Q": "f14",
122
+ "\x1b[25~": "f13", "\x1b[26~": "f14",
123
+ "\x1b[25;3~": "alt+f13", "\x1b[26;3~": "alt+f14",
124
+ "\x1b[1;2R": "f15", "\x1b[1;2S": "f16",
125
+ "\x1b[28~": "f15", "\x1b[29~": "f16",
126
+ "\x1b[28;3~": "alt+f15", "\x1b[29;3~": "alt+f16",
127
+ "\x1b[15;2~": "f17", "\x1b[17;2~": "f18",
128
+ "\x1b[18;2~": "f19", "\x1b[19;2~": "f20",
129
+ "\x1b[31~": "f17", "\x1b[32~": "f18", "\x1b[33~": "f19", "\x1b[34~": "f20",
130
+ # Bracketed paste markers
131
+ "\x1b[200~": "paste_start", "\x1b[201~": "paste_end",
132
+ # Focus events
133
+ "\x1b[I": "focus", "\x1b[O": "blur",
134
+ }
135
+
136
+ # Global reference for atexit cleanup
137
+ _active_terminal: Terminal | None = None
138
+ _cleanup_done: bool = False
139
+
140
+
141
+ def _cleanup_terminal() -> None:
142
+ """Atexit handler to restore terminal state."""
143
+ global _active_terminal, _cleanup_done
144
+
145
+ if _cleanup_done:
146
+ return
147
+ _cleanup_done = True
148
+
149
+ if _active_terminal is not None:
150
+ _active_terminal.stop()
151
+
152
+ # Fallback: run stty sane to ensure terminal is usable
153
+ import subprocess
154
+
155
+ try:
156
+ subprocess.run(["stty", "sane"], stdin=open("/dev/tty"), check=False)
157
+ except Exception:
158
+ pass
159
+
160
+
161
+ class TerminalProtocol(Protocol):
162
+ """Protocol for terminal implementations."""
163
+
164
+ def start(self, on_input: Callable[[str], None], on_resize: Callable[[], None]) -> None: ...
165
+ def stop(self) -> None: ...
166
+ def write(self, data: str) -> None: ...
167
+
168
+ @property
169
+ def columns(self) -> int: ...
170
+
171
+ @property
172
+ def rows(self) -> int: ...
173
+
174
+ def hide_cursor(self) -> None: ...
175
+ def show_cursor(self) -> None: ...
176
+ def clear_line(self) -> None: ...
177
+ def clear_from_cursor(self) -> None: ...
178
+ def clear_screen(self) -> None: ...
179
+
180
+
181
+ class Terminal:
182
+ """Real terminal using /dev/tty with raw mode support.
183
+
184
+ Features:
185
+ - /dev/tty for input (works even when stdin is piped)
186
+ - Non-blocking reads with escape sequence buffering
187
+ - Optional alternate screen buffer
188
+ - Optional bracketed paste mode
189
+ - Atexit cleanup for crash recovery
190
+ """
191
+
192
+ def __init__(
193
+ self,
194
+ *,
195
+ alternate_screen: bool = False,
196
+ use_alternate_screen: bool | None = None, # Compat alias
197
+ bracketed_paste: bool = False,
198
+ mouse: bool = False,
199
+ ) -> None:
200
+ self._old_settings: list | None = None
201
+ self._input_handler: Callable[[str], None] | None = None
202
+ self._resize_handler: Callable[[], None] | None = None
203
+ self._old_sigwinch: Any = None
204
+ self._running = False
205
+ self._tty_fd: int | None = None
206
+ self._input_buffer: str = "" # Buffer for multi-byte reads
207
+ self._paste_buffer: str | None = None # Collecting paste content
208
+ # Accept both alternate_screen and use_alternate_screen (compat)
209
+ if use_alternate_screen is not None:
210
+ self._alternate_screen = use_alternate_screen
211
+ else:
212
+ self._alternate_screen = alternate_screen
213
+ self._bracketed_paste = bracketed_paste
214
+ self._mouse = mouse
215
+
216
+ def start(self, on_input: Callable[[str], None], on_resize: Callable[[], None]) -> None:
217
+ """Start terminal in raw mode with input/resize handlers."""
218
+ global _active_terminal, _cleanup_done
219
+
220
+ self._input_handler = on_input
221
+ self._resize_handler = on_resize
222
+
223
+ _active_terminal = self
224
+ _cleanup_done = False
225
+ atexit.register(_cleanup_terminal)
226
+
227
+ # Open /dev/tty for keyboard input
228
+ try:
229
+ self._tty_fd = os.open("/dev/tty", os.O_RDONLY | os.O_NONBLOCK)
230
+ self._old_settings = termios.tcgetattr(self._tty_fd)
231
+ tty.setraw(self._tty_fd)
232
+ except (OSError, termios.error):
233
+ self._tty_fd = None
234
+ if sys.stdin.isatty():
235
+ self._old_settings = termios.tcgetattr(sys.stdin.fileno())
236
+ tty.setraw(sys.stdin.fileno())
237
+
238
+ # Enter alternate screen buffer
239
+ if self._alternate_screen:
240
+ sys.stdout.write(ALT_SCREEN_ON)
241
+
242
+ # Enable bracketed paste mode
243
+ if self._bracketed_paste:
244
+ sys.stdout.write(BRACKETED_PASTE_ON)
245
+
246
+ # Enable mouse tracking
247
+ if self._mouse:
248
+ sys.stdout.write(MOUSE_ON)
249
+
250
+ sys.stdout.flush()
251
+
252
+ # SIGWINCH for resize events
253
+ self._old_sigwinch = signal.signal(signal.SIGWINCH, self._handle_sigwinch)
254
+ self._running = True
255
+
256
+ def stop(self) -> None:
257
+ """Stop terminal and restore previous settings."""
258
+ global _active_terminal, _cleanup_done
259
+
260
+ if not self._running and self._old_settings is None:
261
+ return
262
+
263
+ self._running = False
264
+ _cleanup_done = True
265
+
266
+ # Restore terminal to clean state
267
+ sys.stdout.write(CURSOR_SHOW)
268
+
269
+ if self._bracketed_paste:
270
+ sys.stdout.write(BRACKETED_PASTE_OFF)
271
+
272
+ if self._mouse:
273
+ sys.stdout.write(MOUSE_OFF)
274
+
275
+ # End synchronized output (in case we're mid-render)
276
+ sys.stdout.write(SYNC_OUTPUT_OFF)
277
+ sys.stdout.write(RESET_ATTRS)
278
+
279
+ if self._alternate_screen:
280
+ sys.stdout.write(ALT_SCREEN_OFF)
281
+ else:
282
+ sys.stdout.write("\n")
283
+
284
+ sys.stdout.flush()
285
+
286
+ # Restore terminal settings
287
+ if self._old_settings is not None:
288
+ if self._tty_fd is not None:
289
+ termios.tcsetattr(self._tty_fd, termios.TCSADRAIN, self._old_settings)
290
+ os.close(self._tty_fd)
291
+ self._tty_fd = None
292
+ elif sys.stdin.isatty():
293
+ termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_settings)
294
+ self._old_settings = None
295
+
296
+ # Restore SIGWINCH handler
297
+ if self._old_sigwinch is not None:
298
+ signal.signal(signal.SIGWINCH, self._old_sigwinch)
299
+ self._old_sigwinch = None
300
+
301
+ self._input_handler = None
302
+ self._resize_handler = None
303
+
304
+ _active_terminal = None
305
+ try:
306
+ atexit.unregister(_cleanup_terminal)
307
+ except Exception:
308
+ pass
309
+
310
+ def write(self, data: str) -> None:
311
+ """Write data to stdout."""
312
+ sys.stdout.write(data)
313
+ sys.stdout.flush()
314
+
315
+ @property
316
+ def columns(self) -> int:
317
+ """Terminal width in columns."""
318
+ return os.get_terminal_size().columns
319
+
320
+ @property
321
+ def rows(self) -> int:
322
+ """Terminal height in rows."""
323
+ return os.get_terminal_size().lines
324
+
325
+ def hide_cursor(self) -> None:
326
+ self.write(CURSOR_HIDE)
327
+
328
+ def show_cursor(self) -> None:
329
+ self.write(CURSOR_SHOW)
330
+
331
+ def clear_line(self) -> None:
332
+ self.write(CLEAR_LINE)
333
+
334
+ def clear_from_cursor(self) -> None:
335
+ self.write(CLEAR_TO_END)
336
+
337
+ def clear_screen(self) -> None:
338
+ self.write(CLEAR_SCREEN_HOME)
339
+
340
+ def move_cursor(self, row: int, col: int) -> None:
341
+ """Move cursor to position (1-indexed)."""
342
+ self.write(f"\x1b[{row};{col}H")
343
+
344
+ def read_input(self) -> str | None:
345
+ """Read available input (non-blocking).
346
+
347
+ Returns None if no input available. Uses buffered reads like bubbletea
348
+ to reduce syscall overhead (read up to 256 bytes at once).
349
+
350
+ Escape sequences are detected by looking for terminators (letters, ~).
351
+ Returns one logical input at a time (single char or complete escape sequence).
352
+ """
353
+ fd = self._tty_fd if self._tty_fd is not None else sys.stdin.fileno()
354
+
355
+ # Check buffer first
356
+ if not self._input_buffer:
357
+ if not select.select([fd], [], [], 0)[0]:
358
+ return None
359
+ # Read up to 256 bytes at once (like bubbletea) to reduce syscalls
360
+ data = os.read(fd, 256).decode("utf-8", errors="replace")
361
+ if not data:
362
+ return None
363
+ self._input_buffer = data
364
+
365
+ # Extract one logical input from buffer
366
+ buf = self._input_buffer
367
+
368
+ if buf[0] != "\x1b":
369
+ # Regular character
370
+ self._input_buffer = buf[1:]
371
+ return buf[0]
372
+
373
+ # Escape sequence - find the end
374
+ if len(buf) == 1:
375
+ # Just ESC, might be incomplete - try to read more
376
+ if select.select([fd], [], [], 0.005)[0]: # 5ms wait
377
+ more = os.read(fd, 256).decode("utf-8", errors="replace")
378
+ if more:
379
+ buf = buf + more
380
+ self._input_buffer = buf
381
+
382
+ # Check against known sequences first (O(1) lookup, longest match first)
383
+ for length in range(min(len(buf), 10), 1, -1): # Check longest first
384
+ candidate = buf[:length]
385
+ if candidate in KNOWN_SEQUENCES:
386
+ self._input_buffer = buf[length:]
387
+ return candidate
388
+
389
+ # Fall back to heuristic: look for sequence terminator
390
+ for i in range(1, len(buf)):
391
+ c = buf[i]
392
+ # CSI sequences end with letter, function keys with ~
393
+ if c.isalpha() or c == "~":
394
+ seq = buf[: i + 1]
395
+ self._input_buffer = buf[i + 1 :]
396
+ return seq
397
+
398
+ # No terminator found - if buffer is small, wait for more
399
+ if len(buf) < 12:
400
+ deadline = time.time() + 0.010 # 10ms max
401
+ while time.time() < deadline:
402
+ if select.select([fd], [], [], 0.001)[0]:
403
+ more = os.read(fd, 256).decode("utf-8", errors="replace")
404
+ if more:
405
+ buf = buf + more
406
+ self._input_buffer = buf
407
+ # Check known sequences
408
+ for length in range(min(len(buf), 10), 1, -1):
409
+ candidate = buf[:length]
410
+ if candidate in KNOWN_SEQUENCES:
411
+ self._input_buffer = buf[length:]
412
+ return candidate
413
+ # Check for terminator
414
+ for i in range(1, len(buf)):
415
+ c = buf[i]
416
+ if c.isalpha() or c == "~":
417
+ seq = buf[: i + 1]
418
+ self._input_buffer = buf[i + 1 :]
419
+ return seq
420
+ else:
421
+ break
422
+
423
+ # Still no terminator - return bare ESC, keep rest in buffer
424
+ self._input_buffer = buf[1:]
425
+ return buf[0]
426
+
427
+ def run_external_editor(self, initial_content: str = "") -> str | None:
428
+ """Temporarily exit raw mode, run $EDITOR, return edited content."""
429
+ import subprocess
430
+ import tempfile
431
+
432
+ editor = os.environ.get("EDITOR", os.environ.get("VISUAL", "vim"))
433
+
434
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
435
+ f.write(initial_content)
436
+ temp_path = f.name
437
+
438
+ try:
439
+ # Restore cooked mode
440
+ if self._old_settings is not None:
441
+ if self._tty_fd is not None:
442
+ termios.tcsetattr(self._tty_fd, termios.TCSADRAIN, self._old_settings)
443
+ elif sys.stdin.isatty():
444
+ termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_settings)
445
+
446
+ if self._bracketed_paste:
447
+ sys.stdout.write(BRACKETED_PASTE_OFF)
448
+ sys.stdout.write(CURSOR_SHOW)
449
+ sys.stdout.write(CLEAR_SCREEN_HOME)
450
+ sys.stdout.flush()
451
+
452
+ with open("/dev/tty") as tty_in, open("/dev/tty", "w") as tty_out:
453
+ result = subprocess.run(
454
+ [editor, temp_path],
455
+ stdin=tty_in,
456
+ stdout=tty_out,
457
+ stderr=tty_out,
458
+ )
459
+
460
+ if result.returncode == 0:
461
+ with open(temp_path) as f:
462
+ content = f.read()
463
+ return content.strip() if content.strip() else None
464
+ return None
465
+
466
+ finally:
467
+ try:
468
+ os.unlink(temp_path)
469
+ except OSError:
470
+ pass
471
+
472
+ # Restore raw mode
473
+ if self._tty_fd is not None:
474
+ tty.setraw(self._tty_fd)
475
+ elif sys.stdin.isatty():
476
+ tty.setraw(sys.stdin.fileno())
477
+
478
+ if self._bracketed_paste:
479
+ sys.stdout.write(BRACKETED_PASTE_ON)
480
+ sys.stdout.write(CURSOR_HIDE)
481
+ sys.stdout.write(CLEAR_SCREEN_HOME)
482
+ sys.stdout.flush()
483
+
484
+ if self._resize_handler:
485
+ self._resize_handler()
486
+
487
+ def _handle_sigwinch(self, signum: int, frame: FrameType | None) -> None:
488
+ if self._resize_handler:
489
+ self._resize_handler()