batrachian-toad 0.5.22__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 (120) hide show
  1. batrachian_toad-0.5.22.dist-info/METADATA +197 -0
  2. batrachian_toad-0.5.22.dist-info/RECORD +120 -0
  3. batrachian_toad-0.5.22.dist-info/WHEEL +4 -0
  4. batrachian_toad-0.5.22.dist-info/entry_points.txt +2 -0
  5. batrachian_toad-0.5.22.dist-info/licenses/LICENSE +661 -0
  6. toad/__init__.py +46 -0
  7. toad/__main__.py +4 -0
  8. toad/_loop.py +86 -0
  9. toad/about.py +90 -0
  10. toad/acp/agent.py +671 -0
  11. toad/acp/api.py +47 -0
  12. toad/acp/encode_tool_call_id.py +12 -0
  13. toad/acp/messages.py +138 -0
  14. toad/acp/prompt.py +54 -0
  15. toad/acp/protocol.py +426 -0
  16. toad/agent.py +62 -0
  17. toad/agent_schema.py +70 -0
  18. toad/agents.py +45 -0
  19. toad/ansi/__init__.py +1 -0
  20. toad/ansi/_ansi.py +1612 -0
  21. toad/ansi/_ansi_colors.py +264 -0
  22. toad/ansi/_control_codes.py +37 -0
  23. toad/ansi/_keys.py +251 -0
  24. toad/ansi/_sgr_styles.py +64 -0
  25. toad/ansi/_stream_parser.py +418 -0
  26. toad/answer.py +22 -0
  27. toad/app.py +557 -0
  28. toad/atomic.py +37 -0
  29. toad/cli.py +257 -0
  30. toad/code_analyze.py +28 -0
  31. toad/complete.py +34 -0
  32. toad/constants.py +58 -0
  33. toad/conversation_markdown.py +19 -0
  34. toad/danger.py +371 -0
  35. toad/data/agents/ampcode.com.toml +51 -0
  36. toad/data/agents/augmentcode.com.toml +40 -0
  37. toad/data/agents/claude.com.toml +41 -0
  38. toad/data/agents/docker.com.toml +59 -0
  39. toad/data/agents/geminicli.com.toml +28 -0
  40. toad/data/agents/goose.ai.toml +51 -0
  41. toad/data/agents/inference.huggingface.co.toml +33 -0
  42. toad/data/agents/kimi.com.toml +35 -0
  43. toad/data/agents/openai.com.toml +53 -0
  44. toad/data/agents/opencode.ai.toml +61 -0
  45. toad/data/agents/openhands.dev.toml +44 -0
  46. toad/data/agents/stakpak.dev.toml +61 -0
  47. toad/data/agents/vibe.mistral.ai.toml +27 -0
  48. toad/data/agents/vtcode.dev.toml +62 -0
  49. toad/data/images/frog.png +0 -0
  50. toad/data/sounds/turn-over.wav +0 -0
  51. toad/db.py +5 -0
  52. toad/dec.py +332 -0
  53. toad/directory.py +234 -0
  54. toad/directory_watcher.py +96 -0
  55. toad/fuzzy.py +140 -0
  56. toad/gist.py +2 -0
  57. toad/history.py +138 -0
  58. toad/jsonrpc.py +576 -0
  59. toad/menus.py +14 -0
  60. toad/messages.py +74 -0
  61. toad/option_content.py +51 -0
  62. toad/os.py +0 -0
  63. toad/path_complete.py +145 -0
  64. toad/path_filter.py +124 -0
  65. toad/paths.py +71 -0
  66. toad/pill.py +23 -0
  67. toad/prompt/extract.py +19 -0
  68. toad/prompt/resource.py +68 -0
  69. toad/protocol.py +28 -0
  70. toad/screens/action_modal.py +94 -0
  71. toad/screens/agent_modal.py +172 -0
  72. toad/screens/command_edit_modal.py +58 -0
  73. toad/screens/main.py +192 -0
  74. toad/screens/permissions.py +390 -0
  75. toad/screens/permissions.tcss +72 -0
  76. toad/screens/settings.py +254 -0
  77. toad/screens/settings.tcss +101 -0
  78. toad/screens/store.py +476 -0
  79. toad/screens/store.tcss +261 -0
  80. toad/settings.py +354 -0
  81. toad/settings_schema.py +318 -0
  82. toad/shell.py +263 -0
  83. toad/shell_read.py +42 -0
  84. toad/slash_command.py +34 -0
  85. toad/toad.tcss +752 -0
  86. toad/version.py +80 -0
  87. toad/visuals/columns.py +273 -0
  88. toad/widgets/agent_response.py +79 -0
  89. toad/widgets/agent_thought.py +41 -0
  90. toad/widgets/command_pane.py +224 -0
  91. toad/widgets/condensed_path.py +93 -0
  92. toad/widgets/conversation.py +1626 -0
  93. toad/widgets/danger_warning.py +65 -0
  94. toad/widgets/diff_view.py +709 -0
  95. toad/widgets/flash.py +81 -0
  96. toad/widgets/future_text.py +126 -0
  97. toad/widgets/grid_select.py +223 -0
  98. toad/widgets/highlighted_textarea.py +180 -0
  99. toad/widgets/mandelbrot.py +294 -0
  100. toad/widgets/markdown_note.py +13 -0
  101. toad/widgets/menu.py +147 -0
  102. toad/widgets/non_selectable_label.py +5 -0
  103. toad/widgets/note.py +18 -0
  104. toad/widgets/path_search.py +381 -0
  105. toad/widgets/plan.py +180 -0
  106. toad/widgets/project_directory_tree.py +74 -0
  107. toad/widgets/prompt.py +741 -0
  108. toad/widgets/question.py +337 -0
  109. toad/widgets/shell_result.py +35 -0
  110. toad/widgets/shell_terminal.py +18 -0
  111. toad/widgets/side_bar.py +74 -0
  112. toad/widgets/slash_complete.py +211 -0
  113. toad/widgets/strike_text.py +66 -0
  114. toad/widgets/terminal.py +526 -0
  115. toad/widgets/terminal_tool.py +338 -0
  116. toad/widgets/throbber.py +90 -0
  117. toad/widgets/tool_call.py +303 -0
  118. toad/widgets/user_input.py +23 -0
  119. toad/widgets/version.py +5 -0
  120. toad/widgets/welcome.py +31 -0
