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,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
+