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
toad/widgets/flash.py ADDED
@@ -0,0 +1,81 @@
1
+ from typing import Literal
2
+
3
+ from textual.content import Content
4
+ from textual.reactive import var
5
+ from textual.widgets import Static
6
+ from textual.timer import Timer
7
+ from textual import getters
8
+
9
+
10
+ from toad.app import ToadApp
11
+
12
+
13
+ class Flash(Static):
14
+ DEFAULT_CSS = """
15
+ Flash {
16
+ height: 1;
17
+ width: 1fr;
18
+ background: $success 10%;
19
+ color: $text-success;
20
+ text-align: center;
21
+ visibility: hidden;
22
+ text-wrap: nowrap;
23
+ text-overflow: ellipsis;
24
+ # overlay: screen;
25
+ # offset-y: -1;
26
+ &.-default {
27
+ background: $primary 10%;
28
+ color: $text-primary;
29
+ }
30
+
31
+ &.-success {
32
+ background: $success 10%;
33
+ color: $text-success;
34
+ }
35
+
36
+
37
+ &.-warning {
38
+ background: $warning 10%;
39
+ color: $text-warning;
40
+ }
41
+
42
+ &.-error {
43
+ background: $error 10%;
44
+ color: $text-error;
45
+ }
46
+ }
47
+ """
48
+ app = getters.app(ToadApp)
49
+ flash_timer: var[Timer | None] = var(None)
50
+
51
+ def flash(
52
+ self,
53
+ content: str | Content,
54
+ *,
55
+ duration: float | None = None,
56
+ style: Literal["default", "success", "warning", "error"] = "default",
57
+ ) -> None:
58
+ """Flash the content for a brief period.
59
+
60
+ Args:
61
+ content: Content to show.
62
+ duration: Duration in seconds to show content.
63
+ style: A semantic style.
64
+ """
65
+ if self.flash_timer is not None:
66
+ self.flash_timer.stop()
67
+ self.visible = False
68
+
69
+ def hide() -> None:
70
+ """Hide the content after a while."""
71
+ self.visible = False
72
+
73
+ self.update(content)
74
+ self.remove_class("-default", "-success", "-warning", "-error", update=False)
75
+ self.add_class(f"-{style}")
76
+ self.visible = True
77
+
78
+ if duration is None:
79
+ duration = self.app.settings.get("ui.flash_duration", float)
80
+
81
+ self.flash_timer = self.set_timer(duration or 3, hide)
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ from math import ceil
4
+ from typing import ClassVar
5
+ from time import monotonic
6
+
7
+ from textual.color import Color
8
+ from textual.reactive import var
9
+ from textual.content import Content
10
+ from textual.style import Style
11
+ from textual.timer import Timer
12
+ from textual.widgets import Static
13
+
14
+
15
+ class FutureText(Static):
16
+ """Text which appears one letter at time, like the movies."""
17
+
18
+ DEFAULT_CSS = """
19
+ FutureText {
20
+ width: auto;
21
+ height: 1;
22
+ text-wrap: nowrap;
23
+ text-align: center;
24
+ color: $primary;
25
+ &>.future-text--cursor {
26
+ color: $primary;
27
+ }
28
+ }
29
+ """
30
+ ALLOW_SELECT = False
31
+ COMPONENT_CLASSES = {"future-text--cursor"}
32
+
33
+ BARS: ClassVar[list[str]] = ["▉", "▊", "▋", "▌", "▍", "▎", "▏", " "]
34
+ text_offset = var(0)
35
+
36
+ def __init__(
37
+ self,
38
+ text_list: list[Content],
39
+ *,
40
+ speed: float = 16.0,
41
+ name: str | None = None,
42
+ id: str | None = None,
43
+ classes: str | None = None,
44
+ ):
45
+ self.text_list = text_list
46
+ self.speed = speed
47
+ self.start_time = monotonic()
48
+ super().__init__(name=name, id=id, classes=classes)
49
+ self._update_timer: Timer | None = None
50
+
51
+ @property
52
+ def text(self) -> Content:
53
+ return self.text_list[self.text_offset % len(self.text_list)]
54
+
55
+ @property
56
+ def time(self) -> float:
57
+ return monotonic() - self.start_time
58
+
59
+ def on_mount(self) -> None:
60
+ self.start_time = monotonic()
61
+ self.set_interval(1 / 60, self._update_text)
62
+
63
+ def _update_text(self) -> None:
64
+ if not self.is_attached or not self.screen.is_active:
65
+ return
66
+ text = self.text + " "
67
+ speed_time = self.time * self.speed
68
+ progress, fractional_progress = divmod(speed_time, 1)
69
+ end = progress >= len(text)
70
+ cursor_progress = 0 if end else int(fractional_progress * 8)
71
+ text = text[: ceil(progress)]
72
+
73
+ bar_character = self.BARS[7 - cursor_progress]
74
+
75
+ cursor_styles = self.get_component_styles("future-text--cursor")
76
+ cursor_style = Style(foreground=cursor_styles.color)
77
+ reverse_cursor_style = cursor_style + Style(reverse=True)
78
+
79
+ # Fade in last character
80
+ fade_style = Style(
81
+ foreground=Color.blend(
82
+ cursor_styles.background, cursor_styles.color, ceil(fractional_progress)
83
+ )
84
+ )
85
+
86
+ fade_text = Content.assemble(
87
+ text[:-1],
88
+ ((text[-1].plain if text else " "), fade_style),
89
+ )
90
+
91
+ if speed_time >= 1:
92
+ text = Content.assemble(
93
+ fade_text,
94
+ (bar_character, reverse_cursor_style),
95
+ (bar_character, cursor_style),
96
+ " " * (len(self.text) + 1 - len(fade_text)),
97
+ )
98
+ self.update(text, layout=False)
99
+
100
+ if progress > len(text) + 10 * 5:
101
+ self.text_offset += 1
102
+ self.start_time = monotonic()
103
+
104
+
105
+ if __name__ == "__main__":
106
+ from textual.app import App, ComposeResult
107
+
108
+ TEXT = [Content("Thinking..."), Content("Working hard..."), Content("Nearly there")]
109
+
110
+ class TextApp(App):
111
+ CSS = """
112
+ Screen {
113
+ padding: 2 4;
114
+ FutureText {
115
+ width: auto;
116
+ max-width: 1fr;
117
+ height: auto;
118
+
119
+ }
120
+ }
121
+ """
122
+
123
+ def compose(self) -> ComposeResult:
124
+ yield FutureText(TEXT)
125
+
126
+ TextApp().run()
@@ -0,0 +1,223 @@
1
+ from dataclasses import dataclass
2
+
3
+ from textual import containers
4
+ from textual.binding import Binding
5
+ from textual import events
6
+ from textual.message import Message
7
+ from textual.reactive import reactive
8
+ from textual.layouts.grid import GridLayout
9
+ from textual.widget import Widget
10
+
11
+
12
+ class GridSelect(containers.ItemGrid, can_focus=True):
13
+ FOCUS_ON_CLICK = False
14
+ CURSOR_GROUP = Binding.Group("Select")
15
+ FOCUS_GROUP = Binding.Group("Focus")
16
+ BINDINGS = [
17
+ Binding("up", "cursor_up", "Cursor Up", group=CURSOR_GROUP),
18
+ Binding("down", "cursor_down", "Cursor Down", group=CURSOR_GROUP),
19
+ Binding("left", "cursor_left", "Cursor Left", group=CURSOR_GROUP),
20
+ Binding("right", "cursor_right", "Cursor Right", group=CURSOR_GROUP),
21
+ Binding("enter", "select", "Select"),
22
+ ]
23
+
24
+ highlighted: reactive[int | None] = reactive(None)
25
+
26
+ @dataclass
27
+ class Selected(Message):
28
+ grid_select: "GridSelect"
29
+ selected_widget: Widget
30
+
31
+ @property
32
+ def control(self) -> Widget:
33
+ return self.grid_select
34
+
35
+ @dataclass
36
+ class LeaveUp(Message):
37
+ grid_select: "GridSelect"
38
+
39
+ @dataclass
40
+ class LeaveDown(Message):
41
+ grid_select: "GridSelect"
42
+
43
+ def __init__(
44
+ self,
45
+ name: str | None = None,
46
+ id: str | None = None,
47
+ classes: str | None = None,
48
+ min_column_width: int = 30,
49
+ max_column_width: int | None = None,
50
+ ):
51
+ super().__init__(
52
+ name=name,
53
+ id=id,
54
+ classes=classes,
55
+ min_column_width=min_column_width,
56
+ max_column_width=max_column_width,
57
+ )
58
+
59
+ @property
60
+ def grid_size(self) -> tuple[int, int] | None:
61
+ assert isinstance(self.layout, GridLayout)
62
+ return self.layout.grid_size
63
+
64
+ def highlight_first(self) -> None:
65
+ self.highlighted = 0
66
+
67
+ def highlight_last(self) -> None:
68
+ if (grid_size := self.grid_size) is not None:
69
+ width, height = grid_size
70
+
71
+ if width == 1:
72
+ self.highlighted = len(self.children) - 1
73
+ else:
74
+ self.highlighted = (height - 1) * width
75
+
76
+ def on_focus(self):
77
+ if self.highlighted is None:
78
+ self.highlighted = 0
79
+ self.reveal_highlight()
80
+
81
+ def on_blur(self) -> None:
82
+ self.highlighted = None
83
+
84
+ def reveal_highlight(self):
85
+ if self.highlighted is None:
86
+ return
87
+ try:
88
+ highlighted_widget = self.children[self.highlighted]
89
+ except IndexError:
90
+ pass
91
+ else:
92
+ if not self.screen.can_view_entire(highlighted_widget):
93
+ self.screen.scroll_to_center(highlighted_widget, origin_visible=True)
94
+
95
+ def watch_highlighted(
96
+ self, old_highlighted: int | None, highlighted: int | None
97
+ ) -> None:
98
+ if old_highlighted is not None:
99
+ try:
100
+ self.children[old_highlighted].remove_class("-highlight")
101
+ except IndexError:
102
+ pass
103
+ if highlighted is not None:
104
+ try:
105
+ highlighted_widget = self.children[highlighted]
106
+ highlighted_widget.add_class("-highlight")
107
+ except IndexError:
108
+ pass
109
+ self.reveal_highlight()
110
+
111
+ def validate_highlighted(self, highlighted: int | None) -> int | None:
112
+ if highlighted is None:
113
+ return None
114
+
115
+ if not self.children:
116
+ return None
117
+ if highlighted < 0:
118
+ return 0
119
+ if highlighted >= len(self.children):
120
+ return len(self.children) - 1
121
+ return highlighted
122
+
123
+ def action_cursor_up(self):
124
+ if (grid_size := self.grid_size) is None:
125
+ self.post_message(self.LeaveUp(self))
126
+ return
127
+ if self.highlighted is None:
128
+ self.highlighted = 0
129
+ else:
130
+ width, _height = grid_size
131
+ if self.highlighted >= width:
132
+ self.highlighted -= width
133
+ else:
134
+ self.post_message(self.LeaveUp(self))
135
+
136
+ def action_cursor_down(self):
137
+ if (grid_size := self.grid_size) is None:
138
+ self.post_message(self.LeaveDown(self))
139
+ return
140
+
141
+ if self.highlighted is None:
142
+ self.highlighted = 0
143
+ else:
144
+ width, height = grid_size
145
+ if self.highlighted + width < len(self.children):
146
+ self.highlighted += width
147
+ else:
148
+ self.post_message(self.LeaveDown(self))
149
+
150
+ def action_cursor_left(self):
151
+ if self.highlighted is None:
152
+ self.highlighted = 0
153
+ else:
154
+ self.highlighted -= 1
155
+
156
+ def action_cursor_right(self):
157
+ if self.highlighted is None:
158
+ self.highlighted = 0
159
+ else:
160
+ self.highlighted += 1
161
+
162
+ def on_click(self, event: events.Click) -> None:
163
+ if event.widget is None:
164
+ return
165
+
166
+ highlighted_widget: Widget | None = None
167
+ if self.highlighted is not None:
168
+ try:
169
+ highlighted_widget = self.children[self.highlighted]
170
+ except IndexError:
171
+ pass
172
+ for widget in event.widget.ancestors_with_self:
173
+ if widget in self.children:
174
+ if highlighted_widget is not None and highlighted_widget is widget:
175
+ self.action_select()
176
+ else:
177
+ self.highlighted = self.children.index(widget)
178
+ break
179
+ self.focus()
180
+
181
+ def action_select(self):
182
+ if self.highlighted is not None:
183
+ try:
184
+ highlighted_widget = self.children[self.highlighted]
185
+ except IndexError:
186
+ pass
187
+ else:
188
+ self.post_message(self.Selected(self, highlighted_widget))
189
+
190
+
191
+ if __name__ == "__main__":
192
+ from textual.app import App, ComposeResult
193
+ from textual import widgets
194
+
195
+ class GridApp(App):
196
+ CSS = """
197
+ .grid-item {
198
+ width: 1fr;
199
+ padding: 0 1;
200
+ # background: blue 20%;
201
+ border: blank;
202
+
203
+ &:hover {
204
+ background: $panel;
205
+ }
206
+
207
+ &.-highlight {
208
+ border: tall $primary;
209
+ background: $panel;
210
+ }
211
+ }
212
+ """
213
+
214
+ def compose(self) -> ComposeResult:
215
+ yield widgets.Footer()
216
+ with GridSelect():
217
+ for n in range(50):
218
+ yield widgets.Label(
219
+ f"#{n} Where there is a Will, there is a Way!",
220
+ classes="grid-item",
221
+ )
222
+
223
+ GridApp().run()
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ import re2 as re
4
+ from typing import Sequence
5
+
6
+ from rich.text import Text
7
+
8
+ from textual import on
9
+ from textual.reactive import reactive
10
+ from textual.content import Content
11
+ from textual.highlight import highlight, HighlightTheme, TokenType
12
+ from textual.message import Message
13
+ from textual.widgets import TextArea
14
+ from textual.widgets.text_area import Selection
15
+
16
+ from pygments.token import Token
17
+
18
+
19
+ RE_MATCH_FILE_PROMPT = re.compile(r"(@\S+)|@\"(.*)\"")
20
+ RE_SLASH_COMMAND = re.compile(r"(\/\S*)(\W.*)?$")
21
+
22
+
23
+ class TextualHighlightTheme(HighlightTheme):
24
+ """Contains the style definition for user with the highlight method."""
25
+
26
+ STYLES: dict[TokenType, str] = {
27
+ Token.Comment: "$text 60%",
28
+ Token.Error: "$text-error on $error-muted",
29
+ Token.Generic.Strong: "bold",
30
+ Token.Generic.Emph: "italic",
31
+ Token.Generic.Error: "$text-error on $error-muted",
32
+ Token.Generic.Heading: "$text-primary underline",
33
+ Token.Generic.Subheading: "$text-primary",
34
+ Token.Keyword: "$text-accent",
35
+ Token.Keyword.Constant: "bold $text-success 80%",
36
+ Token.Keyword.Namespace: "$text-error",
37
+ Token.Keyword.Type: "bold",
38
+ Token.Literal.Number: "$text-warning",
39
+ Token.Literal.String.Backtick: "$text 60%",
40
+ Token.Literal.String: "$text-success 90%",
41
+ Token.Literal.String.Doc: "$text-success 80% italic",
42
+ Token.Literal.String.Double: "$text-success 90%",
43
+ Token.Name: "$text-primary",
44
+ Token.Name.Attribute: "$text-warning",
45
+ Token.Name.Builtin: "$text-accent",
46
+ Token.Name.Builtin.Pseudo: "italic",
47
+ Token.Name.Class: "$text-warning bold",
48
+ Token.Name.Constant: "$text-error",
49
+ Token.Name.Decorator: "$text-primary bold",
50
+ Token.Name.Entity: "$text",
51
+ Token.Name.Function: "$text-warning underline",
52
+ Token.Name.Function.Magic: "$text-warning underline",
53
+ Token.Name.Tag: "$text-primary bold",
54
+ Token.Name.Variable: "$text-secondary",
55
+ Token.Number: "$text-warning",
56
+ Token.Operator: "bold",
57
+ Token.Operator.Word: "bold $text-error",
58
+ Token.String: "$text-success",
59
+ Token.Whitespace: "",
60
+ }
61
+
62
+
63
+ class HighlightedTextArea(TextArea):
64
+ highlight_language = reactive("markdown")
65
+
66
+ @dataclass
67
+ class CursorMove(Message):
68
+ selection: Selection
69
+
70
+ def __init__(
71
+ self,
72
+ text: str = "",
73
+ *,
74
+ name: str | None = None,
75
+ id: str | None = None,
76
+ classes: str | None = None,
77
+ disabled: bool = False,
78
+ placeholder: str | Content = "",
79
+ ):
80
+ self._text_cache: dict[int, Text] = {}
81
+ self._highlight_lines: list[Content] | None = None
82
+ super().__init__(
83
+ text,
84
+ name=name,
85
+ id=id,
86
+ classes=classes,
87
+ disabled=disabled,
88
+ highlight_cursor_line=False,
89
+ placeholder=placeholder,
90
+ )
91
+ self.compact = True
92
+
93
+ def _clear_caches(self) -> None:
94
+ self._highlight_lines = None
95
+ self._text_cache.clear()
96
+
97
+ def notify_style_update(self) -> None:
98
+ self._clear_caches()
99
+ return super().notify_style_update()
100
+
101
+ def _watch_selection(
102
+ self, previous_selection: Selection, selection: Selection
103
+ ) -> None:
104
+ self.post_message(self.CursorMove(selection))
105
+ super()._watch_selection(previous_selection, selection)
106
+
107
+ @property
108
+ def highlight_lines(self) -> Sequence[Content]:
109
+ if self._highlight_lines is None:
110
+ text = self.text
111
+ if text.startswith("/") and "\n" not in text:
112
+ content = self.highlight_slash_command(text)
113
+ self._highlight_lines = [content]
114
+ return self._highlight_lines
115
+
116
+ language = self.highlight_language
117
+ if language == "markdown":
118
+ content = self.highlight_markdown(text)
119
+ content_lines = content.split("\n", allow_blank=True)[:-1]
120
+ self._highlight_lines = content_lines
121
+ elif language == "shell":
122
+ content = self.highlight_shell(text)
123
+ content_lines = content.split("\n", allow_blank=True)
124
+ self._highlight_lines = content_lines
125
+ else:
126
+ raise ValueError("highlight_language must be `markdown` or `shell`")
127
+ return self._highlight_lines
128
+
129
+ def highlight_slash_command(self, text: str) -> Content:
130
+ return Content.styled(text, "$text-success")
131
+
132
+ def highlight_markdown(self, text: str) -> Content:
133
+ """Highlight markdown content.
134
+
135
+ Args:
136
+ text: Text containing Markdown.
137
+
138
+ Returns:
139
+ Highlighted content.
140
+ """
141
+ content = highlight(
142
+ text + "\n```",
143
+ language="markdown",
144
+ theme=TextualHighlightTheme,
145
+ )
146
+ content = content.highlight_regex(RE_MATCH_FILE_PROMPT, style="$primary")
147
+ return content
148
+
149
+ def highlight_shell(self, text: str) -> Content:
150
+ """Highlight text with a bash shell command.
151
+
152
+ Args:
153
+ text: Text containing shell command.
154
+
155
+ Returns:
156
+ Highlighted content.
157
+ """
158
+ content = highlight(text, language="sh")
159
+ return content
160
+
161
+ @on(TextArea.Changed)
162
+ def _on_changed(self) -> None:
163
+ self._highlight_lines = None
164
+ self._text_cache.clear()
165
+
166
+ def get_line(self, line_index: int) -> Text:
167
+ if (cached_line := self._text_cache.get(line_index)) is not None:
168
+ return cached_line.copy()
169
+ try:
170
+ line = self.highlight_lines[line_index]
171
+ except IndexError:
172
+ return Text("", end="", no_wrap=True)
173
+ rendered_line = list(line.render_segments(self.visual_style))
174
+ text = Text.assemble(
175
+ *[(text, style) for text, style, _ in rendered_line],
176
+ end="",
177
+ no_wrap=True,
178
+ )
179
+ self._text_cache[line_index] = text.copy()
180
+ return text