bone-agent 1.4.0 → 2.0.0
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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -201
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -144
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -50
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/skills.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/skills.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -1085
- package/src/core/chat_manager.py +0 -1577
- package/src/core/config_manager.py +0 -260
- package/src/core/cron.py +0 -578
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/metadata.py +0 -75
- package/src/core/retry.py +0 -71
- package/src/core/skills.py +0 -463
- package/src/core/sub_agent.py +0 -376
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -789
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -176
- package/src/llm/codex_provider.py +0 -350
- package/src/llm/config.py +0 -536
- package/src/llm/prompts.py +0 -494
- package/src/llm/providers.py +0 -438
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -399
- package/src/tools/__init__.py +0 -151
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -549
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -99
- package/src/tools/helpers/base.py +0 -599
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -145
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -283
- package/src/tools/helpers/plugin_manifest.py +0 -185
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -190
- package/src/tools/rg_search.py +0 -477
- package/src/tools/search_plugins.py +0 -177
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -3131
- package/src/ui/displays.py +0 -239
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -643
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -226
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -207
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -195
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -201
- 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 & 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 & close</style>")
|
|
286
|
-
elif setting and setting.input_type == "options":
|
|
287
|
-
lines.append("<style fg='gray'>↑↓ Change option, Enter to select, Esc to save & close</style>")
|
|
288
|
-
elif setting and self._is_boolean_setting(setting):
|
|
289
|
-
lines.append("<style fg='gray'>↑↓ Navigate, Enter to toggle, Esc to save & close</style>")
|
|
290
|
-
elif setting:
|
|
291
|
-
lines.append("<style fg='gray'>↑↓ Navigate, Enter to edit, Esc to save & close</style>")
|
|
292
|
-
else:
|
|
293
|
-
lines.append("<style fg='gray'>↑↓ Navigate, Esc to save & 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
|