nexus-tui 0.1.14__py3-none-any.whl → 0.1.15__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.
- nexus/config.py +9 -1
- nexus/screens/create_project.py +0 -4
- nexus/screens/help.py +5 -4
- nexus/screens/project_picker.py +24 -3
- nexus/screens/tool_selector.py +113 -371
- nexus/state.py +6 -2
- nexus/style.tcss +69 -20
- nexus/widgets/footer.py +108 -0
- nexus/widgets/tool_browser.py +253 -0
- nexus/widgets/tool_list_item.py +4 -8
- nexus_tui-0.1.15.dist-info/METADATA +200 -0
- nexus_tui-0.1.15.dist-info/RECORD +28 -0
- nexus_tui-0.1.14.dist-info/METADATA +0 -150
- nexus_tui-0.1.14.dist-info/RECORD +0 -26
- {nexus_tui-0.1.14.dist-info → nexus_tui-0.1.15.dist-info}/WHEEL +0 -0
- {nexus_tui-0.1.14.dist-info → nexus_tui-0.1.15.dist-info}/entry_points.txt +0 -0
- {nexus_tui-0.1.14.dist-info → nexus_tui-0.1.15.dist-info}/licenses/LICENSE +0 -0
nexus/style.tcss
CHANGED
|
@@ -17,13 +17,12 @@ Screen {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
#header-left {
|
|
20
|
-
width:
|
|
21
|
-
content-align:
|
|
20
|
+
width: 30%;
|
|
21
|
+
content-align: center middle;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
#tool-search {
|
|
25
|
-
width:
|
|
26
|
-
margin-left: 2;
|
|
25
|
+
width: 70%;
|
|
27
26
|
padding: 0 1;
|
|
28
27
|
height: 3;
|
|
29
28
|
/* Colors/Borders moved to theme sections */
|
|
@@ -35,8 +34,8 @@ Screen {
|
|
|
35
34
|
text-style: bold;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
/* Main Container */
|
|
39
|
-
|
|
37
|
+
/* Main Container (implicit in ToolBrowser now) */
|
|
38
|
+
ToolBrowser {
|
|
40
39
|
height: 1fr;
|
|
41
40
|
/* Borders defined in theme */
|
|
42
41
|
}
|
|
@@ -53,6 +52,15 @@ Screen {
|
|
|
53
52
|
height: 100%;
|
|
54
53
|
}
|
|
55
54
|
|
|
55
|
+
/* --- RESPONSIVE / COMPACT MODE --- */
|
|
56
|
+
ToolBrowser.compact #left-pane {
|
|
57
|
+
display: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ToolBrowser.compact #right-pane {
|
|
61
|
+
width: 100%;
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
.pane-header {
|
|
57
65
|
height: 1;
|
|
58
66
|
text-style: underline;
|
|
@@ -97,13 +105,6 @@ ListItem {
|
|
|
97
105
|
margin-right: 1;
|
|
98
106
|
}
|
|
99
107
|
|
|
100
|
-
#custom-footer {
|
|
101
|
-
height: 4;
|
|
102
|
-
dock: bottom;
|
|
103
|
-
padding: 0 1;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
108
|
.hidden {
|
|
108
109
|
display: none;
|
|
109
110
|
}
|
|
@@ -134,7 +135,7 @@ ListItem {
|
|
|
134
135
|
|
|
135
136
|
.theme-dark #header,
|
|
136
137
|
.theme-dark .pane-header,
|
|
137
|
-
.theme-dark
|
|
138
|
+
.theme-dark NexusFooter {
|
|
138
139
|
background: #101010;
|
|
139
140
|
color: #c0caf5;
|
|
140
141
|
}
|
|
@@ -145,7 +146,7 @@ ListItem {
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
.theme-dark #header-right { border: solid #c0caf5; }
|
|
148
|
-
.theme-dark
|
|
149
|
+
.theme-dark ToolBrowser { border-top: solid #c0caf5; } /* removed bottom border */
|
|
149
150
|
.theme-dark #left-pane { border-right: solid #c0caf5; }
|
|
150
151
|
.theme-dark .pane-header-container { border-bottom: solid #c0caf5; }
|
|
151
152
|
.theme-dark #tool-description { color: #bb9af7; border-top: solid #c0caf5; }
|
|
@@ -189,13 +190,13 @@ ListItem {
|
|
|
189
190
|
|
|
190
191
|
.theme-storm #header,
|
|
191
192
|
.theme-storm .pane-header,
|
|
192
|
-
.theme-storm
|
|
193
|
+
.theme-storm NexusFooter {
|
|
193
194
|
background: #24283b;
|
|
194
195
|
color: #c0caf5;
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
.theme-storm #header-right { border: solid #c0caf5; }
|
|
198
|
-
.theme-storm
|
|
199
|
+
.theme-storm ToolBrowser { border-top: solid #c0caf5; } /* removed bottom border */
|
|
199
200
|
.theme-storm #left-pane { border-right: solid #c0caf5; }
|
|
200
201
|
.theme-storm .pane-header-container { border-bottom: solid #c0caf5; }
|
|
201
202
|
.theme-storm #tool-description { color: #bb9af7; border-top: solid #c0caf5; }
|
|
@@ -240,13 +241,13 @@ ListItem {
|
|
|
240
241
|
|
|
241
242
|
.theme-light #header,
|
|
242
243
|
.theme-light .pane-header,
|
|
243
|
-
.theme-light
|
|
244
|
+
.theme-light NexusFooter {
|
|
244
245
|
background: #d5d6db;
|
|
245
246
|
color: #343b58;
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
.theme-light #header-right { border: solid #343b58; }
|
|
249
|
-
.theme-light
|
|
250
|
+
.theme-light ToolBrowser { border-top: solid #343b58; } /* removed bottom border */
|
|
250
251
|
.theme-light #left-pane { border-right: solid #343b58; }
|
|
251
252
|
.theme-light .pane-header-container { border-bottom: solid #343b58; }
|
|
252
253
|
.theme-light #tool-description { color: #8c4351; border-top: solid #343b58; }
|
|
@@ -342,7 +343,7 @@ ThemePicker {
|
|
|
342
343
|
|
|
343
344
|
|
|
344
345
|
/* --- NEW FOOTER --- */
|
|
345
|
-
|
|
346
|
+
NexusFooter {
|
|
346
347
|
height: 3;
|
|
347
348
|
dock: bottom;
|
|
348
349
|
padding: 0 1;
|
|
@@ -366,6 +367,7 @@ ThemePicker {
|
|
|
366
367
|
padding: 0 1;
|
|
367
368
|
text-style: bold;
|
|
368
369
|
margin-right: 1; /* Add space between badge and desc */
|
|
370
|
+
width: auto;
|
|
369
371
|
}
|
|
370
372
|
|
|
371
373
|
.key-desc {
|
|
@@ -498,3 +500,50 @@ ThemePicker {
|
|
|
498
500
|
margin-top: 1;
|
|
499
501
|
height: 3;
|
|
500
502
|
}
|
|
503
|
+
|
|
504
|
+
/* --- POLISH: FAVORITES & FOCUS --- */
|
|
505
|
+
|
|
506
|
+
/* Favorites Styling (Generic) */
|
|
507
|
+
.list-item.--favorite {
|
|
508
|
+
color: #ff9e64; /* Orange/Gold for favs */
|
|
509
|
+
text-style: bold;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* Pane Headers - De-emphasized */
|
|
513
|
+
.pane-header {
|
|
514
|
+
color: #565f89;
|
|
515
|
+
text-style: none; /* Remove underline */
|
|
516
|
+
}
|
|
517
|
+
.pane-header-right {
|
|
518
|
+
color: #565f89;
|
|
519
|
+
text-style: italic;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/* Focused Pane Header - Re-emphasize when active */
|
|
523
|
+
#right-pane:focus-within .pane-header,
|
|
524
|
+
#left-pane:focus-within .pane-header {
|
|
525
|
+
color: $text;
|
|
526
|
+
text-style: bold;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* Pane Focus Indication (Stronger Borders + Header Highlight) */
|
|
530
|
+
.theme-dark ToolBrowser:focus-within {
|
|
531
|
+
border-top: thick #7aa2f7;
|
|
532
|
+
}
|
|
533
|
+
.theme-dark #left-pane:focus-within {
|
|
534
|
+
border-right: thick #7aa2f7;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.theme-storm ToolBrowser:focus-within {
|
|
538
|
+
border-top: thick #bb9af7;
|
|
539
|
+
}
|
|
540
|
+
.theme-storm #left-pane:focus-within {
|
|
541
|
+
border-right: thick #bb9af7;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.theme-light ToolBrowser:focus-within {
|
|
545
|
+
border-top: thick #3d59a1;
|
|
546
|
+
}
|
|
547
|
+
.theme-light #left-pane:focus-within {
|
|
548
|
+
border-right: thick #3d59a1;
|
|
549
|
+
}
|
nexus/widgets/footer.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Footer widget for the Nexus application.
|
|
2
|
+
|
|
3
|
+
Displays keybindings and status information, adapting to available width.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Any
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.widgets import Label, Static
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
|
|
12
|
+
class KeyBadge(Static):
|
|
13
|
+
"""A badge displaying a key binding."""
|
|
14
|
+
|
|
15
|
+
DEFAULT_CSS = """
|
|
16
|
+
KeyBadge {
|
|
17
|
+
layout: horizontal;
|
|
18
|
+
width: auto;
|
|
19
|
+
height: 1;
|
|
20
|
+
margin-right: 2;
|
|
21
|
+
}
|
|
22
|
+
KeyBadge:hover {
|
|
23
|
+
background: $primary;
|
|
24
|
+
color: $text;
|
|
25
|
+
}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
class Pressed(Message):
|
|
29
|
+
"""Message sent when the badge is clicked."""
|
|
30
|
+
def __init__(self, action: str) -> None:
|
|
31
|
+
self.action = action
|
|
32
|
+
super().__init__()
|
|
33
|
+
|
|
34
|
+
def __init__(self, key: str, description: str, action: str) -> None:
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.key = key
|
|
37
|
+
self.description = description
|
|
38
|
+
self.action_name = action
|
|
39
|
+
|
|
40
|
+
def compose(self) -> ComposeResult:
|
|
41
|
+
yield Label(self.key, classes="key-badge")
|
|
42
|
+
yield Label(self.description, classes="key-desc")
|
|
43
|
+
|
|
44
|
+
def on_click(self) -> None:
|
|
45
|
+
self.post_message(self.Pressed(self.action_name))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FooterSection(Static):
|
|
49
|
+
"""A section of the footer grouping related keys."""
|
|
50
|
+
|
|
51
|
+
DEFAULT_CSS = """
|
|
52
|
+
FooterSection {
|
|
53
|
+
layout: horizontal;
|
|
54
|
+
width: auto;
|
|
55
|
+
height: 100%;
|
|
56
|
+
padding-right: 2;
|
|
57
|
+
content-align: center middle;
|
|
58
|
+
}
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, title: str) -> None:
|
|
62
|
+
super().__init__()
|
|
63
|
+
self.title_text = title
|
|
64
|
+
|
|
65
|
+
def compose(self) -> ComposeResult:
|
|
66
|
+
yield Label(self.title_text, classes="footer-label")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class NexusFooter(Static):
|
|
70
|
+
"""The main application footer.
|
|
71
|
+
|
|
72
|
+
Displays a minimal set of essential keybindings (Progressive Disclosure).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
DEFAULT_CSS = """
|
|
76
|
+
NexusFooter {
|
|
77
|
+
height: 3;
|
|
78
|
+
dock: bottom;
|
|
79
|
+
padding: 0 2;
|
|
80
|
+
layout: horizontal;
|
|
81
|
+
background: $surface;
|
|
82
|
+
content-align: center middle;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.key-separator {
|
|
86
|
+
width: 4;
|
|
87
|
+
content-align: center middle;
|
|
88
|
+
color: #565f89;
|
|
89
|
+
}
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, key_defs: list[tuple[str, str, str]] | None = None, **kwargs: Any) -> None:
|
|
93
|
+
super().__init__(**kwargs)
|
|
94
|
+
if key_defs is None:
|
|
95
|
+
self.key_defs = [
|
|
96
|
+
("Enter", "Select", "launch_current"),
|
|
97
|
+
("Ctrl+F", "Favorite", "toggle_favorite"),
|
|
98
|
+
("Ctrl+C", "Quit", "app.quit"),
|
|
99
|
+
("Ctrl+H", "Help", "show_help"),
|
|
100
|
+
]
|
|
101
|
+
else:
|
|
102
|
+
self.key_defs = key_defs
|
|
103
|
+
|
|
104
|
+
def compose(self) -> ComposeResult:
|
|
105
|
+
for i, (key, desc, action) in enumerate(self.key_defs):
|
|
106
|
+
yield KeyBadge(key, desc, action)
|
|
107
|
+
if i < len(self.key_defs) - 1:
|
|
108
|
+
yield Label(" ", classes="key-separator")
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Widget for browsing tools by category.
|
|
2
|
+
|
|
3
|
+
Encapsulates the split-pane layout with Category list and Tool list.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
from textual.reactive import reactive
|
|
12
|
+
from textual.widget import Widget
|
|
13
|
+
from textual.widgets import Label, ListView
|
|
14
|
+
|
|
15
|
+
from nexus.config import get_tools
|
|
16
|
+
from nexus.models import Tool
|
|
17
|
+
from nexus.widgets.tool_list_item import CategoryListItem, ToolListItem
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ToolBrowser(Widget):
|
|
21
|
+
"""A dual-pane browser for categories and tools."""
|
|
22
|
+
|
|
23
|
+
DEFAULT_CSS = """
|
|
24
|
+
ToolBrowser {
|
|
25
|
+
height: 1fr;
|
|
26
|
+
layout: horizontal;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Re-use existing ID-based styles from style.tcss via matching IDs or classes */
|
|
30
|
+
/* We will use matching IDs to keep style.tcss compatible without huge changes yet */
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
class ToolSelected(Message):
|
|
34
|
+
"""Message posted when a tool is selected (Enter pressed)."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, tool: Tool) -> None:
|
|
37
|
+
self.tool = tool
|
|
38
|
+
super().__init__()
|
|
39
|
+
|
|
40
|
+
class ToolHighlighted(Message):
|
|
41
|
+
"""Message posted when a tool is highlighted (for description updates)."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, tool: Tool) -> None:
|
|
44
|
+
self.tool = tool
|
|
45
|
+
super().__init__()
|
|
46
|
+
|
|
47
|
+
search_query = reactive("")
|
|
48
|
+
|
|
49
|
+
def compose(self) -> ComposeResult:
|
|
50
|
+
"""Compose the dual-pane layout."""
|
|
51
|
+
# Left Pane: Categories
|
|
52
|
+
with Vertical(id="left-pane"):
|
|
53
|
+
yield Label("Categories", classes="pane-header")
|
|
54
|
+
yield ListView(id="category-list")
|
|
55
|
+
|
|
56
|
+
# Right Pane: Tools
|
|
57
|
+
with Vertical(id="right-pane"):
|
|
58
|
+
with Horizontal(classes="pane-header-container"):
|
|
59
|
+
yield Label("Toolbox", classes="pane-header")
|
|
60
|
+
yield Label("Tip: Ctrl+H for controls", classes="pane-header-right")
|
|
61
|
+
|
|
62
|
+
yield ListView(id="tool-list")
|
|
63
|
+
yield Label(
|
|
64
|
+
"No tools found", id="tools-empty", classes="empty-state hidden"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def on_mount(self) -> None:
|
|
68
|
+
"""Initialize lists."""
|
|
69
|
+
self.populate_categories()
|
|
70
|
+
# Default focus
|
|
71
|
+
self.query_one("#category-list").focus()
|
|
72
|
+
|
|
73
|
+
def watch_search_query(self, new_value: str) -> None:
|
|
74
|
+
"""Filter tools when search query changes."""
|
|
75
|
+
if new_value:
|
|
76
|
+
# Auto-select ALL category if searching
|
|
77
|
+
self.select_all_category()
|
|
78
|
+
self.refresh_tools()
|
|
79
|
+
else:
|
|
80
|
+
self.refresh_tools()
|
|
81
|
+
|
|
82
|
+
def select_all_category(self) -> None:
|
|
83
|
+
"""Selects the 'ALL' category."""
|
|
84
|
+
category_list = self.query_one("#category-list", ListView)
|
|
85
|
+
for idx, child in enumerate(category_list.children):
|
|
86
|
+
if isinstance(child, CategoryListItem) and child.category_id == "ALL":
|
|
87
|
+
if category_list.index != idx:
|
|
88
|
+
category_list.index = cast(int | None, idx) # generic fix
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
def populate_categories(self) -> None:
|
|
92
|
+
"""Populate the category list."""
|
|
93
|
+
category_list = self.query_one("#category-list", ListView)
|
|
94
|
+
category_list.clear()
|
|
95
|
+
|
|
96
|
+
tools = get_tools()
|
|
97
|
+
categories = sorted(list(set(t.category for t in tools)))
|
|
98
|
+
|
|
99
|
+
category_list.append(CategoryListItem("FAVORITES"))
|
|
100
|
+
category_list.append(CategoryListItem("ALL"))
|
|
101
|
+
|
|
102
|
+
for category in categories:
|
|
103
|
+
category_list.append(CategoryListItem(category))
|
|
104
|
+
|
|
105
|
+
# Default to ALL (index 1)
|
|
106
|
+
category_list.index = 1
|
|
107
|
+
# Trigger initial tool population
|
|
108
|
+
self.populate_tools("ALL")
|
|
109
|
+
|
|
110
|
+
def populate_tools(self, category: str, filter_text: str = "") -> None:
|
|
111
|
+
"""Populate the tool list."""
|
|
112
|
+
tool_list = self.query_one("#tool-list", ListView)
|
|
113
|
+
tool_list.clear()
|
|
114
|
+
|
|
115
|
+
tools = get_tools()
|
|
116
|
+
|
|
117
|
+
# Filter by Category
|
|
118
|
+
if category == "ALL":
|
|
119
|
+
filtered_tools = tools
|
|
120
|
+
elif category == "FAVORITES":
|
|
121
|
+
# We need to access app state for favorites.
|
|
122
|
+
# Using a simplified approach: check if available in app
|
|
123
|
+
from nexus.app import NexusApp
|
|
124
|
+
if isinstance(self.app, NexusApp):
|
|
125
|
+
favs = self.app.container.state_manager.get_favorites()
|
|
126
|
+
filtered_tools = [t for t in tools if t.command in favs]
|
|
127
|
+
else:
|
|
128
|
+
filtered_tools = []
|
|
129
|
+
else:
|
|
130
|
+
filtered_tools = [t for t in tools if t.category == category]
|
|
131
|
+
|
|
132
|
+
# Filter by Text
|
|
133
|
+
if filter_text:
|
|
134
|
+
filtered_tools = [
|
|
135
|
+
t
|
|
136
|
+
for t in filtered_tools
|
|
137
|
+
if filter_text.lower() in t.label.lower()
|
|
138
|
+
or filter_text.lower() in t.description.lower()
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
# Update UI
|
|
142
|
+
empty_lbl = self.query_one("#tools-empty", Label)
|
|
143
|
+
|
|
144
|
+
if filtered_tools:
|
|
145
|
+
tool_list.display = True
|
|
146
|
+
empty_lbl.add_class("hidden")
|
|
147
|
+
tool_list.index = 0
|
|
148
|
+
else:
|
|
149
|
+
tool_list.display = False
|
|
150
|
+
empty_lbl.remove_class("hidden")
|
|
151
|
+
if filter_text:
|
|
152
|
+
empty_lbl.update(f"No tools matching '{filter_text}'")
|
|
153
|
+
else:
|
|
154
|
+
empty_lbl.update(f"No tools in category '{category}'")
|
|
155
|
+
|
|
156
|
+
# Render Items
|
|
157
|
+
for i, tool in enumerate(filtered_tools):
|
|
158
|
+
is_fav = False
|
|
159
|
+
from nexus.app import NexusApp
|
|
160
|
+
if isinstance(self.app, NexusApp):
|
|
161
|
+
is_fav = self.app.container.state_manager.is_favorite(tool.command)
|
|
162
|
+
|
|
163
|
+
tool_list.append(ToolListItem(tool, is_favorite=is_fav))
|
|
164
|
+
|
|
165
|
+
def refresh_tools(self) -> None:
|
|
166
|
+
"""Refresh (re-populate) the currently viewed category."""
|
|
167
|
+
category_list = self.query_one("#category-list", ListView)
|
|
168
|
+
if category_list.highlighted_child:
|
|
169
|
+
item = category_list.highlighted_child
|
|
170
|
+
if isinstance(item, CategoryListItem):
|
|
171
|
+
self.populate_tools(item.category_id, filter_text=self.search_query)
|
|
172
|
+
|
|
173
|
+
# --- Event Handlers ---
|
|
174
|
+
|
|
175
|
+
def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
|
|
176
|
+
if event.list_view.id == "category-list":
|
|
177
|
+
# Update tool list when category changes
|
|
178
|
+
if isinstance(event.item, CategoryListItem):
|
|
179
|
+
self.populate_tools(event.item.category_id, filter_text=self.search_query)
|
|
180
|
+
|
|
181
|
+
elif event.list_view.id == "tool-list":
|
|
182
|
+
# Notify parent to update description
|
|
183
|
+
if isinstance(event.item, ToolListItem):
|
|
184
|
+
self.post_message(self.ToolHighlighted(event.item.tool_info))
|
|
185
|
+
|
|
186
|
+
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
187
|
+
if event.list_view.id == "category-list":
|
|
188
|
+
# Move focus to tool list
|
|
189
|
+
self.query_one("#tool-list").focus()
|
|
190
|
+
|
|
191
|
+
# Ensure the first item is selected in tool list
|
|
192
|
+
tool_list = self.query_one("#tool-list", ListView)
|
|
193
|
+
if tool_list.children:
|
|
194
|
+
tool_list.index = 0
|
|
195
|
+
|
|
196
|
+
elif event.list_view.id == "tool-list":
|
|
197
|
+
# Launch tool
|
|
198
|
+
if isinstance(event.item, ToolListItem):
|
|
199
|
+
self.post_message(self.ToolSelected(event.item.tool_info))
|
|
200
|
+
|
|
201
|
+
# --- Public Control Methods (for parent screen bindings) ---
|
|
202
|
+
|
|
203
|
+
def focus_next(self) -> None:
|
|
204
|
+
"""Simulate down arrow."""
|
|
205
|
+
if self.query_one("#category-list").has_focus:
|
|
206
|
+
self._move_list_cursor("#category-list", 1)
|
|
207
|
+
elif self.query_one("#tool-list").has_focus:
|
|
208
|
+
self._move_list_cursor("#tool-list", 1)
|
|
209
|
+
|
|
210
|
+
def focus_prev(self) -> None:
|
|
211
|
+
"""Simulate up arrow."""
|
|
212
|
+
if self.query_one("#category-list").has_focus:
|
|
213
|
+
self._move_list_cursor("#category-list", -1)
|
|
214
|
+
elif self.query_one("#tool-list").has_focus:
|
|
215
|
+
self._move_list_cursor("#tool-list", -1)
|
|
216
|
+
|
|
217
|
+
def _move_list_cursor(self, list_id: str, delta: int) -> None:
|
|
218
|
+
lst = self.query_one(list_id, ListView)
|
|
219
|
+
if lst.index is None:
|
|
220
|
+
lst.index = 0
|
|
221
|
+
else:
|
|
222
|
+
new_index = lst.index + delta
|
|
223
|
+
# Clamp
|
|
224
|
+
new_index = max(0, min(len(lst.children) - 1, new_index))
|
|
225
|
+
lst.index = new_index
|
|
226
|
+
|
|
227
|
+
def focus_right(self) -> None:
|
|
228
|
+
"""Switch to tools."""
|
|
229
|
+
if self.query_one("#category-list").has_focus:
|
|
230
|
+
self.query_one("#tool-list").focus()
|
|
231
|
+
|
|
232
|
+
def focus_left(self) -> None:
|
|
233
|
+
"""Switch to categories."""
|
|
234
|
+
if self.query_one("#tool-list").has_focus:
|
|
235
|
+
self.query_one("#category-list").focus()
|
|
236
|
+
|
|
237
|
+
def get_tool_at_index(self, index: int) -> Tool | None:
|
|
238
|
+
"""Return tool at specific list index (for 1-9 shortcuts)."""
|
|
239
|
+
tool_list = self.query_one("#tool-list", ListView)
|
|
240
|
+
if 0 <= index < len(tool_list.children):
|
|
241
|
+
item = tool_list.children[index]
|
|
242
|
+
if isinstance(item, ToolListItem):
|
|
243
|
+
return item.tool_info
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
def get_current_selection(self) -> Tool | None:
|
|
247
|
+
"""Return currently selected tool."""
|
|
248
|
+
tool_list = self.query_one("#tool-list", ListView)
|
|
249
|
+
if tool_list.highlighted_child:
|
|
250
|
+
item = tool_list.highlighted_child
|
|
251
|
+
if isinstance(item, ToolListItem):
|
|
252
|
+
return item.tool_info
|
|
253
|
+
return None
|
nexus/widgets/tool_list_item.py
CHANGED
|
@@ -16,7 +16,6 @@ class ToolItem(Static):
|
|
|
16
16
|
def __init__(
|
|
17
17
|
self,
|
|
18
18
|
tool_info: Tool,
|
|
19
|
-
hint: str = "",
|
|
20
19
|
is_favorite: bool = False,
|
|
21
20
|
**kwargs: Any,
|
|
22
21
|
):
|
|
@@ -24,13 +23,11 @@ class ToolItem(Static):
|
|
|
24
23
|
|
|
25
24
|
Args:
|
|
26
25
|
tool_info: The Tool model containing tool details.
|
|
27
|
-
hint: Optional numeric hint (e.g. '1').
|
|
28
26
|
is_favorite: Whether the tool is marked as a favorite.
|
|
29
27
|
**kwargs: Additional arguments passed to the Static widget.
|
|
30
28
|
"""
|
|
31
29
|
super().__init__(**kwargs)
|
|
32
30
|
self.tool_info = tool_info
|
|
33
|
-
self.hint = hint
|
|
34
31
|
self.is_favorite = is_favorite
|
|
35
32
|
self.can_focus = True
|
|
36
33
|
|
|
@@ -40,10 +37,9 @@ class ToolItem(Static):
|
|
|
40
37
|
Returns:
|
|
41
38
|
A string representation of the tool label prefixed with '> '.
|
|
42
39
|
"""
|
|
43
|
-
hint_str = f"[bold magenta]{self.hint}[/] " if self.hint else ""
|
|
44
40
|
fav_str = "[bold yellow]★[/] " if self.is_favorite else ""
|
|
45
41
|
label = self.tool_info.label
|
|
46
|
-
return f"{
|
|
42
|
+
return f"{fav_str}> [bold]{label}[/] | [#565f89]{self.tool_info.description}[/]"
|
|
47
43
|
|
|
48
44
|
def on_mount(self) -> None:
|
|
49
45
|
"""Called when the widget is mounted.
|
|
@@ -51,6 +47,8 @@ class ToolItem(Static):
|
|
|
51
47
|
Adds specific CSS classes for styling.
|
|
52
48
|
"""
|
|
53
49
|
self.add_class("list-item")
|
|
50
|
+
if self.is_favorite:
|
|
51
|
+
self.add_class("--favorite")
|
|
54
52
|
self.add_class(f"category-{self.tool_info.category}")
|
|
55
53
|
|
|
56
54
|
|
|
@@ -105,7 +103,6 @@ class ToolListItem(ListItem):
|
|
|
105
103
|
def __init__(
|
|
106
104
|
self,
|
|
107
105
|
tool_info: Tool,
|
|
108
|
-
hint: str = "",
|
|
109
106
|
is_favorite: bool = False,
|
|
110
107
|
**kwargs: Any,
|
|
111
108
|
):
|
|
@@ -113,13 +110,12 @@ class ToolListItem(ListItem):
|
|
|
113
110
|
|
|
114
111
|
Args:
|
|
115
112
|
tool_info: The Tool model.
|
|
116
|
-
hint: Optional numeric hint.
|
|
117
113
|
is_favorite: Whether the tool is a favorite.
|
|
118
114
|
**kwargs: Additional arguments passed to the ListItem.
|
|
119
115
|
"""
|
|
120
116
|
self.tool_info = tool_info
|
|
121
117
|
super().__init__(
|
|
122
|
-
ToolItem(tool_info,
|
|
118
|
+
ToolItem(tool_info, is_favorite=is_favorite), **kwargs
|
|
123
119
|
)
|
|
124
120
|
|
|
125
121
|
|