@@ -0,0 +1,66 @@
1
+ from time import monotonic
2
+
3
+ from textual.widget import Widget
4
+
5
+ from textual.content import Content
6
+ from textual.reactive import reactive
7
+
8
+
9
+ class StrikeText(Widget):
10
+ DEFAULT_CSS = """
11
+ StrikeText {
12
+ height: auto;
13
+ }
14
+ """
15
+
16
+ strike_time: reactive[float | None] = reactive(None)
17
+
18
+ def __init__(
19
+ self,
20
+ content: Content,
21
+ name: str | None = None,
22
+ id: str | None = None,
23
+ classes: str | None = None,
24
+ ):
25
+ self.content = content
26
+ super().__init__(name=name, id=id, classes=classes)
27
+
28
+ def strike(self) -> None:
29
+ self.strike_time = monotonic()
30
+ self.auto_refresh = 1 / 30
31
+
32
+ def render(self) -> Content:
33
+ content = self.content
34
+ if self.strike_time is not None:
35
+ position = int((monotonic() - self.strike_time) * 70)
36
+ content = content.stylize("strike", 0, position)
37
+ if position > len(content):
38
+ self.auto_refresh = None
39
+ return content
40
+
41
+
42
+ if __name__ == "__main__":
43
+ from textual.app import App, ComposeResult
44
+ from textual.widgets import Static
45
+
46
+ class StrikeApp(App):
47
+ CSS = """
48
+ Screen {
49
+ overflow: auto;
50
+ }
51
+
52
+ """
53
+ BINDINGS = [("space", "strike", "Strike")]
54
+
55
+ def compose(self) -> ComposeResult:
56
+ for n in range(20):
57
+ yield Static("HELLO")
58
+ yield StrikeText(Content("Where there is a Will, there is a way"))
59
+ for n in range(200):
60
+ yield Static("World")
61
+
62
+ def action_strike(self):
63
+ self.query_one(StrikeText).strike()
64
+
65
+ app = StrikeApp()
66
+ app.run()
@@ -0,0 +1,526 @@
1
+ from dataclasses import dataclass
2
+
3
+ from time import monotonic
4
+ from typing import Any, Awaitable, Callable
5
+
6
+ from textual.cache import LRUCache
7
+
8
+ from textual import on
9
+ from textual import events
10
+ from textual.css.query import NoMatches
11
+ from textual.message import Message
12
+ from textual.reactive import reactive
13
+ from textual.selection import Selection
14
+ from textual.style import Style
15
+ from textual.geometry import Region, Size
16
+ from textual.scroll_view import ScrollView
17
+ from textual.strip import Strip
18
+ from textual.timer import Timer
19
+
20
+
21
+ from toad import ansi
22
+
23
+
24
+ # Time required to double tab escape
25
+ ESCAPE_TAP_DURATION = 400 / 1000
26
+
27
+
28
+ class Terminal(ScrollView, can_focus=True):
29
+ CURSOR_STYLE = Style.parse("reverse")
30
+
31
+ hide_cursor = reactive(False)
32
+
33
+ @dataclass
34
+ class Finalized(Message):
35
+ """Terminal was finalized."""
36
+
37
+ terminal: Terminal
38
+
39
+ @property
40
+ def control(self) -> Terminal:
41
+ return self.terminal
42
+
43
+ @dataclass
44
+ class AlternateScreenChanged(Message):
45
+ """Terminal enabled or disabled alternate screen."""
46
+
47
+ terminal: Terminal
48
+ enabled: bool
49
+
50
+ @property
51
+ def control(self) -> Terminal:
52
+ return self.terminal
53
+
54
+ def __init__(
55
+ self,
56
+ name: str | None = None,
57
+ id: str | None = None,
58
+ classes: str | None = None,
59
+ disabled: bool = False,
60
+ minimum_terminal_width: int = 0,
61
+ size: tuple[int, int] | None = None,
62
+ get_terminal_dimensions: Callable[[], tuple[int, int]] | None = None,
63
+ ):
64
+ super().__init__(
65
+ name=name,
66
+ id=id,
67
+ classes=classes,
68
+ disabled=disabled,
69
+ )
70
+ self.minimum_terminal_width = minimum_terminal_width
71
+ self._get_terminal_dimensions = get_terminal_dimensions
72
+
73
+ self.state = ansi.TerminalState(self.write_process_stdin)
74
+
75
+ if size is None:
76
+ self._width = minimum_terminal_width or 80
77
+ self._height: int = 24
78
+ else:
79
+ width, height = size
80
+ self._width = width
81
+ self._height = height
82
+
83
+ self.minimum_terminal_width = self._width
84
+
85
+ self.max_window_width = 0
86
+ self._escape_time = monotonic()
87
+ self._escaping = False
88
+ self._escape_reset_timer: Timer | None = None
89
+ self._finalized: bool = False
90
+ self.current_directory: str | None = None
91
+ self._alternate_screen: bool = False
92
+ self._terminal_render_cache: LRUCache[tuple, Strip] = LRUCache(1024)
93
+ self._write_to_stdin: Callable[[str], Awaitable] | None = None
94
+
95
+ @property
96
+ def is_finalized(self) -> bool:
97
+ """Finalized terminals will not accept writes or receive input."""
98
+ return self._finalized
99
+
100
+ @property
101
+ def width(self) -> int:
102
+ """Width of the terminal."""
103
+ return self._width
104
+
105
+ @property
106
+ def height(self) -> int:
107
+ """Height of the terminal."""
108
+ height = self._height
109
+ return height
110
+
111
+ @property
112
+ def size(self) -> Size:
113
+ return Size(self.width, self.height)
114
+
115
+ @property
116
+ def alternate_screen(self) -> bool:
117
+ return self._alternate_screen
118
+
119
+ def notify_style_update(self) -> None:
120
+ """Clear cache when theme chages."""
121
+ self._terminal_render_cache.clear()
122
+ super().notify_style_update()
123
+
124
+ def set_state(self, state: ansi.TerminalState) -> None:
125
+ """Set the terminal state, if this terminal is to inherit an existing state.
126
+
127
+ Args:
128
+ state: Terminal state object.
129
+ """
130
+ self.state = state
131
+
132
+ def set_write_to_stdin(self, write_to_stdin: Callable[[str], Awaitable]) -> None:
133
+ """Set a callable which is invoked with input, to be sent to stdin.
134
+
135
+ Args:
136
+ write_to_stdin: Callable which takes a string.
137
+ """
138
+ self._write_to_stdin = write_to_stdin
139
+
140
+ def finalize(self) -> None:
141
+ """FInalize the terminal.
142
+
143
+ The finalized terminal will reject new writes.
144
+ Adds the TCSS class `-finalize`
145
+ """
146
+ if not self._finalized:
147
+ self._finalized = True
148
+ self.state.show_cursor = False
149
+ self.add_class("-finalized")
150
+ self._terminal_render_cache.clear()
151
+ self.refresh()
152
+ self.blur()
153
+ self.post_message(self.Finalized(self))
154
+ self.state.remove_trailing_blank_lines_from_scrollback()
155
+
156
+ def allow_focus(self) -> bool:
157
+ """Prohibit focus when the terminal is finalized and couldn't accept input."""
158
+ return not self.is_finalized
159
+
160
+ def get_selection(self, selection: Selection) -> tuple[str, str] | None:
161
+ """Get the text under the selection.
162
+
163
+ Args:
164
+ selection: Selection information.
165
+
166
+ Returns:
167
+ Tuple of extracted text and ending (typically "\n" or " "), or `None` if no text could be extracted.
168
+ """
169
+ text = "\n".join(
170
+ line_record.content.plain for line_record in self.state.buffer.lines
171
+ )
172
+ return selection.extract(text), "\n"
173
+
174
+ def _on_resize(self, event: events.Resize) -> None:
175
+ if self._get_terminal_dimensions is None:
176
+ width, height = self.scrollable_content_region.size
177
+ else:
178
+ width, height = self._get_terminal_dimensions()
179
+ self.update_size(width, height)
180
+
181
+ def update_size(self, width: int, height: int) -> None:
182
+ old_width = self._width
183
+ old_height = self._height
184
+ self._terminal_render_cache.grow(height * 2)
185
+ self._width = width or 80
186
+ self._height = height or 24
187
+ self._width = max(self._width, self.minimum_terminal_width)
188
+
189
+ if (
190
+ old_width != self._width
191
+ or old_height != self._height
192
+ and not self.is_finalized
193
+ ):
194
+ from toad.widgets.conversation import Conversation
195
+
196
+ try:
197
+ conversation = self.query_ancestor(Conversation)
198
+ except NoMatches:
199
+ pass
200
+ else:
201
+ conversation.shell.update_size(self._width, self._height)
202
+
203
+ self.state.update_size(self._width, height)
204
+ self._terminal_render_cache.clear()
205
+ self.refresh()
206
+
207
+ def on_mount(self) -> None:
208
+ self.auto_links = False
209
+ self.anchor()
210
+ if self._get_terminal_dimensions is None:
211
+ width, height = self.scrollable_content_region.size
212
+ else:
213
+ width, height = self._get_terminal_dimensions()
214
+ self.update_size(width, height)
215
+
216
+ async def write(self, text: str, hide_output: bool=False) -> bool:
217
+ """Write sequences to the terminal.
218
+
219
+ Args:
220
+ text: Text with ANSI escape sequences.
221
+ hide_output: Do not update the buffers with visible text.
222
+
223
+ Returns:
224
+ `True` if the state visuals changed, `False` if no visual change.
225
+ """
226
+ scrollback_delta, alternate_delta = await self.state.write(
227
+ text, hide_output=hide_output
228
+ )
229
+ self._update_from_state(scrollback_delta, alternate_delta)
230
+ scrollback_changed = bool(scrollback_delta is None or scrollback_delta)
231
+ alternate_changed = bool(alternate_delta is None or alternate_delta)
232
+
233
+ if self._alternate_screen != self.state.alternate_screen:
234
+ self.post_message(
235
+ self.AlternateScreenChanged(self, enabled=self.state.alternate_screen)
236
+ )
237
+ self._alternate_screen = self.state.alternate_screen
238
+ return scrollback_changed or alternate_changed
239
+
240
+ def on_click(self, event: events.Click) -> None:
241
+ self.focus()
242
+ event.stop()
243
+
244
+ def _update_from_state(
245
+ self, scrollback_delta: set[int] | None, alternate_delta: set[int] | None
246
+ ) -> None:
247
+ if self.state.current_directory:
248
+ self.current_directory = self.state.current_directory
249
+ self.finalize()
250
+ width = self.state.width
251
+ height = self.state.scrollback_buffer.height
252
+
253
+ if self.state.alternate_screen:
254
+ height += self.state.alternate_buffer.height
255
+ self.virtual_size = Size(min(self.state.buffer.max_line_width, width), height)
256
+ if self._anchored and not self._anchor_released:
257
+ self.scroll_y = self.max_scroll_y
258
+
259
+ scroll_y = int(self.scroll_y)
260
+ visible_lines = frozenset(range(scroll_y, scroll_y + height))
261
+
262
+ if scrollback_delta is None and alternate_delta is None:
263
+ self.refresh()
264
+ else:
265
+ window_width = self.region.width
266
+ scrollback_height = self.state.scrollback_buffer.line_count
267
+ if scrollback_delta is None:
268
+ self.refresh(Region(0, 0, window_width, scrollback_height))
269
+ else:
270
+ refresh_lines = [
271
+ Region(0, y - scroll_y, window_width, 1)
272
+ for y in sorted(scrollback_delta & visible_lines)
273
+ ]
274
+ if refresh_lines:
275
+ self.refresh(*refresh_lines)
276
+ alternate_height = self.state.alternate_buffer.line_count
277
+ if alternate_delta is None:
278
+ self.refresh(
279
+ Region(
280
+ 0,
281
+ scrollback_height - scroll_y,
282
+ window_width,
283
+ scrollback_height + alternate_height,
284
+ )
285
+ )
286
+ else:
287
+ alternate_delta = {
288
+ line_no + scrollback_height for line_no in alternate_delta
289
+ }
290
+ refresh_lines = [
291
+ Region(0, y - scroll_y, window_width, 1)
292
+ for y in sorted(alternate_delta & visible_lines)
293
+ ]
294
+ if refresh_lines:
295
+ self.refresh(*refresh_lines)
296
+
297
+ def render_line(self, y: int) -> Strip:
298
+ scroll_x, scroll_y = self.scroll_offset
299
+ strip = self._render_line(scroll_x, scroll_y + y, self._width)
300
+ return strip
301
+
302
+ def on_focus(self) -> None:
303
+ self.border_subtitle = "Tap [b]esc[/b] [i]twice[/i] to exit"
304
+
305
+ def on_blur(self) -> None:
306
+ self.border_subtitle = "Click to focus"
307
+
308
+ def _render_line(self, x: int, y: int, width: int) -> Strip:
309
+ selection = self.text_selection
310
+ visual_style = self.visual_style
311
+ rich_style = visual_style.rich_style
312
+
313
+ state = self.state
314
+ buffer = state.scrollback_buffer
315
+ buffer_offset = 0
316
+ # If alternate screen is active place it (virtually) at the end
317
+ if y >= len(buffer.folded_lines) and state.alternate_screen:
318
+ buffer_offset = len(buffer.folded_lines)
319
+ buffer = state.alternate_buffer
320
+ # Get the folded line, which as a one to one relationship with y
321
+ try:
322
+ folded_line_ = buffer.folded_lines[y - buffer_offset]
323
+ line_no, line_offset, offset, line, updates = folded_line_
324
+ except IndexError:
325
+ return Strip.blank(width, rich_style)
326
+
327
+ line_record = buffer.lines[line_no]
328
+ cache_key: tuple | None = (
329
+ self.state.alternate_screen,
330
+ y,
331
+ line_record.updates,
332
+ updates,
333
+ )
334
+
335
+ # Add in cursor
336
+ if (
337
+ not self.hide_cursor
338
+ and state.show_cursor
339
+ and buffer.cursor_line == y - buffer_offset
340
+ ):
341
+ if buffer.cursor_offset >= len(line):
342
+ line = line.pad_right(buffer.cursor_offset - len(line) + 1)
343
+ line_cursor_offset = buffer.cursor_offset
344
+ line = line.stylize(
345
+ self.CURSOR_STYLE, line_cursor_offset, line_cursor_offset + 1
346
+ )
347
+ cache_key = None
348
+
349
+ # get cached strip if there is no selection
350
+ if (
351
+ not selection
352
+ and cache_key is not None
353
+ and (strip := self._terminal_render_cache.get(cache_key))
354
+ ):
355
+ strip = strip.crop(x, x + width)
356
+ strip = strip.adjust_cell_length(
357
+ width, (visual_style + line_record.style).rich_style
358
+ )
359
+ strip = strip.apply_offsets(x + offset, line_no)
360
+ return strip
361
+
362
+ # Apply selection
363
+ if selection is not None and (select_span := selection.get_span(line_no)):
364
+ unfolded_content = line_record.content.expand_tabs(8)
365
+ start, end = select_span
366
+ if end == -1:
367
+ end = len(unfolded_content)
368
+ selection_style = self.screen.get_visual_style("screen--selection")
369
+ unfolded_content = unfolded_content.stylize(selection_style, start, end)
370
+ try:
371
+ folded_lines = self.state._fold_line(line_no, unfolded_content, width)
372
+ line = folded_lines[line_offset].content
373
+ cache_key = None
374
+ except IndexError:
375
+ pass
376
+
377
+ try:
378
+ strip = Strip(
379
+ line.render_segments(visual_style), cell_length=line.cell_length
380
+ )
381
+ except Exception:
382
+ # TODO: Is this neccesary?
383
+ strip = Strip.blank(line.cell_length)
384
+
385
+ if cache_key is not None:
386
+ self._terminal_render_cache[cache_key] = strip
387
+
388
+ strip = strip.crop(x, x + width)
389
+ strip = strip.adjust_cell_length(
390
+ width, (visual_style + line_record.style).rich_style
391
+ )
392
+ strip = strip.apply_offsets(x + offset, line_no)
393
+
394
+ return strip
395
+
396
+ async def _reset_escaping(self) -> None:
397
+ if self._escaping:
398
+ await self.write_process_stdin(self.state.key_escape())
399
+ self._escaping = False
400
+
401
+ async def on_key(self, event: events.Key):
402
+ event.prevent_default()
403
+ event.stop()
404
+
405
+ if event.key == "escape":
406
+ if self._escaping:
407
+ if monotonic() < self._escape_time + ESCAPE_TAP_DURATION:
408
+ self.blur()
409
+ self._escaping = False
410
+ return
411
+ else:
412
+ await self.write_process_stdin(self.state.key_escape())
413
+ else:
414
+ self._escaping = True
415
+ self._escape_time = monotonic()
416
+ self._escape_reset_timer = self.set_timer(
417
+ ESCAPE_TAP_DURATION, self._reset_escaping
418
+ )
419
+ return
420
+ else:
421
+ await self._reset_escaping()
422
+ if self._escape_reset_timer is not None:
423
+ self._escape_reset_timer.stop()
424
+
425
+ if (stdin := self.state.key_event_to_stdin(event)) is not None:
426
+ await self.write_process_stdin(stdin)
427
+
428
+ @property
429
+ def allow_select(self) -> bool:
430
+ return self.is_finalized or not self._alternate_screen
431
+
432
+ def _encode_mouse_event_sgr(self, event: events.MouseEvent) -> str:
433
+ x = int(event.x)
434
+ y = int(event.y)
435
+
436
+ if isinstance(event, events.MouseMove):
437
+ button = event.button + 32 if event.button else 35
438
+ else:
439
+ button = event.button - 1
440
+ if button >= 4:
441
+ button = button - 4 + 128
442
+ if event.shift:
443
+ button += 4
444
+ if event.meta:
445
+ button += 8
446
+ if event.ctrl:
447
+ button += 16
448
+
449
+ if isinstance(event, events.MouseDown):
450
+ final_character = "M"
451
+ elif isinstance(event, events.MouseUp):
452
+ button = 0
453
+ final_character = "m"
454
+ else:
455
+ final_character = "M"
456
+ mouse_stdin = f"\x1b[<{button};{x + 1};{y + 1}{final_character}"
457
+ return mouse_stdin
458
+
459
+ @on(events.MouseMove)
460
+ async def on_mouse_move(self, event: events.MouseMove) -> None:
461
+ if self.is_finalized:
462
+ return
463
+ if (mouse_tracking := self.state.mouse_tracking) is None:
464
+ return
465
+ if mouse_tracking.tracking == "all" or (
466
+ event.button and mouse_tracking.tracking == "drag"
467
+ ):
468
+ await self._handle_mouse_event(event)
469
+ event.prevent_default()
470
+ event.stop()
471
+
472
+ @on(events.MouseDown)
473
+ @on(events.MouseUp)
474
+ async def on_mouse_button(self, event: events.MouseUp | events.MouseDown) -> None:
475
+ if self.is_finalized:
476
+ return
477
+ if self.state.mouse_tracking is None:
478
+ return
479
+ await self._handle_mouse_event(event)
480
+ event.prevent_default()
481
+ event.stop()
482
+
483
+ async def _handle_mouse_event(self, event: events.MouseEvent) -> None:
484
+ if (mouse_tracking := self.state.mouse_tracking) is None:
485
+ return
486
+ # TODO: Other mouse tracking formats
487
+ match mouse_tracking.format:
488
+ case "sgr":
489
+ await self.write_process_stdin(self._encode_mouse_event_sgr(event))
490
+
491
+ async def on_paste(self, event: events.Paste) -> None:
492
+ for character in event.text:
493
+ await self.write_process_stdin(character)
494
+
495
+ async def write_process_stdin(self, input: str) -> None:
496
+ if self._write_to_stdin is not None:
497
+ await self._write_to_stdin(input)
498
+
499
+
500
+ if __name__ == "__main__":
501
+ from textual.app import App, ComposeResult
502
+
503
+ TEST = (
504
+ "\033[31mThis is red text\033[0m\n"
505
+ "\033[32mThis is green text\033[0m\n"
506
+ "\033[33mThis is yellow text\033[0m\n"
507
+ "\033[34mThis is blue text\033[0m\n"
508
+ "\033[35mThis is magenta text\033[0m\n"
509
+ "\033[36mThis is cyan text\033[0m\n"
510
+ "\033[1mThis is bold text\033[0m\n"
511
+ "\033[4mThis is underlined text\033[0m\n"
512
+ "\033[1;31mThis is bold red text\033[0m\n"
513
+ "\033[42mThis has a green background\033[0m\n"
514
+ "\033[97;44mWhite text on blue background\033[0m"
515
+ )
516
+
517
+ class TApp(App):
518
+ def compose(self) -> ComposeResult:
519
+ yield Terminal()
520
+
521
+ def on_mount(self) -> None:
522
+ terminal = self.query_one(Terminal)
523
+ terminal.write(TEST)
524
+
525
+ app = TApp()
526
+ app.run()