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
toad/screens/settings.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textual import on
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
|
+
from textual import lazy
|
|
6
|
+
from textual import containers
|
|
7
|
+
from textual.content import Content
|
|
8
|
+
from textual.screen import ModalScreen, ScreenResultType
|
|
9
|
+
from textual.widgets import Input, Select, Checkbox, Footer, Static, TextArea
|
|
10
|
+
from textual.compose import compose
|
|
11
|
+
from textual.validation import Validator, Number
|
|
12
|
+
from textual import getters
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from toad.settings import Setting
|
|
16
|
+
from toad.app import ToadApp
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SettingsInput(Input):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SettingsScreen(ModalScreen):
|
|
24
|
+
BINDINGS = [
|
|
25
|
+
("escape", "dismiss", "Dismiss settings"),
|
|
26
|
+
("ctrl+s", "screen.focus('#search')", "Focus search"),
|
|
27
|
+
]
|
|
28
|
+
CSS_PATH = "settings.tcss"
|
|
29
|
+
|
|
30
|
+
app = getters.app(ToadApp)
|
|
31
|
+
|
|
32
|
+
search_input = getters.query_one("Input#search", Input)
|
|
33
|
+
|
|
34
|
+
AUTO_FOCUS = "Input#search"
|
|
35
|
+
|
|
36
|
+
def compose(self) -> ComposeResult:
|
|
37
|
+
settings = self.app.settings
|
|
38
|
+
schema = self.app.settings_schema
|
|
39
|
+
|
|
40
|
+
def schema_to_widget(
|
|
41
|
+
group_title: str, settings_map: dict[str, Setting]
|
|
42
|
+
) -> ComposeResult:
|
|
43
|
+
for _key, setting in settings_map.items():
|
|
44
|
+
if not setting.editable:
|
|
45
|
+
continue
|
|
46
|
+
if setting.type == "object":
|
|
47
|
+
if setting.children is not None:
|
|
48
|
+
with containers.VerticalGroup(classes="setting-object"):
|
|
49
|
+
with containers.VerticalGroup(classes="heading"):
|
|
50
|
+
yield Static(setting.title, classes="title")
|
|
51
|
+
yield Static(setting.help, classes="help")
|
|
52
|
+
with containers.VerticalGroup(
|
|
53
|
+
id="setting-group", classes="setting-group"
|
|
54
|
+
):
|
|
55
|
+
yield from compose(
|
|
56
|
+
self,
|
|
57
|
+
schema_to_widget(setting.title, setting.children),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
else:
|
|
61
|
+
with containers.VerticalGroup(
|
|
62
|
+
classes="setting",
|
|
63
|
+
name=f"{group_title.lower()} {setting.title.lower()}",
|
|
64
|
+
):
|
|
65
|
+
value = settings.get(setting.key, object, expand=False)
|
|
66
|
+
default = settings.schema.get_default(setting.key)
|
|
67
|
+
|
|
68
|
+
if setting.type == "text" or default is None:
|
|
69
|
+
help = Content.from_markup(setting.help)
|
|
70
|
+
else:
|
|
71
|
+
if setting.type == "choices":
|
|
72
|
+
# For choices we need to translate the default to its associated label
|
|
73
|
+
choices = setting.choices or []
|
|
74
|
+
for choice in choices:
|
|
75
|
+
if isinstance(choice, tuple):
|
|
76
|
+
title, choice_value = choice
|
|
77
|
+
else:
|
|
78
|
+
title = choice_value = choice
|
|
79
|
+
if default == choice_value:
|
|
80
|
+
default = title
|
|
81
|
+
else:
|
|
82
|
+
help = Content()
|
|
83
|
+
|
|
84
|
+
if setting.help:
|
|
85
|
+
help = Content.assemble(
|
|
86
|
+
Content.from_markup(setting.help),
|
|
87
|
+
(f"\ndefault: {default!r}", "$text-secondary"),
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
help = Content.styled(
|
|
91
|
+
f"default: {default!r}", "$text-secondary"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
yield Static(setting.title, classes="title")
|
|
95
|
+
if help:
|
|
96
|
+
yield Static(help, classes="help")
|
|
97
|
+
if setting.type == "string":
|
|
98
|
+
with self.prevent(Input.Changed):
|
|
99
|
+
yield Input(
|
|
100
|
+
str(value), classes="input", name=setting.key
|
|
101
|
+
)
|
|
102
|
+
if setting.type == "text":
|
|
103
|
+
# with self.prevent(TextArea.Changed):
|
|
104
|
+
yield TextArea(
|
|
105
|
+
str(value), classes="input", name=setting.key
|
|
106
|
+
)
|
|
107
|
+
elif setting.type == "boolean":
|
|
108
|
+
with self.prevent(Checkbox.Changed):
|
|
109
|
+
yield Checkbox(
|
|
110
|
+
value=bool(value),
|
|
111
|
+
classes="input",
|
|
112
|
+
name=setting.key,
|
|
113
|
+
)
|
|
114
|
+
elif setting.type == "integer":
|
|
115
|
+
try:
|
|
116
|
+
integer_value = int(value)
|
|
117
|
+
except (ValueError, TypeError):
|
|
118
|
+
integer_value = setting.default
|
|
119
|
+
setting_validate = setting.validate or []
|
|
120
|
+
validators: list[Validator] = []
|
|
121
|
+
for validate in setting_validate:
|
|
122
|
+
validate_type = validate["type"]
|
|
123
|
+
if validate_type == "minimum":
|
|
124
|
+
validators.append(Number(minimum=validate["value"]))
|
|
125
|
+
elif validate_type == "maximum":
|
|
126
|
+
validators.append(Number(maximum=validate["value"]))
|
|
127
|
+
with self.prevent(Input.Changed):
|
|
128
|
+
yield Input(
|
|
129
|
+
str(integer_value),
|
|
130
|
+
type="integer",
|
|
131
|
+
classes="input",
|
|
132
|
+
name=setting.key,
|
|
133
|
+
validators=validators,
|
|
134
|
+
)
|
|
135
|
+
elif setting.type == "number":
|
|
136
|
+
try:
|
|
137
|
+
integer_value = float(value)
|
|
138
|
+
except (ValueError, TypeError):
|
|
139
|
+
integer_value = setting.default
|
|
140
|
+
setting_validate = setting.validate or []
|
|
141
|
+
validators: list[Validator] = []
|
|
142
|
+
for validate in setting_validate:
|
|
143
|
+
validate_type = validate["type"]
|
|
144
|
+
if validate_type == "minimum":
|
|
145
|
+
validators.append(Number(minimum=validate["value"]))
|
|
146
|
+
elif validate_type == "maximum":
|
|
147
|
+
validators.append(Number(maximum=validate["value"]))
|
|
148
|
+
with self.prevent(Input.Changed):
|
|
149
|
+
yield Input(
|
|
150
|
+
str(integer_value),
|
|
151
|
+
type="number",
|
|
152
|
+
classes="input",
|
|
153
|
+
name=setting.key,
|
|
154
|
+
validators=validators,
|
|
155
|
+
)
|
|
156
|
+
elif setting.type == "choices":
|
|
157
|
+
select_value = str(value)
|
|
158
|
+
choices = setting.choices or []
|
|
159
|
+
with self.prevent(Select.Changed):
|
|
160
|
+
select_choices = [
|
|
161
|
+
(
|
|
162
|
+
choice
|
|
163
|
+
if isinstance(choice, tuple)
|
|
164
|
+
else (choice, choice)
|
|
165
|
+
)
|
|
166
|
+
for choice in choices
|
|
167
|
+
]
|
|
168
|
+
choices_set = {choice[1] for choice in select_choices}
|
|
169
|
+
yield Select(
|
|
170
|
+
select_choices,
|
|
171
|
+
value=(
|
|
172
|
+
select_value
|
|
173
|
+
if select_value in choices_set
|
|
174
|
+
else setting.default
|
|
175
|
+
),
|
|
176
|
+
classes="input",
|
|
177
|
+
name=setting.key,
|
|
178
|
+
allow_blank=setting.default is None,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
with containers.Vertical(id="contents"):
|
|
182
|
+
with containers.VerticalGroup(classes="search-container"):
|
|
183
|
+
yield Input(id="search", placeholder="Search settings")
|
|
184
|
+
with lazy.Reveal(
|
|
185
|
+
containers.VerticalScroll(can_focus=False, id="settings-container")
|
|
186
|
+
):
|
|
187
|
+
yield from compose(self, schema_to_widget("", schema.settings_map))
|
|
188
|
+
|
|
189
|
+
yield Footer()
|
|
190
|
+
|
|
191
|
+
@on(Input.Blurred, "Input")
|
|
192
|
+
@on(Input.Submitted, "Input")
|
|
193
|
+
def on_input_blurred(self, event: Input.Blurred) -> None:
|
|
194
|
+
if event.validation_result and not event.validation_result.is_valid:
|
|
195
|
+
self.notify(
|
|
196
|
+
event.validation_result.failures[0].description or "error",
|
|
197
|
+
title=event.input.name or "",
|
|
198
|
+
severity="error",
|
|
199
|
+
)
|
|
200
|
+
event.input.value = str(
|
|
201
|
+
self.app.settings.get(event.input.name or "", expand=False)
|
|
202
|
+
)
|
|
203
|
+
return
|
|
204
|
+
if event.input.name is not None:
|
|
205
|
+
if event.input.type == "integer":
|
|
206
|
+
self.app.settings.set(event.input.name, int(event.value or "0"))
|
|
207
|
+
elif event.input.type == "number":
|
|
208
|
+
self.app.settings.set(event.input.name, float(event.value or "0"))
|
|
209
|
+
else:
|
|
210
|
+
self.app.settings.set(event.input.name, event.value)
|
|
211
|
+
|
|
212
|
+
@on(TextArea.Changed)
|
|
213
|
+
def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
|
214
|
+
if event.text_area.name is not None:
|
|
215
|
+
self.app.settings.set(event.text_area.name, event.text_area.text)
|
|
216
|
+
|
|
217
|
+
@on(Checkbox.Changed)
|
|
218
|
+
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
|
219
|
+
if event.checkbox.name is not None:
|
|
220
|
+
self.app.settings.set(event.checkbox.name, event.checkbox.value)
|
|
221
|
+
|
|
222
|
+
@on(Select.Changed)
|
|
223
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
224
|
+
if event.select.name is not None:
|
|
225
|
+
self.app.settings.set(event.select.name, event.select.value)
|
|
226
|
+
|
|
227
|
+
def filter_settings(self, search_term: str) -> None:
|
|
228
|
+
if search_term:
|
|
229
|
+
search_term = search_term.lower()
|
|
230
|
+
for setting in self.query(".setting"):
|
|
231
|
+
if setting.name:
|
|
232
|
+
setting.display = search_term in setting.name
|
|
233
|
+
for container in reversed(self.query(".setting-object")):
|
|
234
|
+
container.display = not container.get_child_by_id(
|
|
235
|
+
"setting-group"
|
|
236
|
+
).is_empty
|
|
237
|
+
else:
|
|
238
|
+
self.query(".setting").set(display=True)
|
|
239
|
+
self.query(".setting-object").set(display=True)
|
|
240
|
+
|
|
241
|
+
@on(Input.Changed, "#search")
|
|
242
|
+
def on_search_input(self, event: Input.Changed) -> None:
|
|
243
|
+
self.filter_settings(event.value)
|
|
244
|
+
|
|
245
|
+
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
|
|
246
|
+
if action == "focus":
|
|
247
|
+
if not self.is_mounted:
|
|
248
|
+
return None
|
|
249
|
+
return None if self.search_input.has_focus else True
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
async def action_dismiss(self, result: ScreenResultType | None = None) -> None:
|
|
253
|
+
self.query("#search").focus()
|
|
254
|
+
self.call_after_refresh(self.dismiss, result)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
|
|
2
|
+
SettingsScreen {
|
|
3
|
+
overflow: hidden;
|
|
4
|
+
background: $background 60%;
|
|
5
|
+
align-horizontal: right;
|
|
6
|
+
|
|
7
|
+
&.-narrow #contents {
|
|
8
|
+
width: 1fr;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#contents {
|
|
12
|
+
width: 50%;
|
|
13
|
+
padding: 0 1;
|
|
14
|
+
background: black 10%;
|
|
15
|
+
& > VerticalScroll {
|
|
16
|
+
overflow: hidden scroll;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#settings-container {
|
|
21
|
+
layout: stream;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.hidden {
|
|
25
|
+
display: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.setting:last-child .input {
|
|
29
|
+
margin: 1 0 0 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Input,Select SelectCurrent, Checkbox,TextArea {
|
|
33
|
+
border: tall black 20%;
|
|
34
|
+
&:focus {
|
|
35
|
+
border: tall $primary;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
Select:focus > SelectCurrent {
|
|
39
|
+
border: tall $primary;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.input {
|
|
43
|
+
margin: 1 0 1 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Input.-invalid {
|
|
47
|
+
border: tall $error 60%;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
TextArea {
|
|
51
|
+
height: 10;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.search-container {
|
|
55
|
+
dock: top;
|
|
56
|
+
padding: 1 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.setting-group {
|
|
60
|
+
padding: 0;
|
|
61
|
+
& > .setting-object:last-child {
|
|
62
|
+
margin-bottom: 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.setting-object {
|
|
67
|
+
border: tall $secondary-muted;
|
|
68
|
+
&:light {
|
|
69
|
+
border: tall $foreground 20%;
|
|
70
|
+
}
|
|
71
|
+
padding: 0 1;
|
|
72
|
+
|
|
73
|
+
&:focus-within {
|
|
74
|
+
border: tall $secondary;
|
|
75
|
+
}
|
|
76
|
+
.heading {
|
|
77
|
+
margin-bottom: 1;
|
|
78
|
+
}
|
|
79
|
+
margin-bottom: 1;
|
|
80
|
+
|
|
81
|
+
&:last-child {
|
|
82
|
+
margin: 0 0 0 0;
|
|
83
|
+
}
|
|
84
|
+
.heading .title {
|
|
85
|
+
color: $primary;
|
|
86
|
+
text-style: none;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.title {
|
|
91
|
+
margin: 0;
|
|
92
|
+
padding: 0 0 0 1;
|
|
93
|
+
text-style: bold;
|
|
94
|
+
}
|
|
95
|
+
.help {
|
|
96
|
+
color: $text-muted;
|
|
97
|
+
padding: 0 0 0 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|