scc-cli 1.4.1__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.
Potentially problematic release.
This version of scc-cli might be problematic. Click here for more details.
- scc_cli/__init__.py +15 -0
- scc_cli/audit/__init__.py +37 -0
- scc_cli/audit/parser.py +191 -0
- scc_cli/audit/reader.py +180 -0
- scc_cli/auth.py +145 -0
- scc_cli/claude_adapter.py +485 -0
- scc_cli/cli.py +259 -0
- scc_cli/cli_admin.py +706 -0
- scc_cli/cli_audit.py +245 -0
- scc_cli/cli_common.py +166 -0
- scc_cli/cli_config.py +527 -0
- scc_cli/cli_exceptions.py +705 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/cli_init.py +272 -0
- scc_cli/cli_launch.py +1454 -0
- scc_cli/cli_org.py +1428 -0
- scc_cli/cli_support.py +322 -0
- scc_cli/cli_team.py +892 -0
- scc_cli/cli_worktree.py +865 -0
- scc_cli/config.py +583 -0
- scc_cli/console.py +562 -0
- scc_cli/constants.py +79 -0
- scc_cli/contexts.py +377 -0
- scc_cli/deprecation.py +54 -0
- scc_cli/deps.py +189 -0
- scc_cli/docker/__init__.py +127 -0
- scc_cli/docker/core.py +466 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +604 -0
- scc_cli/doctor/__init__.py +99 -0
- scc_cli/doctor/checks.py +1074 -0
- scc_cli/doctor/render.py +346 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/errors.py +288 -0
- scc_cli/evaluation/__init__.py +27 -0
- scc_cli/evaluation/apply_exceptions.py +207 -0
- scc_cli/evaluation/evaluate.py +97 -0
- scc_cli/evaluation/models.py +80 -0
- scc_cli/exit_codes.py +55 -0
- scc_cli/git.py +1521 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +96 -0
- scc_cli/kinds.py +62 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/adapter.py +74 -0
- scc_cli/marketplace/compute.py +377 -0
- scc_cli/marketplace/constants.py +87 -0
- scc_cli/marketplace/managed.py +135 -0
- scc_cli/marketplace/materialize.py +723 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +257 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +260 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +688 -0
- scc_cli/marketplace/trust.py +244 -0
- scc_cli/models/__init__.py +41 -0
- scc_cli/models/exceptions.py +273 -0
- scc_cli/models/plugin_audit.py +434 -0
- scc_cli/org_templates.py +269 -0
- scc_cli/output_mode.py +167 -0
- scc_cli/panels.py +113 -0
- scc_cli/platform.py +350 -0
- scc_cli/profiles.py +960 -0
- scc_cli/remote.py +443 -0
- scc_cli/schemas/__init__.py +1 -0
- scc_cli/schemas/org-v1.schema.json +456 -0
- scc_cli/schemas/team-config.v1.schema.json +163 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +588 -0
- scc_cli/source_resolver.py +470 -0
- scc_cli/stats.py +378 -0
- scc_cli/stores/__init__.py +13 -0
- scc_cli/stores/exception_store.py +251 -0
- scc_cli/subprocess_utils.py +88 -0
- scc_cli/teams.py +382 -0
- scc_cli/templates/__init__.py +2 -0
- scc_cli/templates/org/__init__.py +0 -0
- scc_cli/templates/org/minimal.json +19 -0
- scc_cli/templates/org/reference.json +74 -0
- scc_cli/templates/org/strict.json +38 -0
- scc_cli/templates/org/teams.json +42 -0
- scc_cli/templates/statusline.sh +75 -0
- scc_cli/theme.py +348 -0
- scc_cli/ui/__init__.py +124 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +395 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +677 -0
- scc_cli/ui/dashboard/loaders.py +395 -0
- scc_cli/ui/dashboard/models.py +184 -0
- scc_cli/ui/dashboard/orchestrator.py +390 -0
- scc_cli/ui/formatters.py +443 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +538 -0
- scc_cli/ui/list_screen.py +431 -0
- scc_cli/ui/picker.py +700 -0
- scc_cli/ui/prompts.py +200 -0
- scc_cli/ui/wizard.py +675 -0
- scc_cli/update.py +680 -0
- scc_cli/utils/__init__.py +39 -0
- scc_cli/utils/fixit.py +264 -0
- scc_cli/utils/fuzzy.py +124 -0
- scc_cli/utils/locks.py +101 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.4.1.dist-info/METADATA +369 -0
- scc_cli-1.4.1.dist-info/RECORD +113 -0
- scc_cli-1.4.1.dist-info/WHEEL +4 -0
- scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
- scc_cli-1.4.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""Core navigation engine for interactive list-based UI.
|
|
2
|
+
|
|
3
|
+
This module provides the ListScreen component - the heart of the interactive
|
|
4
|
+
UI system. It handles:
|
|
5
|
+
- State management (cursor, scroll, filter, selection)
|
|
6
|
+
- Key handling (using the keys module)
|
|
7
|
+
- Rendering (using the chrome module)
|
|
8
|
+
- Event loop (blocking key reads → state updates → re-render)
|
|
9
|
+
|
|
10
|
+
The design is mode-agnostic: SINGLE_SELECT, MULTI_SELECT, and ACTIONABLE
|
|
11
|
+
modes use the same engine with different handlers for actions.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> items = [ListItem(value=t, label=t.name) for t in teams]
|
|
15
|
+
>>> screen = ListScreen(items, title="Select Team")
|
|
16
|
+
>>> selected = screen.run() # Blocking until selection or cancel
|
|
17
|
+
>>> if selected:
|
|
18
|
+
... print(f"Selected: {selected.value}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Callable, Sequence
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from enum import Enum, auto
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
27
|
+
|
|
28
|
+
from rich.console import RenderableType
|
|
29
|
+
from rich.live import Live
|
|
30
|
+
from rich.text import Text
|
|
31
|
+
|
|
32
|
+
from scc_cli.theme import Indicators
|
|
33
|
+
|
|
34
|
+
from .chrome import Chrome, ChromeConfig
|
|
35
|
+
from .keys import Action, ActionType, KeyReader, TeamSwitchRequested
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
T = TypeVar("T")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ListMode(Enum):
|
|
44
|
+
"""Operating mode for ListScreen.
|
|
45
|
+
|
|
46
|
+
Determines how the screen handles selection and confirmation.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
SINGLE_SELECT = auto() # Enter returns single item
|
|
50
|
+
MULTI_SELECT = auto() # Space toggles, Enter returns list
|
|
51
|
+
ACTIONABLE = auto() # Action keys dispatch callbacks
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ListItem(Generic[T]):
|
|
56
|
+
"""Wrapper for items in a list with display metadata.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
value: The underlying value (domain object).
|
|
60
|
+
label: Primary display text.
|
|
61
|
+
description: Secondary text (optional).
|
|
62
|
+
metadata: Additional key-value pairs for display.
|
|
63
|
+
governance_status: "blocked", "warning", or None.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
value: T
|
|
67
|
+
label: str
|
|
68
|
+
description: str = ""
|
|
69
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
70
|
+
governance_status: str | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ListState(Generic[T]):
|
|
75
|
+
"""Mutable state for ListScreen navigation and selection.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
items: All items in the list.
|
|
79
|
+
cursor: Current cursor position (in filtered items).
|
|
80
|
+
scroll_offset: Scroll position for viewport.
|
|
81
|
+
filter_query: Current type-to-filter query.
|
|
82
|
+
selected: Set of selected indices (for multi-select).
|
|
83
|
+
viewport_height: Max items visible at once.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
items: Sequence[ListItem[T]]
|
|
87
|
+
cursor: int = 0
|
|
88
|
+
scroll_offset: int = 0
|
|
89
|
+
filter_query: str = ""
|
|
90
|
+
selected: set[int] = field(default_factory=set)
|
|
91
|
+
viewport_height: int = 10
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def filtered_items(self) -> list[ListItem[T]]:
|
|
95
|
+
"""Items matching the current filter query."""
|
|
96
|
+
if not self.filter_query:
|
|
97
|
+
return list(self.items)
|
|
98
|
+
query = self.filter_query.lower()
|
|
99
|
+
return [
|
|
100
|
+
item
|
|
101
|
+
for item in self.items
|
|
102
|
+
if query in item.label.lower() or query in item.description.lower()
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def visible_items(self) -> list[ListItem[T]]:
|
|
107
|
+
"""Items visible in the current viewport."""
|
|
108
|
+
filtered = self.filtered_items
|
|
109
|
+
end = min(self.scroll_offset + self.viewport_height, len(filtered))
|
|
110
|
+
return filtered[self.scroll_offset : end]
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def current_item(self) -> ListItem[T] | None:
|
|
114
|
+
"""Item at cursor position, or None if list empty."""
|
|
115
|
+
filtered = self.filtered_items
|
|
116
|
+
if 0 <= self.cursor < len(filtered):
|
|
117
|
+
return filtered[self.cursor]
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def move_cursor(self, delta: int) -> bool:
|
|
121
|
+
"""Move cursor by delta, clamping to valid range.
|
|
122
|
+
|
|
123
|
+
Returns True if cursor position changed.
|
|
124
|
+
"""
|
|
125
|
+
filtered = self.filtered_items
|
|
126
|
+
if not filtered:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
old_cursor = self.cursor
|
|
130
|
+
self.cursor = max(0, min(len(filtered) - 1, self.cursor + delta))
|
|
131
|
+
|
|
132
|
+
# Adjust scroll to keep cursor visible
|
|
133
|
+
if self.cursor < self.scroll_offset:
|
|
134
|
+
self.scroll_offset = self.cursor
|
|
135
|
+
elif self.cursor >= self.scroll_offset + self.viewport_height:
|
|
136
|
+
self.scroll_offset = self.cursor - self.viewport_height + 1
|
|
137
|
+
|
|
138
|
+
return self.cursor != old_cursor
|
|
139
|
+
|
|
140
|
+
def toggle_selection(self) -> bool:
|
|
141
|
+
"""Toggle selection state of current item."""
|
|
142
|
+
if self.current_item is None:
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
if self.cursor in self.selected:
|
|
146
|
+
self.selected.discard(self.cursor)
|
|
147
|
+
else:
|
|
148
|
+
self.selected.add(self.cursor)
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
def toggle_all(self) -> bool:
|
|
152
|
+
"""Toggle all items selected/deselected."""
|
|
153
|
+
filtered = self.filtered_items
|
|
154
|
+
if not filtered:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
all_selected = len(self.selected) == len(filtered)
|
|
158
|
+
if all_selected:
|
|
159
|
+
self.selected.clear()
|
|
160
|
+
else:
|
|
161
|
+
self.selected = set(range(len(filtered)))
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def add_filter_char(self, char: str) -> bool:
|
|
165
|
+
"""Add character to filter query."""
|
|
166
|
+
self.filter_query += char
|
|
167
|
+
# Reset cursor and selection when filter changes
|
|
168
|
+
# Selection indices become stale when filtered list shrinks
|
|
169
|
+
self.cursor = 0
|
|
170
|
+
self.scroll_offset = 0
|
|
171
|
+
self.selected.clear()
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
def delete_filter_char(self) -> bool:
|
|
175
|
+
"""Remove last character from filter query."""
|
|
176
|
+
if not self.filter_query:
|
|
177
|
+
return False
|
|
178
|
+
self.filter_query = self.filter_query[:-1]
|
|
179
|
+
# Reset cursor and selection when filter changes
|
|
180
|
+
# Selection indices become stale when filtered list changes
|
|
181
|
+
self.cursor = 0
|
|
182
|
+
self.scroll_offset = 0
|
|
183
|
+
self.selected.clear()
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
def clear_filter(self) -> bool:
|
|
187
|
+
"""Clear the entire filter query.
|
|
188
|
+
|
|
189
|
+
Returns True if filter was cleared, False if already empty.
|
|
190
|
+
"""
|
|
191
|
+
if not self.filter_query:
|
|
192
|
+
return False
|
|
193
|
+
self.filter_query = ""
|
|
194
|
+
# Reset cursor and selection when filter changes
|
|
195
|
+
# Selection indices become stale when filtered list changes
|
|
196
|
+
self.cursor = 0
|
|
197
|
+
self.scroll_offset = 0
|
|
198
|
+
self.selected.clear()
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class ListScreen(Generic[T]):
|
|
203
|
+
"""Core navigation engine for list-based UI.
|
|
204
|
+
|
|
205
|
+
ListScreen combines state management, key handling, and rendering
|
|
206
|
+
into a cohesive event loop that blocks for user input and returns
|
|
207
|
+
the selected value(s).
|
|
208
|
+
|
|
209
|
+
Attributes:
|
|
210
|
+
state: Current list state.
|
|
211
|
+
mode: Operating mode (SINGLE_SELECT, MULTI_SELECT, ACTIONABLE).
|
|
212
|
+
title: Display title for the chrome.
|
|
213
|
+
custom_actions: Custom key handlers for ACTIONABLE mode.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def __init__(
|
|
217
|
+
self,
|
|
218
|
+
items: Sequence[ListItem[T]],
|
|
219
|
+
*,
|
|
220
|
+
title: str = "Select",
|
|
221
|
+
mode: ListMode = ListMode.SINGLE_SELECT,
|
|
222
|
+
custom_actions: dict[str, Callable[[ListItem[T]], None]] | None = None,
|
|
223
|
+
viewport_height: int = 10,
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Initialize the list screen.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
items: Items to display in the list.
|
|
229
|
+
title: Title for the chrome header.
|
|
230
|
+
mode: Operating mode for selection behavior.
|
|
231
|
+
custom_actions: Key → handler map for ACTIONABLE mode.
|
|
232
|
+
viewport_height: Max items visible at once.
|
|
233
|
+
"""
|
|
234
|
+
self.state = ListState(items=items, viewport_height=viewport_height)
|
|
235
|
+
self.mode = mode
|
|
236
|
+
self.title = title
|
|
237
|
+
self.custom_actions = custom_actions or {}
|
|
238
|
+
from ..console import get_err_console
|
|
239
|
+
|
|
240
|
+
self._console = get_err_console()
|
|
241
|
+
|
|
242
|
+
def run(self) -> T | list[T] | None:
|
|
243
|
+
"""Run the interactive list screen.
|
|
244
|
+
|
|
245
|
+
Blocks until the user makes a selection or cancels.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
- SINGLE_SELECT: The selected item's value, or None if cancelled.
|
|
249
|
+
- MULTI_SELECT: List of selected values, or None if cancelled.
|
|
250
|
+
- ACTIONABLE: None (actions handled via callbacks).
|
|
251
|
+
"""
|
|
252
|
+
# Set up key reader with custom keys if in actionable mode
|
|
253
|
+
custom_keys = {k: k for k in self.custom_actions} if self.custom_actions else None
|
|
254
|
+
reader = KeyReader(custom_keys=custom_keys, enable_filter=True)
|
|
255
|
+
|
|
256
|
+
# Use Rich Live for efficient updates
|
|
257
|
+
with Live(
|
|
258
|
+
self._render(),
|
|
259
|
+
console=self._console,
|
|
260
|
+
auto_refresh=False, # Manual refresh for instant response
|
|
261
|
+
transient=True, # Clear on exit
|
|
262
|
+
) as live:
|
|
263
|
+
while True:
|
|
264
|
+
action = reader.read(filter_active=bool(self.state.filter_query))
|
|
265
|
+
|
|
266
|
+
# Handle action based on type
|
|
267
|
+
result = self._handle_action(action)
|
|
268
|
+
|
|
269
|
+
if result is not None:
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
if action.should_exit:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
# Re-render if state changed
|
|
276
|
+
if action.state_changed:
|
|
277
|
+
live.update(self._render(), refresh=True)
|
|
278
|
+
|
|
279
|
+
def _render(self) -> RenderableType:
|
|
280
|
+
"""Render the current state to a Rich renderable."""
|
|
281
|
+
# Build the list body
|
|
282
|
+
body = self._render_list_body()
|
|
283
|
+
|
|
284
|
+
# Get appropriate chrome config
|
|
285
|
+
config = self._get_chrome_config()
|
|
286
|
+
|
|
287
|
+
# Render with chrome
|
|
288
|
+
chrome = Chrome(config)
|
|
289
|
+
return chrome.render(body, search_query=self.state.filter_query)
|
|
290
|
+
|
|
291
|
+
def _render_list_body(self) -> Text:
|
|
292
|
+
"""Render the list items."""
|
|
293
|
+
text = Text()
|
|
294
|
+
filtered = self.state.filtered_items
|
|
295
|
+
visible = self.state.visible_items
|
|
296
|
+
|
|
297
|
+
if not filtered:
|
|
298
|
+
text.append("No matches found", style="dim italic")
|
|
299
|
+
if self.state.filter_query:
|
|
300
|
+
text.append(" — ", style="dim")
|
|
301
|
+
text.append("Backspace", style="cyan")
|
|
302
|
+
text.append(" to edit filter", style="dim")
|
|
303
|
+
return text
|
|
304
|
+
|
|
305
|
+
for i, item in enumerate(visible):
|
|
306
|
+
# Calculate actual index in filtered list
|
|
307
|
+
actual_index = self.state.scroll_offset + i
|
|
308
|
+
is_cursor = actual_index == self.state.cursor
|
|
309
|
+
is_selected = actual_index in self.state.selected
|
|
310
|
+
|
|
311
|
+
# Build line with cursor indicator
|
|
312
|
+
if is_cursor:
|
|
313
|
+
text.append(f"{Indicators.get('CURSOR')} ", style="cyan bold")
|
|
314
|
+
else:
|
|
315
|
+
text.append(" ")
|
|
316
|
+
|
|
317
|
+
# Selection checkbox for multi-select
|
|
318
|
+
if self.mode == ListMode.MULTI_SELECT:
|
|
319
|
+
if is_selected:
|
|
320
|
+
text.append(f"[{Indicators.get('PASS')}] ", style="green")
|
|
321
|
+
else:
|
|
322
|
+
text.append("[ ] ", style="dim")
|
|
323
|
+
|
|
324
|
+
# Governance indicator
|
|
325
|
+
if item.governance_status == "blocked":
|
|
326
|
+
text.append("⛔ ", style="red")
|
|
327
|
+
elif item.governance_status == "warning":
|
|
328
|
+
text.append("⚠️ ", style="yellow")
|
|
329
|
+
|
|
330
|
+
# Label and description
|
|
331
|
+
label_style = "bold" if is_cursor else ""
|
|
332
|
+
text.append(item.label, style=label_style)
|
|
333
|
+
|
|
334
|
+
if item.description:
|
|
335
|
+
text.append(f" {item.description}", style="dim")
|
|
336
|
+
|
|
337
|
+
# Metadata badges
|
|
338
|
+
if item.metadata:
|
|
339
|
+
for key, value in item.metadata.items():
|
|
340
|
+
if value: # Only show non-empty values
|
|
341
|
+
text.append(f" [{value}]", style="cyan dim")
|
|
342
|
+
|
|
343
|
+
text.append("\n")
|
|
344
|
+
|
|
345
|
+
# Scroll indicators
|
|
346
|
+
if self.state.scroll_offset > 0:
|
|
347
|
+
text.append(f"{Indicators.get('SCROLL_UP')} more above\n", style="dim")
|
|
348
|
+
if self.state.scroll_offset + self.state.viewport_height < len(filtered):
|
|
349
|
+
text.append(f"{Indicators.get('SCROLL_DOWN')} more below\n", style="dim")
|
|
350
|
+
|
|
351
|
+
return text
|
|
352
|
+
|
|
353
|
+
def _get_chrome_config(self) -> ChromeConfig:
|
|
354
|
+
"""Get the appropriate chrome config for current mode."""
|
|
355
|
+
filtered_count = len(self.state.filtered_items)
|
|
356
|
+
|
|
357
|
+
if self.mode == ListMode.MULTI_SELECT:
|
|
358
|
+
return ChromeConfig.for_multi_select(
|
|
359
|
+
self.title,
|
|
360
|
+
len(self.state.selected),
|
|
361
|
+
filtered_count,
|
|
362
|
+
)
|
|
363
|
+
# SINGLE_SELECT and ACTIONABLE use picker style
|
|
364
|
+
return ChromeConfig.for_picker(self.title, item_count=filtered_count)
|
|
365
|
+
|
|
366
|
+
def _handle_action(self, action: Action[Any]) -> T | list[T] | None:
|
|
367
|
+
"""Handle an action and return result if selection complete."""
|
|
368
|
+
match action.action_type:
|
|
369
|
+
case ActionType.NAVIGATE_UP:
|
|
370
|
+
self.state.move_cursor(-1)
|
|
371
|
+
|
|
372
|
+
case ActionType.NAVIGATE_DOWN:
|
|
373
|
+
self.state.move_cursor(1)
|
|
374
|
+
|
|
375
|
+
case ActionType.SELECT:
|
|
376
|
+
if self.mode == ListMode.SINGLE_SELECT:
|
|
377
|
+
item = self.state.current_item
|
|
378
|
+
if item:
|
|
379
|
+
return item.value
|
|
380
|
+
elif self.mode == ListMode.MULTI_SELECT:
|
|
381
|
+
# In MULTI_SELECT, Enter confirms selection (same as CONFIRM)
|
|
382
|
+
# This prevents should_exit from returning None as "cancelled"
|
|
383
|
+
filtered = self.state.filtered_items
|
|
384
|
+
selected_indices = self.state.selected & set(range(len(filtered)))
|
|
385
|
+
return [filtered[i].value for i in sorted(selected_indices)]
|
|
386
|
+
|
|
387
|
+
case ActionType.TOGGLE:
|
|
388
|
+
if self.mode == ListMode.MULTI_SELECT:
|
|
389
|
+
self.state.toggle_selection()
|
|
390
|
+
|
|
391
|
+
case ActionType.TOGGLE_ALL:
|
|
392
|
+
if self.mode == ListMode.MULTI_SELECT:
|
|
393
|
+
self.state.toggle_all()
|
|
394
|
+
|
|
395
|
+
case ActionType.CONFIRM:
|
|
396
|
+
if self.mode == ListMode.MULTI_SELECT:
|
|
397
|
+
# Return all selected items
|
|
398
|
+
filtered = self.state.filtered_items
|
|
399
|
+
return [filtered[i].value for i in sorted(self.state.selected)]
|
|
400
|
+
|
|
401
|
+
case ActionType.FILTER_CHAR:
|
|
402
|
+
if action.filter_char:
|
|
403
|
+
self.state.add_filter_char(action.filter_char)
|
|
404
|
+
|
|
405
|
+
case ActionType.FILTER_DELETE:
|
|
406
|
+
self.state.delete_filter_char()
|
|
407
|
+
|
|
408
|
+
case ActionType.CUSTOM:
|
|
409
|
+
if action.custom_key and action.custom_key in self.custom_actions:
|
|
410
|
+
item = self.state.current_item
|
|
411
|
+
if item:
|
|
412
|
+
self.custom_actions[action.custom_key](item)
|
|
413
|
+
|
|
414
|
+
case ActionType.CANCEL | ActionType.QUIT:
|
|
415
|
+
# Will be handled by should_exit in caller
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
case ActionType.TEAM_SWITCH:
|
|
419
|
+
# Bubble up to orchestrator for consistent team switching
|
|
420
|
+
raise TeamSwitchRequested()
|
|
421
|
+
|
|
422
|
+
case ActionType.HELP:
|
|
423
|
+
# Show mode-aware help overlay
|
|
424
|
+
from .help import HelpMode, show_help_overlay
|
|
425
|
+
|
|
426
|
+
help_mode = (
|
|
427
|
+
HelpMode.MULTI_SELECT if self.mode == ListMode.MULTI_SELECT else HelpMode.PICKER
|
|
428
|
+
)
|
|
429
|
+
show_help_overlay(help_mode, self._console)
|
|
430
|
+
|
|
431
|
+
return None
|