bone-agent 1.4.0 → 2.0.1

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 (126) hide show
  1. package/bin/bone.js +39 -0
  2. package/package.json +25 -39
  3. package/LICENSE +0 -21
  4. package/README.md +0 -201
  5. package/bin/npm-wrapper.js +0 -235
  6. package/bin/rg +0 -0
  7. package/bin/rg.exe +0 -0
  8. package/config.yaml.example +0 -144
  9. package/prompts/main/ask_questions.md +0 -31
  10. package/prompts/main/batch_independent_calls.md +0 -5
  11. package/prompts/main/casual_interactions.md +0 -11
  12. package/prompts/main/code_references.md +0 -8
  13. package/prompts/main/communication_style.md +0 -12
  14. package/prompts/main/context_reliability.md +0 -12
  15. package/prompts/main/conversational_tool_calling.md +0 -15
  16. package/prompts/main/dream.md +0 -50
  17. package/prompts/main/editing_pattern.md +0 -13
  18. package/prompts/main/error_handling.md +0 -6
  19. package/prompts/main/exploration_pattern.md +0 -21
  20. package/prompts/main/intro.md +0 -1
  21. package/prompts/main/obsidian.md +0 -16
  22. package/prompts/main/obsidian_project.md +0 -79
  23. package/prompts/main/professional_objectivity.md +0 -3
  24. package/prompts/main/skills.md +0 -3
  25. package/prompts/main/targeted_searching.md +0 -10
  26. package/prompts/main/task_lists_pattern.md +0 -8
  27. package/prompts/main/temp_folder.md +0 -9
  28. package/prompts/main/think_before_acting.md +0 -10
  29. package/prompts/main/tone_and_style.md +0 -4
  30. package/prompts/main/tool_preferences.md +0 -24
  31. package/prompts/main/trust_subagent_context.md +0 -21
  32. package/prompts/main/when_to_use_sub_agent.md +0 -7
  33. package/prompts/micro/ask_questions.md +0 -1
  34. package/prompts/micro/batch_independent_calls.md +0 -1
  35. package/prompts/micro/casual_interactions.md +0 -1
  36. package/prompts/micro/code_references.md +0 -1
  37. package/prompts/micro/communication_style.md +0 -1
  38. package/prompts/micro/context_reliability.md +0 -1
  39. package/prompts/micro/conversational_tool_calling.md +0 -1
  40. package/prompts/micro/editing_pattern.md +0 -1
  41. package/prompts/micro/error_handling.md +0 -1
  42. package/prompts/micro/exploration_pattern.md +0 -1
  43. package/prompts/micro/intro.md +0 -1
  44. package/prompts/micro/obsidian.md +0 -4
  45. package/prompts/micro/obsidian_project.md +0 -5
  46. package/prompts/micro/professional_objectivity.md +0 -1
  47. package/prompts/micro/skills.md +0 -1
  48. package/prompts/micro/targeted_searching.md +0 -1
  49. package/prompts/micro/task_lists_pattern.md +0 -1
  50. package/prompts/micro/temp_folder.md +0 -1
  51. package/prompts/micro/think_before_acting.md +0 -5
  52. package/prompts/micro/tone_and_style.md +0 -1
  53. package/prompts/micro/tool_preferences.md +0 -1
  54. package/prompts/micro/trust_subagent_context.md +0 -1
  55. package/prompts/micro/when_to_use_sub_agent.md +0 -1
  56. package/requirements.txt +0 -9
  57. package/src/__init__.py +0 -11
  58. package/src/core/__init__.py +0 -1
  59. package/src/core/agentic.py +0 -1085
  60. package/src/core/chat_manager.py +0 -1577
  61. package/src/core/config_manager.py +0 -260
  62. package/src/core/cron.py +0 -578
  63. package/src/core/cron_allowlist.py +0 -118
  64. package/src/core/memory.py +0 -145
  65. package/src/core/metadata.py +0 -75
  66. package/src/core/retry.py +0 -71
  67. package/src/core/skills.py +0 -463
  68. package/src/core/sub_agent.py +0 -376
  69. package/src/core/tool_approval.py +0 -220
  70. package/src/core/tool_feedback.py +0 -789
  71. package/src/exceptions.py +0 -79
  72. package/src/llm/__init__.py +0 -1
  73. package/src/llm/client.py +0 -176
  74. package/src/llm/codex_provider.py +0 -350
  75. package/src/llm/config.py +0 -536
  76. package/src/llm/prompts.py +0 -494
  77. package/src/llm/providers.py +0 -438
  78. package/src/llm/streaming.py +0 -163
  79. package/src/llm/token_tracker.py +0 -399
  80. package/src/tools/__init__.py +0 -151
  81. package/src/tools/constants.py +0 -59
  82. package/src/tools/create_file.py +0 -136
  83. package/src/tools/directory.py +0 -389
  84. package/src/tools/edit.py +0 -549
  85. package/src/tools/file_reader.py +0 -322
  86. package/src/tools/helpers/__init__.py +0 -99
  87. package/src/tools/helpers/base.py +0 -599
  88. package/src/tools/helpers/converters.py +0 -44
  89. package/src/tools/helpers/file_helpers.py +0 -189
  90. package/src/tools/helpers/formatters.py +0 -411
  91. package/src/tools/helpers/loader.py +0 -145
  92. package/src/tools/helpers/parallel_executor.py +0 -231
  93. package/src/tools/helpers/path_resolver.py +0 -283
  94. package/src/tools/helpers/plugin_manifest.py +0 -185
  95. package/src/tools/obsidian.py +0 -96
  96. package/src/tools/review_sub_agent.py +0 -190
  97. package/src/tools/rg_search.py +0 -477
  98. package/src/tools/search_plugins.py +0 -177
  99. package/src/tools/select_option.py +0 -600
  100. package/src/tools/shell.py +0 -302
  101. package/src/tools/sub_agent.py +0 -139
  102. package/src/tools/task_list.py +0 -269
  103. package/src/tools/web_search.py +0 -61
  104. package/src/ui/__init__.py +0 -1
  105. package/src/ui/banner.py +0 -87
  106. package/src/ui/commands.py +0 -3131
  107. package/src/ui/displays.py +0 -239
  108. package/src/ui/loader.py +0 -284
  109. package/src/ui/main.py +0 -643
  110. package/src/ui/prompt_utils.py +0 -113
  111. package/src/ui/setting_selector.py +0 -590
  112. package/src/ui/setup_wizard.py +0 -294
  113. package/src/ui/sub_agent_panel.py +0 -234
  114. package/src/ui/tool_confirmation.py +0 -226
  115. package/src/utils/__init__.py +0 -1
  116. package/src/utils/citation_parser.py +0 -199
  117. package/src/utils/editor.py +0 -207
  118. package/src/utils/gitignore_filter.py +0 -149
  119. package/src/utils/logger.py +0 -254
  120. package/src/utils/paths.py +0 -30
  121. package/src/utils/result_parsers.py +0 -108
  122. package/src/utils/safe_commands.py +0 -243
  123. package/src/utils/settings.py +0 -195
  124. package/src/utils/user_message_logger.py +0 -120
  125. package/src/utils/validation.py +0 -201
  126. package/src/utils/web_search.py +0 -173
