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/jsonrpc.py ADDED
@@ -0,0 +1,576 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ from asyncio import Future, get_running_loop
6
+ from dataclasses import dataclass
7
+ from functools import wraps
8
+ import inspect
9
+ from inspect import signature
10
+ from enum import IntEnum
11
+ import logging
12
+ from types import TracebackType
13
+ import weakref
14
+
15
+ import rich.repr
16
+ from typing import Callable, ParamSpec, TypeVar
17
+ from typeguard import check_type, CollectionCheckStrategy, TypeCheckError
18
+
19
+ import textual
20
+
21
+ type MethodType = Callable
22
+ type JSONValue = str | int | float | bool | None
23
+ type JSONType = dict[str, JSONType] | list[JSONType] | str | int | float | bool | None
24
+ type JSONObject = dict[str, JSONType]
25
+ type JSONList = list[JSONType]
26
+
27
+ log = logging.getLogger("jsonrpc")
28
+
29
+
30
+ def expose(name: str = "", prefix: str = ""):
31
+ """Expose a method."""
32
+
33
+ def expose_method[T: Callable](callable: T) -> T:
34
+ setattr(callable, "_jsonrpc_expose", f"{prefix}{name or callable.__name__}")
35
+ return callable
36
+
37
+ return expose_method
38
+
39
+
40
+ class NoDefault:
41
+ def __repr__(self) -> str:
42
+ return "NO_DEFAULT"
43
+
44
+
45
+ NO_DEFAULT = NoDefault()
46
+
47
+
48
+ class ErrorCode(IntEnum):
49
+ """JSONRPC error codes"""
50
+
51
+ # https://www.jsonrpc.org/specification
52
+ PARSE_ERROR = -32700
53
+ INVALID_REQUEST = -32600
54
+ METHOD_NOT_FOUND = -32601
55
+ INVALID_PARAMS = -32602
56
+ INTERNAL_ERROR = -32603
57
+
58
+
59
+ @dataclass
60
+ class Parameter:
61
+ type: type
62
+ default: JSONType | NoDefault
63
+
64
+
65
+ @dataclass
66
+ class Method:
67
+ name: str
68
+ callable: Callable
69
+ parameters: dict[str, Parameter]
70
+
71
+
72
+ @rich.repr.auto
73
+ class JSONRPCError(Exception):
74
+ """An error thrown by the JSONRPC system."""
75
+
76
+ CODE: ErrorCode = ErrorCode.INTERNAL_ERROR
77
+ """Default code to use (may be overridden in the constructor)."""
78
+
79
+ def __init__(
80
+ self, message: str, id: str | int | None = None, code: ErrorCode | None = None
81
+ ) -> None:
82
+ self.message = message
83
+ self.id = id
84
+ self.code = code if code is not None else self.CODE
85
+ super().__init__(message)
86
+
87
+ def __rich_repr__(self) -> rich.repr.Result:
88
+ yield self.message
89
+ yield "id", self.id
90
+ yield "code", self.code
91
+
92
+
93
+ class InvalidRequest(JSONRPCError):
94
+ CODE = ErrorCode.INVALID_REQUEST
95
+
96
+
97
+ class InvalidParams(JSONRPCError):
98
+ CODE = ErrorCode.INVALID_PARAMS
99
+
100
+
101
+ class MethodNotFound(JSONRPCError):
102
+ CODE = ErrorCode.METHOD_NOT_FOUND
103
+
104
+
105
+ class InternalError(JSONRPCError):
106
+ CODE = ErrorCode.INTERNAL_ERROR
107
+
108
+
109
+ @rich.repr.auto
110
+ class APIError(Exception):
111
+ def __init__(self, code: int, message: str, data: JSONType) -> None:
112
+ self.code = code
113
+ self.message = message
114
+ self.data = data
115
+ if data is None:
116
+ super().__init__(f"{message} ({code})")
117
+ else:
118
+ super().__init__(f"{message} ({code}); data={data!r}")
119
+
120
+
121
+ class Server:
122
+ def __init__(self) -> None:
123
+ self._methods: dict[str, Method] = {}
124
+
125
+ async def call(self, json: JSONObject | JSONList) -> JSONType:
126
+ if isinstance(json, dict):
127
+ # Single call
128
+ response = await self._dispatch_object(json)
129
+ else:
130
+ # Batch call
131
+ response = await self._dispatch_batch(json)
132
+ log.debug(f"OUT {response}")
133
+ return response
134
+
135
+ def expose_instance(self, instance: object) -> None:
136
+ """Add methods from the given instance."""
137
+ for method_name in dir(instance):
138
+ method = getattr(instance, method_name)
139
+ if (jsonrpc_expose := getattr(method, "_jsonrpc_expose", None)) is not None:
140
+ self.method(jsonrpc_expose)(method)
141
+
142
+ async def _dispatch_object(self, json: JSONObject) -> JSONType | None:
143
+ json_id = json.get("id")
144
+ if isinstance(json_id, (int, str)):
145
+ request_id = json_id
146
+ else:
147
+ request_id = None
148
+ try:
149
+ return await self._dispatch_object_call(request_id, json)
150
+ except JSONRPCError as error:
151
+ return {
152
+ "jsonrpc": "2.0",
153
+ "id": error.id,
154
+ "error": {
155
+ "code": int(error.code),
156
+ "message": error.message,
157
+ },
158
+ }
159
+ except Exception as error:
160
+ log.exception("Error dispatching JSONRPC request")
161
+ return {
162
+ "jsonrpc": "2.0",
163
+ "id": request_id,
164
+ "error": {
165
+ "code": int(ErrorCode.INTERNAL_ERROR),
166
+ "message": f"An error occurred handling your request: {error!r}",
167
+ },
168
+ }
169
+
170
+ async def _dispatch_object_call(
171
+ self, request_id: int | str | None, json: JSONObject
172
+ ) -> JSONType | None:
173
+ """Dispatch a JSONRPC call.
174
+
175
+ Args:
176
+ request_id: The request ID.
177
+ json: JSON object with the remote call information.
178
+
179
+ Returns:
180
+ Result encoding in a JSONRPC result object, or `None` if the call is a notification
181
+ and doesn't require a result.
182
+ """
183
+ if (jsonrpc := json.get("jsonrpc")) != "2.0":
184
+ raise InvalidRequest(
185
+ f"jsonrpc attribute should be '2.0'; found {jsonrpc!r}", id=request_id
186
+ )
187
+ if (method_name := json.get("method")) is None:
188
+ raise InvalidRequest(
189
+ "Invalid request; no value for 'method' given", id=request_id
190
+ )
191
+
192
+ if not isinstance(method_name, str):
193
+ raise InvalidRequest(
194
+ "Invalid request; 'method' should be a string type", id=request_id
195
+ )
196
+
197
+ if (method := self._methods.get(method_name)) is None:
198
+ raise MethodNotFound(
199
+ f"Method not found; {method_name!r} is not an exposed method",
200
+ id=request_id,
201
+ )
202
+
203
+ no_params: JSONList = []
204
+ params = json.get("params", no_params)
205
+
206
+ if not isinstance(params, (list, dict)):
207
+ raise InvalidRequest(
208
+ "Invalid request; 'params' attribute should be a list or an object"
209
+ )
210
+
211
+ arguments: dict[str, JSONType | Server | NoDefault] = {
212
+ name: parameter.default for name, parameter in method.parameters.items()
213
+ }
214
+
215
+ def validate(value: JSONType, parameter_type: type) -> None:
216
+ """Validate types."""
217
+ try:
218
+ check_type(
219
+ value,
220
+ parameter_type,
221
+ collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS,
222
+ )
223
+ except TypeCheckError as error:
224
+ raise InvalidParams(
225
+ f"Parameter is not the expected type ({parameter_type}); {error}",
226
+ id=request_id,
227
+ )
228
+
229
+ if isinstance(params, list):
230
+ parameter_items = [
231
+ (name, parameter)
232
+ for name, parameter in method.parameters.items()
233
+ if not issubclass(parameter.type, Server)
234
+ ]
235
+ for (parameter_name, parameter), value in zip(parameter_items, params):
236
+ if issubclass(parameter.type, Server):
237
+ value = self
238
+ else:
239
+ validate(value, parameter.type)
240
+ arguments[parameter_name] = value
241
+ else:
242
+ for parameter_name, value in params.items():
243
+ if parameter := method.parameters.get(parameter_name):
244
+ validate(value, parameter.type)
245
+ arguments[parameter_name] = value
246
+
247
+ for name, parameter in method.parameters.items():
248
+ if inspect.isclass(parameter.type) and issubclass(parameter.type, Server):
249
+ arguments[name] = self
250
+
251
+ try:
252
+ call_result = method.callable(**arguments)
253
+ if inspect.isawaitable(call_result):
254
+ result = await call_result
255
+ else:
256
+ result = call_result
257
+ except JSONRPCError as error:
258
+ error.id = request_id
259
+ raise error
260
+ except Exception as error:
261
+ # raise
262
+ # print("JSON error", repr(error))
263
+ # log.debug(f"Error in exposed JSONRPC method; {error}")
264
+ print("INTERNAL ERROR", error)
265
+ raise InternalError(str(error), id=request_id)
266
+
267
+ if request_id is None:
268
+ # Notification
269
+ return None
270
+
271
+ response_object = {"jsonrpc": "2.0", "result": result, "id": request_id}
272
+ return response_object
273
+
274
+ async def _dispatch_batch(self, json: JSONList) -> list[JSONType]:
275
+ batch_results: list[JSONType] = []
276
+ for request in json:
277
+ if not isinstance(request, dict):
278
+ continue
279
+ result = await self._dispatch_object(request)
280
+ if result is not None:
281
+ batch_results.append(result)
282
+ return batch_results
283
+
284
+ def process_callable(
285
+ self, callable: Callable[[MethodType], MethodType]
286
+ ) -> Callable[[MethodType], MethodType]:
287
+ return callable
288
+
289
+ def method[MethodT: Callable](
290
+ self,
291
+ name: str = "",
292
+ *,
293
+ prefix: str = "",
294
+ ) -> Callable[[MethodT], MethodT]:
295
+ """Decorator to expose a method via JSONRPC.
296
+
297
+ Args:
298
+ name: The name of the exposed method. Leave blank to auto-detect.
299
+ prefix: A prefix to be applied to the name.
300
+
301
+ Returns:
302
+ Decorator.
303
+ """
304
+
305
+ def expose_method[T: Callable](callable: T) -> T:
306
+ nonlocal name
307
+ if not name:
308
+ name = callable.__name__
309
+ name = f"{prefix}{name}"
310
+
311
+ parameters = {
312
+ name: Parameter(
313
+ (
314
+ eval(parameter.annotation)
315
+ if isinstance(parameter.annotation, str)
316
+ else parameter.annotation
317
+ ),
318
+ (
319
+ NO_DEFAULT
320
+ if parameter.default is inspect._empty
321
+ else parameter.default
322
+ ),
323
+ )
324
+ for name, parameter in signature(callable).parameters.items()
325
+ }
326
+ self._methods[name] = Method(name, callable, parameters)
327
+ return callable
328
+
329
+ return expose_method
330
+
331
+
332
+ @rich.repr.auto
333
+ class MethodCall[ReturnType]:
334
+ def __init__(
335
+ self, method: str, id: int | None, parameters: dict[str, JSONType]
336
+ ) -> None:
337
+ self.method = method
338
+ self.id = id
339
+ self.parameters = parameters
340
+ self.notification = False
341
+ self.future: Future[ReturnType] = get_running_loop().create_future()
342
+
343
+ def __rich_repr__(self) -> rich.repr.Result:
344
+ yield "method", self.method
345
+ yield "id", self.id, None
346
+ yield "parameters", self.parameters
347
+ yield "notification", self.notification, False
348
+
349
+ @property
350
+ def as_json_object(self) -> JSONType:
351
+ json: JSONType
352
+ if self.id is None:
353
+ json = {
354
+ "jsonrpc": "2.0",
355
+ "method": self.method,
356
+ "params": self.parameters,
357
+ }
358
+ else:
359
+ json = {
360
+ "jsonrpc": "2.0",
361
+ "method": self.method,
362
+ "params": self.parameters,
363
+ "id": self.id,
364
+ }
365
+ return json
366
+
367
+ async def wait(self, timeout: float | None = None) -> ReturnType | None:
368
+ if self.id is None:
369
+ return None
370
+ async with asyncio.timeout(timeout):
371
+ return await self.future
372
+
373
+
374
+ P = ParamSpec("P") # Captures parameter types
375
+ T = TypeVar("T") # Original return type
376
+
377
+
378
+ class Request:
379
+ def __init__(self, api: API, callback: Callable[[Request], None] | None) -> None:
380
+ self.api = api
381
+ self._calls: list[MethodCall] = []
382
+ self._callback = callback
383
+
384
+ def add_call(self, call: MethodCall) -> None:
385
+ self._calls.append(call)
386
+
387
+ def __enter__(self) -> Request:
388
+ self.api._requests.append(self)
389
+ return self
390
+
391
+ def __exit__(
392
+ self,
393
+ exc_type: type[BaseException] | None,
394
+ exc_val: type[BaseException],
395
+ exc_tb: TracebackType,
396
+ ) -> None:
397
+ self.api._requests.pop()
398
+ if self._callback is not None:
399
+ self._callback(self)
400
+
401
+ @property
402
+ def body(self) -> JSONType | None:
403
+ """The un-encoded JSON.
404
+
405
+ Returns:
406
+ The body, or `None` if no calls present on the request.
407
+ """
408
+ if not self._calls:
409
+ return None
410
+ calls = self._calls
411
+ if len(calls) == 1:
412
+ method_call = calls[0]
413
+ return method_call.as_json_object
414
+ else:
415
+ return [method_call.as_json_object for method_call in calls]
416
+
417
+ @property
418
+ def body_json(self) -> bytes:
419
+ """Dump the body as encoded json."""
420
+ body_json = json.dumps(self.body).encode("utf-8")
421
+ return body_json
422
+
423
+
424
+ class API:
425
+ def __init__(self) -> None:
426
+ self._request_id = 0
427
+ self._requests: list[Request] = []
428
+ self._calls: weakref.WeakValueDictionary[int, MethodCall] = (
429
+ weakref.WeakValueDictionary()
430
+ )
431
+
432
+ def request(self, callback: Callable[[Request], None] | None = None) -> Request:
433
+ """Create a Request context manager."""
434
+ request = Request(self, callback)
435
+ return request
436
+
437
+ def _process_method_response(self, response: JSONObject) -> None:
438
+ if (id := response.get("id")) is not None and isinstance(id, int):
439
+ if (method_call := self._calls.get(id)) is not None:
440
+ try:
441
+ result = response["result"]
442
+ except KeyError:
443
+ if (error := response.get("error")) is not None:
444
+ if isinstance(error, dict):
445
+ code = error.get("error", -1)
446
+ if not isinstance(code, int):
447
+ code = -1
448
+ message = str(error.get("message", "unknown error"))
449
+ data = error.get("data", None)
450
+ method_call.future.set_exception(
451
+ APIError(code, message, data)
452
+ )
453
+ else:
454
+ method_call.future.set_result(result)
455
+
456
+ def process_response(self, response: JSONType) -> None:
457
+ if isinstance(response, list):
458
+ for response_object in response:
459
+ if isinstance(response_object, dict):
460
+ self._process_method_response(response_object)
461
+ elif isinstance(response, dict):
462
+ self._process_method_response(response)
463
+
464
+ def method(
465
+ self, name: str = "", *, prefix: str = "", notification: bool = False
466
+ ) -> Callable[[Callable[P, T]], Callable[P, MethodCall[T]]]:
467
+ """Decorator to define a method.
468
+
469
+ Args:
470
+ name: Name of the method, or "" to auto-detect.
471
+ prefix: String to prefix the name.
472
+
473
+ Returns:
474
+ Decorator.
475
+ """
476
+
477
+ def decorator(func: Callable[P, T]) -> Callable[P, MethodCall[T]]:
478
+ nonlocal name
479
+ if not name:
480
+ name = func.__name__
481
+ name = f"{prefix}{name}"
482
+
483
+ @wraps(func)
484
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> MethodCall[T]:
485
+ parameters = signature(func).parameters
486
+ call_parameters = {}
487
+ for arg, parameter_name in zip(args, parameters):
488
+ call_parameters[parameter_name] = arg
489
+ for parameter_name, arg in kwargs:
490
+ call_parameters[parameter_name] = arg
491
+ if notification:
492
+ method_call = MethodCall(name, None, call_parameters)
493
+ else:
494
+ self._request_id += 1
495
+ method_call = MethodCall(name, self._request_id, call_parameters)
496
+ self._requests[-1].add_call(method_call)
497
+ if method_call.id is not None:
498
+ self._calls[method_call.id] = method_call
499
+ return method_call
500
+
501
+ return wrapper
502
+
503
+ return decorator
504
+
505
+ def notification(
506
+ self,
507
+ name: str = "",
508
+ *,
509
+ prefix: str = "",
510
+ ) -> Callable[[Callable[P, T]], Callable[P, MethodCall[T]]]:
511
+ return self.method(name, prefix=prefix, notification=True)
512
+
513
+
514
+ if __name__ == "__main__":
515
+ from rich import print
516
+
517
+ server = Server()
518
+
519
+ @server.method()
520
+ def hello(name: str) -> str:
521
+ return f"hello {name}"
522
+
523
+ @server.method()
524
+ def add(server: Server, a: int, b: int) -> int:
525
+ print("SERVER", server)
526
+ return a + b
527
+
528
+ # print("!", add(1, 2))
529
+ print(server._methods)
530
+
531
+ # print(
532
+ # server.call(
533
+ # [
534
+ # {
535
+ # "jsonrpc": "2.0",
536
+ # "method": "hello",
537
+ # "params": {"name": "Will"},
538
+ # "id": "1",
539
+ # },
540
+ # {
541
+ # "jsonrpc": "2.0",
542
+ # "method": "add",
543
+ # "params": {"a": 10, "b": 20},
544
+ # "id": "2",
545
+ # },
546
+ # {"jsonrpc": "2.0", "method": "alert", "params": {"message": "Alert!"}},
547
+ # ]
548
+ # )
549
+ # )
550
+
551
+ async def test_proxy():
552
+ api = API()
553
+
554
+ @api.method()
555
+ def add(a: int, b: int) -> int: ...
556
+
557
+ @api.method()
558
+ def greet(name: str) -> str: ...
559
+
560
+ @api.notification()
561
+ def alert(text: str) -> None: ...
562
+
563
+ with api.request() as request:
564
+ add(2, 4)
565
+ greeting = greet("Will")
566
+ alert("test")
567
+ # add(1, "not a number")
568
+
569
+ # greeting = await greeting.wait()
570
+ # print(greeting)
571
+
572
+ print(greeting)
573
+
574
+ print(request.body)
575
+
576
+ asyncio.run(test_proxy())
toad/menus.py ADDED
@@ -0,0 +1,14 @@
1
+ from typing import NamedTuple
2
+
3
+
4
+ class MenuItem(NamedTuple):
5
+ """An entry in a Menu."""
6
+
7
+ description: str
8
+ action: str | None
9
+ key: str | None = None
10
+
11
+
12
+ CONVERSATION_MENUS: dict[str, list[MenuItem]] = {
13
+ "fence": [MenuItem("Run this code", "run", "r")]
14
+ }
toad/messages.py ADDED
@@ -0,0 +1,74 @@
1
+ from dataclasses import dataclass
2
+
3
+ from typing import Literal
4
+
5
+ from textual.content import Content
6
+ from textual.widget import Widget
7
+ from textual.message import Message
8
+
9
+
10
+ class WorkStarted(Message):
11
+ """Work has started."""
12
+
13
+
14
+ class WorkFinished(Message):
15
+ """Work has finished."""
16
+
17
+
18
+ @dataclass
19
+ class HistoryMove(Message):
20
+ """Getting a new item form history."""
21
+
22
+ direction: Literal[-1, +1]
23
+ shell: bool
24
+ body: str
25
+
26
+
27
+ @dataclass
28
+ class UserInputSubmitted(Message):
29
+ body: str
30
+ shell: bool = False
31
+ auto_complete: bool = False
32
+
33
+
34
+ @dataclass
35
+ class PromptSuggestion(Message):
36
+ suggestion: str
37
+
38
+
39
+ @dataclass
40
+ class Dismiss(Message):
41
+ widget: Widget
42
+
43
+ @property
44
+ def control(self) -> Widget:
45
+ return self.widget
46
+
47
+
48
+ @dataclass
49
+ class InsertPath(Message):
50
+ path: str
51
+
52
+
53
+ @dataclass
54
+ class ChangeMode(Message):
55
+ mode_id: str | None
56
+
57
+
58
+ @dataclass
59
+ class Flash(Message):
60
+ """Request a message flash.
61
+
62
+ Args:
63
+ Message: Content of flash.
64
+ style: Semantic style.
65
+ duration: Duration in seconds or `None` for default.
66
+ """
67
+
68
+ content: str | Content
69
+ style: Literal["default", "warning", "success", "error"]
70
+ duration: float | None = None
71
+
72
+
73
+ class ProjectDirectoryUpdated(Message):
74
+ """The project directory may may changed."""
toad/option_content.py ADDED
@@ -0,0 +1,51 @@
1
+ from textual.content import Content
2
+ from textual.css.styles import RulesMap
3
+ from textual.style import Style
4
+ from textual.visual import Visual, RenderOptions
5
+ from textual.strip import Strip
6
+
7
+ from itertools import zip_longest
8
+
9
+
10
+ class OptionContent(Visual):
11
+ def __init__(self, option: str | Content, help: str | Content) -> None:
12
+ self.option = Content(option) if isinstance(option, str) else option
13
+ self.help = Content(help) if isinstance(help, str) else help
14
+ self._label = Content(f"{option} {help}")
15
+
16
+ def __str__(self) -> str:
17
+ return str(self.option)
18
+
19
+ def render_strips(
20
+ self, width: int, height: int | None, style: Style, options: RenderOptions
21
+ ) -> list[Strip]:
22
+ option_strips = [
23
+ Strip(
24
+ self.option.render_segments(style), cell_length=self.option.cell_length
25
+ )
26
+ ]
27
+
28
+ option_width = self.option.cell_length
29
+ remaining_width = width - self.option.cell_length
30
+
31
+ help_strips = self.help.render_strips(
32
+ remaining_width, None, style, options=options
33
+ )
34
+ help_width = max(strip.cell_length for strip in help_strips)
35
+ help_width = [strip.extend_cell_length(help_width) for strip in help_strips]
36
+
37
+ strips: list[Strip] = []
38
+ for option_strip, help_strip in zip_longest(option_strips, help_strips):
39
+ if option_strip is None:
40
+ option_strip = Strip.blank(option_width)
41
+ assert isinstance(help_strip, Strip)
42
+ strips.append(Strip.join([option_strip, help_strip]))
43
+ return strips
44
+
45
+ def get_optimal_width(self, rules: RulesMap, container_width: int) -> int:
46
+ return self._label.get_optimal_width(rules, container_width)
47
+
48
+ def get_height(self, rules: RulesMap, width: int) -> int:
49
+ label_width = self.option.cell_length + 1
50
+ height = self.help.get_height(rules, width - label_width)
51
+ return height