nexus-tui 0.1.13__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/style.tcss CHANGED
@@ -17,13 +17,12 @@ Screen {
17
17
  }
18
18
 
19
19
  #header-left {
20
- width: auto;
21
- content-align: left middle;
20
+ width: 30%;
21
+ content-align: center middle;
22
22
  }
23
23
 
24
24
  #tool-search {
25
- width: 60%;
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
- #main-container {
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 #footer-container {
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 #main-container { border-top: solid #c0caf5; } /* removed bottom border */
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 #footer-container {
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 #main-container { border-top: solid #c0caf5; } /* removed bottom border */
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 #footer-container {
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 #main-container { border-top: solid #343b58; } /* removed bottom border */
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
- #footer-container {
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
+ }
@@ -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
@@ -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"{hint_str}{fav_str}> {label} | [dim]{self.tool_info.description}[/]"
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, hint=hint, is_favorite=is_favorite), **kwargs
118
+ ToolItem(tool_info, is_favorite=is_favorite), **kwargs
123
119
  )
124
120
 
125
121