@@ -1,590 +0,0 @@
1
- """Reusable component for interactive setting selection and editing."""
2
-
3
- import asyncio
4
- from dataclasses import dataclass, field
5
- from typing import Optional, List, Dict, Any, Callable, Union
6
-
7
- from prompt_toolkit import HTML
8
- from prompt_toolkit.application import Application
9
- from prompt_toolkit.key_binding import KeyBindings
10
- from prompt_toolkit.keys import Keys
11
- from prompt_toolkit.layout import Layout, HSplit, Window
12
- from prompt_toolkit.layout.dimension import D
13
- from prompt_toolkit.layout.controls import FormattedTextControl
14
-
15
- from ui.prompt_utils import TOOLBAR_STYLE
16
-
17
-
18
- @dataclass
19
- class SettingOption:
20
- """A single setting option with validation support."""
21
- key: str # Config key
22
- text: str # Display label
23
- value: Any # Current value
24
- options: List[Dict[str, Any]] = None # For enum-style: {"value": x, "text": y}
25
- input_type: str = "select" # "select", "text", "number", "boolean", "float", "options"
26
- description: str = ""
27
- min_val: Union[int, float] = None
28
- max_val: Union[int, float] = None
29
- step: Union[int, float] = None
30
- validate_fn: Callable[[Any], bool] = None # Custom validator
31
- on_text: str = "" # Custom label when value is truthy (e.g. "Active")
32
- off_text: str = "" # Custom label when value is falsy (e.g. "-")
33
-
34
- def __post_init__(self):
35
- if self.options is None:
36
- self.options = []
37
-
38
-
39
- @dataclass
40
- class SettingCategory:
41
- """A category containing related settings."""
42
- title: str
43
- icon: str = ""
44
- settings: List[SettingOption] = field(default_factory=list)
45
-
46
-
47
- class SettingSelector:
48
- """Interactive setting selector with live value editing.
49
-
50
- Boolean settings display one per line with green ON / red OFF.
51
- Enter toggles booleans directly. A Save option sits at the bottom.
52
- Non-boolean types still support inline editing.
53
- """
54
-
55
- _CURSOR = " "
56
- _ON_SAVE = False # Sentinel: cursor is on the Save button
57
-
58
- def __init__(
59
- self,
60
- categories: List[SettingCategory],
61
- title: str = "Settings",
62
- on_change: Callable[[str, str, Any], None] = None, # Called on value change
63
- show_save: bool = True, # Whether to show the Save button
64
- ):
65
- """Initialize the setting selector.
66
-
67
- Args:
68
- categories: List of SettingCategory objects with settings
69
- title: Panel title
70
- on_change: Callback(key, action, value) when setting changes
71
- show_save: Whether to display the Save button
72
- """
73
- self.categories = categories
74
- self.title = title
75
- self.on_change = on_change
76
- self.show_save = show_save
77
-
78
- self.current_cat_idx = 0
79
- self.current_setting_idx = 0
80
- self._on_save = self._ON_SAVE
81
- self.editing_value = False
82
- self.input_buffer = ""
83
- self._initial_values: Dict[str, Any] = {
84
- s.key: s.value
85
- for cat in categories
86
- for s in cat.settings
87
- }
88
-
89
- def _get_current_setting(self) -> Optional[SettingOption]:
90
- """Get the currently selected setting."""
91
- if 0 <= self.current_cat_idx < len(self.categories):
92
- cat = self.categories[self.current_cat_idx]
93
- if 0 <= self.current_setting_idx < len(cat.settings):
94
- return cat.settings[self.current_setting_idx]
95
- return None
96
-
97
- def _format_value(self, setting: SettingOption) -> str:
98
- """Format a setting value for display."""
99
- if setting.input_type in ("boolean", "nav"):
100
- if setting.on_text and setting.value:
101
- return setting.on_text
102
- if setting.off_text and not setting.value:
103
- return setting.off_text
104
- return "ON" if setting.value else "OFF"
105
- elif setting.input_type == "select" and setting.options:
106
- for opt in setting.options:
107
- if opt.get("value") == setting.value:
108
- return opt.get("text", str(setting.value))
109
- elif isinstance(setting.value, bool):
110
- return "Yes" if setting.value else "No"
111
- elif isinstance(setting.value, float) and setting.step and setting.step < 1:
112
- return f"{setting.value:.2f}"
113
- return str(setting.value)
114
-
115
- def _total_setting_rows(self) -> int:
116
- """Total navigable rows across all categories."""
117
- return sum(len(cat.settings) for cat in self.categories)
118
-
119
- def _is_boolean_setting(self, setting: Optional[SettingOption]) -> bool:
120
- """Check if a setting is a boolean toggle or nav item."""
121
- return setting is not None and setting.input_type in ("boolean", "nav")
122
-
123
- def _get_display_text(self) -> HTML:
124
- """Build the display HTML with one-line boolean toggles."""
125
- lines = []
126
-
127
- # Title (only if provided)
128
- if self.title:
129
- lines.append(f"<b>{self.title}</b>")
130
- lines.append("")
131
-
132
- # Show category headers only when there are multiple categories
133
- show_headers = len(self.categories) > 1
134
-
135
- for c_idx, cat in enumerate(self.categories):
136
- is_active_cat = c_idx == self.current_cat_idx
137
-
138
- # Add spacing between categories
139
- if c_idx > 0:
140
- lines.append("")
141
-
142
- if show_headers:
143
- lines.append(f"<b><style fg='#5F9EA0'>{cat.title}</style></b>")
144
-
145
- for s_idx, setting in enumerate(cat.settings):
146
- is_selected = (is_active_cat
147
- and s_idx == self.current_setting_idx
148
- and not self._on_save)
149
- is_editing = is_selected and self.editing_value
150
-
151
- if setting.input_type == "boolean":
152
- is_on = bool(setting.value)
153
- tag = "ON" if is_on else "OFF"
154
- label = setting.text
155
-
156
- if is_selected:
157
- color = "green" if is_on else "red"
158
- lines.append(
159
- f"> <style fg='{color}' bold='true'>{tag}</style>"
160
- f" <b>{label}</b>"
161
- )
162
- else:
163
- lines.append(
164
- f" <style fg='gray'>{tag}</style>"
165
- f" {label}"
166
- )
167
-
168
- elif setting.input_type == "nav":
169
- tag = self._format_value(setting)
170
- label = setting.text
171
-
172
- if is_selected:
173
- if tag and tag not in ("ON", "OFF"):
174
- lines.append(
175
- f"> <style fg='#5F9EA0' bold='true'>{tag}</style>"
176
- f" <b>{label}</b>"
177
- )
178
- else:
179
- lines.append(
180
- f"> <b>{label}</b>"
181
- )
182
- else:
183
- if tag and tag not in ("ON", "OFF"):
184
- lines.append(
185
- f" <style fg='gray'>{tag}</style>"
186
- f" {label}"
187
- )
188
- else:
189
- lines.append(
190
- f" {label}"
191
- )
192
-
193
- elif is_editing and setting.input_type in ("number", "float"):
194
- label = setting.text
195
- lines.append(
196
- f"> <b>{label}:</b>"
197
- f" <style fg='yellow'>{self.input_buffer}</style>"
198
- )
199
- elif is_editing and setting.input_type == "text":
200
- label = setting.text
201
- lines.append(
202
- f"> <b>{label}:</b>"
203
- f" <style fg='yellow'>{self.input_buffer}</style>"
204
- )
205
- elif is_editing and setting.input_type == "select" and setting.options:
206
- tag = self._format_value(setting)
207
- label = setting.text
208
- lines.append(
209
- f"> <style fg='yellow' bold='true'>{tag}</style>"
210
- f" <b>{label}</b>"
211
- )
212
- elif setting.input_type == "select" and setting.options:
213
- tag = self._format_value(setting)
214
- label = setting.text
215
- if is_selected:
216
- lines.append(
217
- f"> <style fg='#5F9EA0' bold='true'>{tag}</style>"
218
- f" <b>{label}</b>"
219
- )
220
- else:
221
- lines.append(
222
- f" <style fg='gray'>{tag}</style>"
223
- f" {label}"
224
- )
225
-
226
- elif setting.input_type == "options" and setting.options:
227
- # Show each option on its own line with radio-style selection
228
- label = setting.text
229
- lines.append(f" <b>{label}</b>")
230
- for opt_idx, opt in enumerate(setting.options):
231
- opt_text = opt.get("text", str(opt.get("value", "")))
232
- opt_value = opt.get("value")
233
- is_current = opt_value == setting.value
234
- is_opt_selected = is_selected and is_current
235
- indent = " > " if is_opt_selected else " "
236
- marker = "◉" if is_current else "○"
237
- desc = opt.get("description", "")
238
- if is_current:
239
- color = "#5F9EA0"
240
- lines.append(
241
- f'{indent}<style fg="{color}">{marker}</style> '
242
- f'<style fg="{color}" bold="true">{opt_text}</style>'
243
- + (f' <style fg="gray">{desc}</style>' if desc else '')
244
- )
245
- else:
246
- lines.append(
247
- f'{indent}<style fg="gray">{marker}</style> '
248
- f'{opt_text}'
249
- + (f' <style fg="gray">{desc}</style>' if desc else '')
250
- )
251
- else:
252
- label = setting.text
253
- val = self._format_value(setting)
254
- if is_selected:
255
- lines.append(
256
- f"> <b>{label}:</b>"
257
- f" <style fg='#5F9EA0'>{val}</style>"
258
- )
259
- else:
260
- lines.append(
261
- f" {label}: "
262
- f"<style fg='gray'>{val}</style>"
263
- )
264
-
265
- # Separator + Save button
266
- if self.show_save:
267
- lines.append("")
268
- if self._on_save:
269
- lines.append("> <b>[ Save ]</b>")
270
- else:
271
- lines.append(" [ Save ]")
272
-
273
- # Help text
274
- lines.append("")
275
- setting = self._get_current_setting()
276
- if self._on_save:
277
- lines.append("<style fg='gray'>Enter to save changes, Esc to save &amp; close</style>")
278
- elif self.editing_value:
279
- if setting and setting.input_type in ("number", "float", "text"):
280
- lines.append("<style fg='gray'>Type value, Enter to confirm, Esc to discard</style>")
281
- elif setting and setting.input_type == "select":
282
- lines.append("<style fg='gray'>↑↓ Change, Enter to confirm, Esc to discard</style>")
283
- else:
284
- if setting and setting.input_type == "nav":
285
- lines.append("<style fg='gray'>↑↓ Navigate, Enter to open, Esc to save &amp; close</style>")
286
- elif setting and setting.input_type == "options":
287
- lines.append("<style fg='gray'>↑↓ Change option, Enter to select, Esc to save &amp; close</style>")
288
- elif setting and self._is_boolean_setting(setting):
289
- lines.append("<style fg='gray'>↑↓ Navigate, Enter to toggle, Esc to save &amp; close</style>")
290
- elif setting:
291
- lines.append("<style fg='gray'>↑↓ Navigate, Enter to edit, Esc to save &amp; close</style>")
292
- else:
293
- lines.append("<style fg='gray'>↑↓ Navigate, Esc to save &amp; close</style>")
294
-
295
- return HTML("\n".join(lines))
296
-
297
- def _validate_input(self, setting: SettingOption, value: str) -> bool:
298
- """Validate user input for a setting."""
299
- if setting.validate_fn:
300
- try:
301
- if setting.input_type == "number":
302
- return setting.validate_fn(int(value))
303
- elif setting.input_type == "float":
304
- return setting.validate_fn(float(value))
305
- return setting.validate_fn(value)
306
- except (ValueError, TypeError):
307
- return False
308
-
309
- # Built-in validation
310
- if setting.input_type == "number":
311
- try:
312
- int_val = int(value)
313
- if setting.min_val is not None and int_val < setting.min_val:
314
- return False
315
- if setting.max_val is not None and int_val > setting.max_val:
316
- return False
317
- if setting.step is not None and setting.step > 0:
318
- if (int_val - setting.min_val if setting.min_val is not None else int_val) % setting.step != 0:
319
- return False
320
- return True
321
- except ValueError:
322
- return False
323
- elif setting.input_type == "float":
324
- try:
325
- float_val = float(value)
326
- if setting.min_val is not None and float_val < setting.min_val:
327
- return False
328
- if setting.max_val is not None and float_val > setting.max_val:
329
- return False
330
- if setting.step is not None and setting.step > 0:
331
- base = setting.min_val if setting.min_val is not None else 0.0
332
- remainder = abs(float_val - base) % setting.step
333
- if remainder > 1e-9 and abs(remainder - setting.step) > 1e-9:
334
- return False
335
- return True
336
- except ValueError:
337
- return False
338
-
339
- return len(value) > 0
340
-
341
- def _apply_change(self, key: str, new_value: Any) -> None:
342
- """Apply a setting change."""
343
- # Find and update the setting
344
- for cat in self.categories:
345
- for setting in cat.settings:
346
- if setting.key == key:
347
- old_value = setting.value
348
- setting.value = new_value
349
- if self.on_change and old_value != new_value:
350
- self.on_change(key, "change", new_value)
351
- return
352
-
353
- def _navigate_down(self):
354
- """Move selection down one row, wrapping into the Save button."""
355
- if self._on_save:
356
- return # Already at bottom
357
- cat = self.categories[self.current_cat_idx]
358
- if self.current_setting_idx < len(cat.settings) - 1:
359
- self.current_setting_idx += 1
360
- elif self.current_cat_idx < len(self.categories) - 1:
361
- self.current_cat_idx += 1
362
- self.current_setting_idx = 0
363
- elif self.show_save:
364
- # Past last setting -> move to Save button
365
- self._on_save = True
366
-
367
- def _navigate_up(self):
368
- """Move selection up one row, off the Save button if needed."""
369
- if self._on_save:
370
- self._on_save = False
371
- return
372
- if self.current_setting_idx > 0:
373
- self.current_setting_idx -= 1
374
- elif self.current_cat_idx > 0:
375
- self.current_cat_idx -= 1
376
- self.current_setting_idx = len(self.categories[self.current_cat_idx].settings) - 1
377
-
378
- def _save(self, event):
379
- """Exit with changes (or empty dict if nothing changed)."""
380
- changes = {}
381
- for cat in self.categories:
382
- for setting in cat.settings:
383
- if setting.value != self._initial_values.get(setting.key):
384
- changes[setting.key] = setting.value
385
- event.app.exit(result=changes if changes else {})
386
-
387
- def run(self) -> Optional[Dict[str, Any]]:
388
- """Display and run the setting selector.
389
-
390
- Returns:
391
- Dict of {key: new_value} for changed settings, or None if canceled
392
- """
393
- bindings = KeyBindings()
394
-
395
- def invalidate():
396
- if hasattr(invalidate, 'app'):
397
- invalidate.app.invalidate()
398
-
399
- @bindings.add(Keys.Up)
400
- def move_up(event):
401
- if self.editing_value:
402
- setting = self._get_current_setting()
403
- if setting and setting.input_type == "select" and setting.options:
404
- current_idx = next((i for i, o in enumerate(setting.options) if o.get("value") == setting.value), 0)
405
- new_idx = max(0, current_idx - 1)
406
- self._apply_change(setting.key, setting.options[new_idx].get("value"))
407
- invalidate()
408
- return
409
- # Options type: navigate within options
410
- setting = self._get_current_setting()
411
- if setting and setting.input_type == "options" and setting.options:
412
- current_idx = next((i for i, o in enumerate(setting.options) if o.get("value") == setting.value), 0)
413
- new_idx = max(0, current_idx - 1)
414
- self._apply_change(setting.key, setting.options[new_idx].get("value"))
415
- invalidate()
416
- return
417
- self._navigate_up()
418
- invalidate()
419
-
420
- @bindings.add(Keys.Down)
421
- def move_down(event):
422
- if self.editing_value:
423
- setting = self._get_current_setting()
424
- if setting and setting.input_type == "select" and setting.options:
425
- current_idx = next((i for i, o in enumerate(setting.options) if o.get("value") == setting.value), 0)
426
- new_idx = min(len(setting.options) - 1, current_idx + 1)
427
- self._apply_change(setting.key, setting.options[new_idx].get("value"))
428
- invalidate()
429
- return
430
- # Options type: navigate within options
431
- setting = self._get_current_setting()
432
- if setting and setting.input_type == "options" and setting.options:
433
- current_idx = next((i for i, o in enumerate(setting.options) if o.get("value") == setting.value), 0)
434
- new_idx = min(len(setting.options) - 1, current_idx + 1)
435
- self._apply_change(setting.key, setting.options[new_idx].get("value"))
436
- invalidate()
437
- return
438
- self._navigate_down()
439
- invalidate()
440
-
441
- @bindings.add(Keys.Enter)
442
- def confirm(event):
443
- # On the Save button -> commit
444
- if self._on_save:
445
- self._save(event)
446
- return
447
-
448
- setting = self._get_current_setting()
449
- if not setting:
450
- return
451
-
452
- # Nav: activate drill-down
453
- if setting.input_type == 'nav':
454
- event.app.exit(result={'_nav': setting.key})
455
- return
456
-
457
- # Boolean: toggle directly
458
- if self._is_boolean_setting(setting):
459
- self._apply_change(setting.key, not setting.value)
460
- invalidate()
461
- return
462
-
463
- # Select: cycle to next option directly
464
- if setting.input_type == "select" and setting.options:
465
- current_idx = next((i for i, o in enumerate(setting.options) if o.get("value") == setting.value), 0)
466
- new_idx = (current_idx + 1) % len(setting.options)
467
- self._apply_change(setting.key, setting.options[new_idx].get("value"))
468
- invalidate()
469
- return
470
-
471
- # Options: confirm selection (same behavior as save)
472
- if setting.input_type == "options" and setting.options:
473
- self._save(event)
474
- return
475
-
476
- if self.editing_value:
477
- if setting.input_type in ("text", "number", "float"):
478
- if self._validate_input(setting, self.input_buffer):
479
- if setting.input_type == "number":
480
- new_val = int(self.input_buffer)
481
- elif setting.input_type == "float":
482
- new_val = float(self.input_buffer)
483
- else:
484
- new_val = self.input_buffer
485
- self._apply_change(setting.key, new_val)
486
- self.editing_value = False
487
- self.input_buffer = ""
488
- invalidate()
489
- else:
490
- # Start editing for text/number/float types
491
- if setting.input_type in ("text", "number", "float"):
492
- self.editing_value = True
493
- self.input_buffer = str(setting.value)
494
- invalidate()
495
-
496
- @bindings.add(Keys.Escape)
497
- def close(event):
498
- if self.editing_value:
499
- # First Esc while editing: discard in-progress input
500
- self.editing_value = False
501
- self.input_buffer = ""
502
- invalidate()
503
- return
504
- # Esc saves (same as Save button)
505
- self._save(event)
506
-
507
- @bindings.add(Keys.Right)
508
- def enter_edit(event):
509
- setting = self._get_current_setting()
510
- if setting and not self.editing_value and setting.input_type not in ("boolean",):
511
- self.editing_value = True
512
- if setting.input_type in ("text", "number", "float"):
513
- self.input_buffer = str(setting.value)
514
- invalidate()
515
-
516
- @bindings.add(Keys.Left)
517
- def exit_edit(event):
518
- if self.editing_value:
519
- self.editing_value = False
520
- self.input_buffer = ""
521
- invalidate()
522
-
523
- # Character input for text/number editing
524
- @bindings.add(Keys.Any)
525
- def handle_char(event):
526
- if not self.editing_value:
527
- return
528
- setting = self._get_current_setting()
529
- if setting and setting.input_type in ("text", "number", "float"):
530
- data = event.data
531
- if len(data) == 1 and ord(data) >= 32:
532
- if setting.input_type == "number":
533
- if data.isdigit() or (data == '-' and not self.input_buffer):
534
- self.input_buffer += data
535
- elif setting.input_type == "float":
536
- if data.isdigit() or (data == '.' and '.' not in self.input_buffer) or (data == '-' and not self.input_buffer):
537
- self.input_buffer += data
538
- else:
539
- self.input_buffer += data
540
- invalidate()
541
-
542
- @bindings.add(Keys.Backspace)
543
- def handle_backspace(event):
544
- if self.editing_value and self.input_buffer:
545
- self.input_buffer = self.input_buffer[:-1]
546
- invalidate()
547
-
548
- @bindings.add(Keys.Delete)
549
- def handle_delete(event):
550
- if self.editing_value and self.input_buffer:
551
- self.input_buffer = self.input_buffer[:-1]
552
- invalidate()
553
-
554
- # Layout
555
- def get_content():
556
- return self._get_display_text()
557
-
558
- content = FormattedTextControl(get_content)
559
- container = HSplit([
560
- Window(content=content, height=D(min=1), width=D(min=1), wrap_lines=True),
561
- ])
562
-
563
- application = Application(
564
- layout=Layout(container),
565
- key_bindings=bindings,
566
- full_screen=False,
567
- mouse_support=False,
568
- cursor=None,
569
- style=TOOLBAR_STYLE,
570
- )
571
-
572
- invalidate.app = application
573
- # Add a blank line before the selector to separate from prior output
574
- application.output.write_raw("\n")
575
- # Save cursor position before rendering so we can erase on exit
576
- application.output.write_raw("\033[s")
577
- application.output.flush()
578
- # Use run_async with asyncio to properly await coroutines
579
- result = asyncio.run(application.run_async())
580
-
581
- # Erase rendered content: use ANSI save/restore cursor position.
582
- # We saved cursor before the app rendered, so restoring it puts
583
- # us back at the top of our content. Then erase to end of screen.
584
- output = application.output
585
- output.write_raw("\033[u") # Restore cursor to saved position
586
- output.write_raw("\033[J") # Erase from cursor to end of screen
587
- output.flush()
588
-
589
- # None = cancelled, {} = saved with no changes, {...} = saved with changes
590
- return result