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.
- batrachian_toad-0.5.22.dist-info/METADATA +197 -0
- batrachian_toad-0.5.22.dist-info/RECORD +120 -0
- batrachian_toad-0.5.22.dist-info/WHEEL +4 -0
- batrachian_toad-0.5.22.dist-info/entry_points.txt +2 -0
- batrachian_toad-0.5.22.dist-info/licenses/LICENSE +661 -0
- toad/__init__.py +46 -0
- toad/__main__.py +4 -0
- toad/_loop.py +86 -0
- toad/about.py +90 -0
- toad/acp/agent.py +671 -0
- toad/acp/api.py +47 -0
- toad/acp/encode_tool_call_id.py +12 -0
- toad/acp/messages.py +138 -0
- toad/acp/prompt.py +54 -0
- toad/acp/protocol.py +426 -0
- toad/agent.py +62 -0
- toad/agent_schema.py +70 -0
- toad/agents.py +45 -0
- toad/ansi/__init__.py +1 -0
- toad/ansi/_ansi.py +1612 -0
- toad/ansi/_ansi_colors.py +264 -0
- toad/ansi/_control_codes.py +37 -0
- toad/ansi/_keys.py +251 -0
- toad/ansi/_sgr_styles.py +64 -0
- toad/ansi/_stream_parser.py +418 -0
- toad/answer.py +22 -0
- toad/app.py +557 -0
- toad/atomic.py +37 -0
- toad/cli.py +257 -0
- toad/code_analyze.py +28 -0
- toad/complete.py +34 -0
- toad/constants.py +58 -0
- toad/conversation_markdown.py +19 -0
- toad/danger.py +371 -0
- toad/data/agents/ampcode.com.toml +51 -0
- toad/data/agents/augmentcode.com.toml +40 -0
- toad/data/agents/claude.com.toml +41 -0
- toad/data/agents/docker.com.toml +59 -0
- toad/data/agents/geminicli.com.toml +28 -0
- toad/data/agents/goose.ai.toml +51 -0
- toad/data/agents/inference.huggingface.co.toml +33 -0
- toad/data/agents/kimi.com.toml +35 -0
- toad/data/agents/openai.com.toml +53 -0
- toad/data/agents/opencode.ai.toml +61 -0
- toad/data/agents/openhands.dev.toml +44 -0
- toad/data/agents/stakpak.dev.toml +61 -0
- toad/data/agents/vibe.mistral.ai.toml +27 -0
- toad/data/agents/vtcode.dev.toml +62 -0
- toad/data/images/frog.png +0 -0
- toad/data/sounds/turn-over.wav +0 -0
- toad/db.py +5 -0
- toad/dec.py +332 -0
- toad/directory.py +234 -0
- toad/directory_watcher.py +96 -0
- toad/fuzzy.py +140 -0
- toad/gist.py +2 -0
- toad/history.py +138 -0
- toad/jsonrpc.py +576 -0
- toad/menus.py +14 -0
- toad/messages.py +74 -0
- toad/option_content.py +51 -0
- toad/os.py +0 -0
- toad/path_complete.py +145 -0
- toad/path_filter.py +124 -0
- toad/paths.py +71 -0
- toad/pill.py +23 -0
- toad/prompt/extract.py +19 -0
- toad/prompt/resource.py +68 -0
- toad/protocol.py +28 -0
- toad/screens/action_modal.py +94 -0
- toad/screens/agent_modal.py +172 -0
- toad/screens/command_edit_modal.py +58 -0
- toad/screens/main.py +192 -0
- toad/screens/permissions.py +390 -0
- toad/screens/permissions.tcss +72 -0
- toad/screens/settings.py +254 -0
- toad/screens/settings.tcss +101 -0
- toad/screens/store.py +476 -0
- toad/screens/store.tcss +261 -0
- toad/settings.py +354 -0
- toad/settings_schema.py +318 -0
- toad/shell.py +263 -0
- toad/shell_read.py +42 -0
- toad/slash_command.py +34 -0
- toad/toad.tcss +752 -0
- toad/version.py +80 -0
- toad/visuals/columns.py +273 -0
- toad/widgets/agent_response.py +79 -0
- toad/widgets/agent_thought.py +41 -0
- toad/widgets/command_pane.py +224 -0
- toad/widgets/condensed_path.py +93 -0
- toad/widgets/conversation.py +1626 -0
- toad/widgets/danger_warning.py +65 -0
- toad/widgets/diff_view.py +709 -0
- toad/widgets/flash.py +81 -0
- toad/widgets/future_text.py +126 -0
- toad/widgets/grid_select.py +223 -0
- toad/widgets/highlighted_textarea.py +180 -0
- toad/widgets/mandelbrot.py +294 -0
- toad/widgets/markdown_note.py +13 -0
- toad/widgets/menu.py +147 -0
- toad/widgets/non_selectable_label.py +5 -0
- toad/widgets/note.py +18 -0
- toad/widgets/path_search.py +381 -0
- toad/widgets/plan.py +180 -0
- toad/widgets/project_directory_tree.py +74 -0
- toad/widgets/prompt.py +741 -0
- toad/widgets/question.py +337 -0
- toad/widgets/shell_result.py +35 -0
- toad/widgets/shell_terminal.py +18 -0
- toad/widgets/side_bar.py +74 -0
- toad/widgets/slash_complete.py +211 -0
- toad/widgets/strike_text.py +66 -0
- toad/widgets/terminal.py +526 -0
- toad/widgets/terminal_tool.py +338 -0
- toad/widgets/throbber.py +90 -0
- toad/widgets/tool_call.py +303 -0
- toad/widgets/user_input.py +23 -0
- toad/widgets/version.py +5 -0
- toad/widgets/welcome.py +31 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
from typing import NamedTuple
|
|
2
|
+
|
|
3
|
+
from rich.segment import Segment
|
|
4
|
+
from rich.color import Color as RichColor
|
|
5
|
+
from rich.style import Style as RichStyle
|
|
6
|
+
|
|
7
|
+
from textual import events
|
|
8
|
+
from textual.color import Color
|
|
9
|
+
from textual.content import Content
|
|
10
|
+
from textual.geometry import NULL_SIZE, Offset
|
|
11
|
+
from textual.reactive import reactive, var
|
|
12
|
+
from textual.strip import Strip
|
|
13
|
+
from textual.app import App, ComposeResult
|
|
14
|
+
from textual.widget import Widget
|
|
15
|
+
from textual.timer import Timer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
COLORS = [
|
|
19
|
+
Color.parse(color).rgb
|
|
20
|
+
for color in [
|
|
21
|
+
"#881177",
|
|
22
|
+
"#aa3355",
|
|
23
|
+
"#cc6666",
|
|
24
|
+
"#ee9944",
|
|
25
|
+
"#eedd00",
|
|
26
|
+
"#99dd55",
|
|
27
|
+
"#44dd88",
|
|
28
|
+
"#22ccbb",
|
|
29
|
+
"#00bbcc",
|
|
30
|
+
"#0099cc",
|
|
31
|
+
"#3366bb",
|
|
32
|
+
"#663399",
|
|
33
|
+
]
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MandelbrotRegion(NamedTuple):
|
|
38
|
+
"""Defines the extents of the mandelbrot set."""
|
|
39
|
+
|
|
40
|
+
x_min: float
|
|
41
|
+
x_max: float
|
|
42
|
+
y_min: float
|
|
43
|
+
y_max: float
|
|
44
|
+
|
|
45
|
+
def zoom(
|
|
46
|
+
self, focal_x: float, focal_y: float, zoom_factor: float
|
|
47
|
+
) -> "MandelbrotRegion":
|
|
48
|
+
"""
|
|
49
|
+
Return a new region zoomed in or out from a focal point.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
focal_x: X coordinate of the point to zoom around (in complex plane coordinates)
|
|
53
|
+
focal_y: Y coordinate of the point to zoom around (in complex plane coordinates)
|
|
54
|
+
zoom_factor: Zoom factor (>1 to zoom in, <1 to zoom out, =1 for no change)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A new MandelbrotRegion with the focal point at the same relative position
|
|
58
|
+
"""
|
|
59
|
+
# Calculate current dimensions
|
|
60
|
+
width = self.x_max - self.x_min
|
|
61
|
+
height = self.y_max - self.y_min
|
|
62
|
+
|
|
63
|
+
# Calculate new dimensions
|
|
64
|
+
new_width = width / zoom_factor
|
|
65
|
+
new_height = height / zoom_factor
|
|
66
|
+
|
|
67
|
+
# Calculate focal point's relative position in current region
|
|
68
|
+
fx = (focal_x - self.x_min) / width
|
|
69
|
+
fy = (focal_y - self.y_min) / height
|
|
70
|
+
|
|
71
|
+
# Calculate new bounds maintaining the focal point's relative position
|
|
72
|
+
new_x_min = focal_x - fx * new_width
|
|
73
|
+
new_x_max = focal_x + (1 - fx) * new_width
|
|
74
|
+
new_y_min = focal_y - fy * new_height
|
|
75
|
+
new_y_max = focal_y + (1 - fy) * new_height
|
|
76
|
+
|
|
77
|
+
return MandelbrotRegion(new_x_min, new_x_max, new_y_min, new_y_max)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Mandelbrot(Widget):
|
|
81
|
+
ALLOW_SELECT = False
|
|
82
|
+
DEFAULT_CSS = """
|
|
83
|
+
Mandelbrot {
|
|
84
|
+
border: block black 20%;
|
|
85
|
+
text-wrap: nowrap;
|
|
86
|
+
text-overflow: clip;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
}
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
set_region = reactive(MandelbrotRegion(-2, 1.0, -1.0, 1.0), init=False)
|
|
92
|
+
max_iterations = var(64)
|
|
93
|
+
rendered_size = var(NULL_SIZE)
|
|
94
|
+
rendered_set = var(Content(""))
|
|
95
|
+
zoom_position = var(Offset(0, 0))
|
|
96
|
+
zoom_timer: var[Timer | None] = var(None)
|
|
97
|
+
zoom_scale = var(0.99)
|
|
98
|
+
|
|
99
|
+
BRAILLE_CHARACTERS = [chr(0x2800 + i) for i in range(256)]
|
|
100
|
+
|
|
101
|
+
# (BIT, X_OFFSET, Y_OFFSET)
|
|
102
|
+
PATCH_COORDS = [
|
|
103
|
+
(1, 0, 0),
|
|
104
|
+
(2, 0, 1),
|
|
105
|
+
(4, 0, 2),
|
|
106
|
+
(8, 1, 0),
|
|
107
|
+
(16, 1, 1),
|
|
108
|
+
(32, 1, 2),
|
|
109
|
+
(64, 0, 3),
|
|
110
|
+
(128, 1, 3),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
name: str | None = None,
|
|
116
|
+
id: str | None = None,
|
|
117
|
+
classes: str | None = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
self._strip_cache: dict[int, Strip] = {}
|
|
120
|
+
super().__init__(name=name, id=id, classes=classes)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def mandelbrot(c_real: float, c_imag: float, max_iterations: int):
|
|
124
|
+
"""
|
|
125
|
+
Determine the smooth iteration count for a point in the Mandelbrot set.
|
|
126
|
+
Uses continuous (smooth) iteration counting for better detail outside the set.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
c_real: The real part of the complex number.
|
|
130
|
+
c_imag: The imaginary part of the complex number.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
A float representing the smooth iteration count, or MAX_ITER for points in the set.
|
|
134
|
+
"""
|
|
135
|
+
# Early escape: check if point is in main cardioid
|
|
136
|
+
# The main cardioid can be detected with: q(q + (x - 1/4)) < 1/4 * y^2
|
|
137
|
+
# where q = (x - 1/4)^2 + y^2
|
|
138
|
+
x_shifted = c_real - 0.25
|
|
139
|
+
q = x_shifted * x_shifted + c_imag * c_imag
|
|
140
|
+
if q * (q + x_shifted) < 0.25 * c_imag * c_imag:
|
|
141
|
+
return max_iterations
|
|
142
|
+
|
|
143
|
+
# Early escape: check if point is in period-2 bulb
|
|
144
|
+
# The period-2 bulb is the circle: (x + 1)^2 + y^2 < 1/16
|
|
145
|
+
x_plus_one = c_real + 1.0
|
|
146
|
+
if x_plus_one * x_plus_one + c_imag * c_imag < 0.0625:
|
|
147
|
+
return max_iterations
|
|
148
|
+
|
|
149
|
+
z_real = 0.0
|
|
150
|
+
z_imag = 0.0
|
|
151
|
+
for i in range(max_iterations):
|
|
152
|
+
z_real_new = z_real * z_real - z_imag * z_imag + c_real
|
|
153
|
+
z_imag_new = 2 * z_real * z_imag + c_imag
|
|
154
|
+
z_real = z_real_new
|
|
155
|
+
z_imag = z_imag_new
|
|
156
|
+
if z_real * z_real + z_imag * z_imag > 4:
|
|
157
|
+
return i
|
|
158
|
+
return max_iterations
|
|
159
|
+
|
|
160
|
+
def on_mount(self):
|
|
161
|
+
self.call_after_refresh(self.refresh)
|
|
162
|
+
|
|
163
|
+
def on_resize(self) -> None:
|
|
164
|
+
self._strip_cache.clear()
|
|
165
|
+
|
|
166
|
+
def on_mouse_down(self, event: events.Click) -> None:
|
|
167
|
+
if self.zoom_timer:
|
|
168
|
+
self.zoom_timer.stop()
|
|
169
|
+
self.zoom_position = event.offset
|
|
170
|
+
self.zoom_scale = 0.95 if event.ctrl else 1.05
|
|
171
|
+
self.zoom_timer = self.set_interval(1 / 20, self.zoom)
|
|
172
|
+
self.capture_mouse()
|
|
173
|
+
|
|
174
|
+
def on_mouse_up(self, event: events.Click) -> None:
|
|
175
|
+
self.release_mouse()
|
|
176
|
+
if self.zoom_timer:
|
|
177
|
+
self.zoom_timer.stop()
|
|
178
|
+
|
|
179
|
+
def on_mouse_move(self, event: events.MouseMove) -> None:
|
|
180
|
+
self.zoom_position = event.offset
|
|
181
|
+
|
|
182
|
+
def zoom(self) -> None:
|
|
183
|
+
zoom_x, zoom_y = self.zoom_position
|
|
184
|
+
width, height = self.content_size
|
|
185
|
+
x_min, x_max, y_min, y_max = self.set_region
|
|
186
|
+
|
|
187
|
+
set_width = x_max - x_min
|
|
188
|
+
set_height = y_max - y_min
|
|
189
|
+
|
|
190
|
+
x = x_min + (zoom_x / width) * set_width
|
|
191
|
+
y = y_min + (zoom_y / height) * set_height
|
|
192
|
+
|
|
193
|
+
self.set_region = self.set_region.zoom(x, y, self.zoom_scale)
|
|
194
|
+
|
|
195
|
+
def notify_style_update(self) -> None:
|
|
196
|
+
self._strip_cache.clear()
|
|
197
|
+
return super().notify_style_update()
|
|
198
|
+
|
|
199
|
+
def watch_set_region(self) -> None:
|
|
200
|
+
self._strip_cache.clear()
|
|
201
|
+
self.refresh()
|
|
202
|
+
|
|
203
|
+
def render_line(self, y: int) -> Strip:
|
|
204
|
+
if (cached_line := self._strip_cache.get(y)) is not None:
|
|
205
|
+
return cached_line
|
|
206
|
+
|
|
207
|
+
width, height = self.content_size
|
|
208
|
+
x_min, x_max, y_min, y_max = self.set_region
|
|
209
|
+
mandelbrot_width = x_max - x_min
|
|
210
|
+
mandelbrot_height = y_max - y_min
|
|
211
|
+
|
|
212
|
+
mandelbrot = self.mandelbrot
|
|
213
|
+
|
|
214
|
+
max_iterations = self.max_iterations
|
|
215
|
+
set_width = width * 2
|
|
216
|
+
set_height = height * 4
|
|
217
|
+
BRAILLE_MAP = self.BRAILLE_CHARACTERS
|
|
218
|
+
PATCH_COORDS = self.PATCH_COORDS
|
|
219
|
+
max_color = len(COLORS) - 1
|
|
220
|
+
|
|
221
|
+
row = y * 4
|
|
222
|
+
|
|
223
|
+
colors: list[tuple[int, int, int]] = []
|
|
224
|
+
|
|
225
|
+
segments: list[Segment] = []
|
|
226
|
+
base_style = self.rich_style
|
|
227
|
+
|
|
228
|
+
for column in range(0, width * 2, 2):
|
|
229
|
+
braille_key = 0
|
|
230
|
+
for bit, dot_x, dot_y in PATCH_COORDS:
|
|
231
|
+
patch_x: int = column + dot_x
|
|
232
|
+
patch_y: int = row + dot_y
|
|
233
|
+
c_real: float = x_min + mandelbrot_width * patch_x / set_width
|
|
234
|
+
c_imag: float = y_min + mandelbrot_height * patch_y / set_height
|
|
235
|
+
if (
|
|
236
|
+
iterations := mandelbrot(c_real, c_imag, max_iterations)
|
|
237
|
+
) < max_iterations:
|
|
238
|
+
braille_key |= bit
|
|
239
|
+
colors.append(
|
|
240
|
+
COLORS[round((iterations / max_iterations) * max_color)]
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if colors:
|
|
244
|
+
patch_red = 0
|
|
245
|
+
patch_green = 0
|
|
246
|
+
patch_blue = 0
|
|
247
|
+
for red, green, blue in colors:
|
|
248
|
+
patch_red += red
|
|
249
|
+
patch_green += green
|
|
250
|
+
patch_blue += blue
|
|
251
|
+
|
|
252
|
+
color_count = len(colors)
|
|
253
|
+
patch_color = RichColor.from_rgb(
|
|
254
|
+
patch_red // color_count,
|
|
255
|
+
patch_green // color_count,
|
|
256
|
+
patch_blue // color_count,
|
|
257
|
+
)
|
|
258
|
+
segments.append(
|
|
259
|
+
Segment(
|
|
260
|
+
BRAILLE_MAP[braille_key],
|
|
261
|
+
base_style + RichStyle.from_color(patch_color),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
colors.clear()
|
|
265
|
+
else:
|
|
266
|
+
segments.append(Segment(" ", base_style))
|
|
267
|
+
|
|
268
|
+
strip = Strip(segments, cell_length=width)
|
|
269
|
+
strip.simplify()
|
|
270
|
+
self._strip_cache[y] = strip
|
|
271
|
+
return strip
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == "__main__":
|
|
275
|
+
|
|
276
|
+
class MApp(App):
|
|
277
|
+
CSS = """
|
|
278
|
+
Screen {
|
|
279
|
+
align: center middle;
|
|
280
|
+
background: $panel;
|
|
281
|
+
Mandelbrot {
|
|
282
|
+
background: black 20%;
|
|
283
|
+
width: 40;
|
|
284
|
+
height: 16;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
def compose(self) -> ComposeResult:
|
|
291
|
+
yield Mandelbrot()
|
|
292
|
+
|
|
293
|
+
app = MApp()
|
|
294
|
+
app.run()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
from textual.widgets import Markdown
|
|
3
|
+
|
|
4
|
+
from toad.menus import MenuItem
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MarkdownNote(Markdown):
|
|
8
|
+
def get_block_menu(self) -> Iterable[MenuItem]:
|
|
9
|
+
return
|
|
10
|
+
yield
|
|
11
|
+
|
|
12
|
+
def get_block_content(self, destination: str) -> str | None:
|
|
13
|
+
return self.source
|
toad/widgets/menu.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from textual import on
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
from textual.message import Message
|
|
9
|
+
from textual.widgets import ListView, ListItem, Label
|
|
10
|
+
from textual._partition import partition
|
|
11
|
+
from textual import events
|
|
12
|
+
from textual.widget import Widget
|
|
13
|
+
|
|
14
|
+
from toad.menus import MenuItem
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NonSelectableLabel(Label):
|
|
18
|
+
ALLOW_SELECT = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MenuOption(ListItem):
|
|
22
|
+
ALLOW_SELECT = False
|
|
23
|
+
|
|
24
|
+
def __init__(self, action: str | None, description: str, key: str | None) -> None:
|
|
25
|
+
self._action = action
|
|
26
|
+
self._description = description
|
|
27
|
+
self._key = key
|
|
28
|
+
super().__init__(classes="-has-key" if key else "-no_key")
|
|
29
|
+
|
|
30
|
+
def compose(self) -> ComposeResult:
|
|
31
|
+
yield NonSelectableLabel(self._key or " ", id="key")
|
|
32
|
+
yield NonSelectableLabel(self._description, id="description")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Menu(ListView, can_focus=True):
|
|
36
|
+
BINDINGS = [Binding("escape", "dismiss", "Dismiss")]
|
|
37
|
+
|
|
38
|
+
DEFAULT_CSS = """
|
|
39
|
+
Menu {
|
|
40
|
+
margin: 1 1;
|
|
41
|
+
width: auto;
|
|
42
|
+
height: auto;
|
|
43
|
+
max-width: 100%;
|
|
44
|
+
overlay: screen;
|
|
45
|
+
position: absolute;
|
|
46
|
+
color: $foreground;
|
|
47
|
+
background: $panel;
|
|
48
|
+
border: block $panel;
|
|
49
|
+
constrain: inside inside;
|
|
50
|
+
|
|
51
|
+
& > MenuOption {
|
|
52
|
+
|
|
53
|
+
layout: horizontal;
|
|
54
|
+
width: 1fr;
|
|
55
|
+
padding: 0 1;
|
|
56
|
+
height: auto !important;
|
|
57
|
+
overflow: auto;
|
|
58
|
+
expand: optimal;
|
|
59
|
+
#description {
|
|
60
|
+
color: $text 80%;
|
|
61
|
+
width: 1fr;
|
|
62
|
+
}
|
|
63
|
+
#key {
|
|
64
|
+
padding-right: 1;
|
|
65
|
+
text-style: bold;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&:blur {
|
|
71
|
+
background-tint: transparent;
|
|
72
|
+
& > ListItem.-highlight {
|
|
73
|
+
color: $block-cursor-blurred-foreground;
|
|
74
|
+
background: $block-cursor-blurred-background 30%;
|
|
75
|
+
text-style: $block-cursor-blurred-text-style;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
&:focus {
|
|
80
|
+
background-tint: transparent;
|
|
81
|
+
& > ListItem.-highlight {
|
|
82
|
+
color: $block-cursor-blurred-foreground;
|
|
83
|
+
background: $block-cursor-blurred-background;
|
|
84
|
+
text-style: $block-cursor-blurred-text-style;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class OptionSelected(Message):
|
|
92
|
+
"""The user selected on of the options."""
|
|
93
|
+
|
|
94
|
+
menu: Menu
|
|
95
|
+
owner: Widget
|
|
96
|
+
action: str | None
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class Dismissed(Message):
|
|
100
|
+
"""Menu was dismissed."""
|
|
101
|
+
|
|
102
|
+
menu: Menu
|
|
103
|
+
|
|
104
|
+
def __init__(self, owner: Widget, options: list[MenuItem], *args, **kwargs) -> None:
|
|
105
|
+
self._owner = owner
|
|
106
|
+
self._options = options
|
|
107
|
+
super().__init__(*args, **kwargs)
|
|
108
|
+
|
|
109
|
+
def _insert_options(self) -> None:
|
|
110
|
+
with_keys, without_keys = partition(
|
|
111
|
+
lambda option: option.key is None, self._options
|
|
112
|
+
)
|
|
113
|
+
self.extend(
|
|
114
|
+
MenuOption(menu_item.action, menu_item.description, menu_item.key)
|
|
115
|
+
for menu_item in with_keys
|
|
116
|
+
)
|
|
117
|
+
self.extend(
|
|
118
|
+
MenuOption(menu_item.action, menu_item.description, menu_item.key)
|
|
119
|
+
for menu_item in without_keys
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def on_mount(self) -> None:
|
|
123
|
+
self._insert_options()
|
|
124
|
+
|
|
125
|
+
async def activate_index(self, index: int) -> None:
|
|
126
|
+
action = self._options[index].action
|
|
127
|
+
self.post_message(self.OptionSelected(self, self._owner, action))
|
|
128
|
+
|
|
129
|
+
async def action_dismiss(self) -> None:
|
|
130
|
+
self.post_message(self.Dismissed(self))
|
|
131
|
+
|
|
132
|
+
async def on_blur(self) -> None:
|
|
133
|
+
self.post_message(self.Dismissed(self))
|
|
134
|
+
|
|
135
|
+
@on(events.Key)
|
|
136
|
+
async def on_key(self, event: events.Key) -> None:
|
|
137
|
+
for index, option in enumerate(self._options):
|
|
138
|
+
if event.key == option.key:
|
|
139
|
+
self.index = index
|
|
140
|
+
event.stop()
|
|
141
|
+
await self.activate_index(index)
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
@on(ListView.Selected)
|
|
145
|
+
async def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
146
|
+
event.stop()
|
|
147
|
+
await self.activate_index(event.index)
|
toad/widgets/note.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
from textual.widgets import Static
|
|
3
|
+
|
|
4
|
+
from toad.menus import MenuItem
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Note(Static):
|
|
8
|
+
DEFAULT_CLASSES = "block"
|
|
9
|
+
|
|
10
|
+
def get_block_menu(self) -> Iterable[MenuItem]:
|
|
11
|
+
return
|
|
12
|
+
yield
|
|
13
|
+
|
|
14
|
+
def get_block_content(self, destination: str) -> str | None:
|
|
15
|
+
return str(self.render())
|
|
16
|
+
|
|
17
|
+
def action_hello(self, message: str) -> None:
|
|
18
|
+
self.notify(message, severity="warning")
|