Coverage for openhcs/textual_tui/windows/function_selector_window.py: 0.0%
86 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""Function selector window for selecting functions from the registry."""
3from typing import Callable, Optional, List, Tuple
4from textual.app import ComposeResult
5from textual.containers import Vertical, Horizontal
6from textual.widgets import Input, Tree, Button, Static
7from textual.widgets.tree import TreeNode
9from openhcs.textual_tui.windows.base_window import BaseOpenHCSWindow
10from openhcs.textual_tui.services.function_registry_service import FunctionRegistryService
13class FunctionSelectorWindow(BaseOpenHCSWindow):
14 """Window for selecting functions from the registry."""
16 DEFAULT_CSS = """
17 FunctionSelectorWindow {
18 width: 75; height: 25;
19 min-width: 75; min-height: 25;
20 }
21 """
23 def __init__(self, current_function: Optional[Callable] = None, on_result_callback: Optional[Callable] = None, **kwargs):
24 """Initialize function selector window.
26 Args:
27 current_function: Currently selected function (for highlighting)
28 on_result_callback: Callback function to handle the result
29 """
30 self.current_function = current_function
31 self.selected_function = None
32 self.functions_by_backend = {}
33 self.all_functions = []
34 self.on_result_callback = on_result_callback
36 # Load function data
37 self._load_function_data()
39 super().__init__(
40 window_id="function_selector",
41 title="Select Function",
42 mode="temporary",
43 **kwargs
44 )
46 def _load_function_data(self) -> None:
47 """Load function data from registry."""
48 registry_service = FunctionRegistryService()
49 self.functions_by_backend = registry_service.get_functions_by_backend()
51 # Flatten for search
52 self.all_functions = []
53 for backend, functions in self.functions_by_backend.items():
54 for func, display_name in functions:
55 self.all_functions.append((func, display_name, backend))
57 def compose(self) -> ComposeResult:
58 """Compose the function selector content."""
59 with Vertical():
60 # Search input
61 yield Input(
62 placeholder="Search functions...",
63 id="search_input"
64 )
66 # Function tree
67 yield self._build_function_tree()
69 # Buttons - use unified dialog-buttons class for centered alignment
70 with Horizontal(classes="dialog-buttons"):
71 yield Button("Select", id="select_btn", variant="primary", compact=True, disabled=True)
72 yield Button("Cancel", id="cancel_btn", compact=True)
74 def _build_function_tree(self) -> Tree:
75 """Build tree widget with functions grouped by backend."""
76 tree = Tree("Functions", id="function_tree")
78 # Expand the root node to show all categories
79 tree.root.expand()
81 # Add backend nodes
82 for backend, functions in self.functions_by_backend.items():
83 backend_node = tree.root.add(f"{backend} ({len(functions)} functions)")
84 backend_node.data = {"type": "backend", "name": backend}
86 # Expand all backend nodes by default
87 backend_node.expand()
89 # Add function nodes
90 for func, display_name in functions:
91 func_node = backend_node.add(display_name)
92 func_node.data = {"type": "function", "func": func, "name": display_name}
94 return tree
96 def on_input_changed(self, event: Input.Changed) -> None:
97 """Handle search input changes."""
98 if event.input.id == "search_input":
99 self._filter_functions(event.value)
101 def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
102 """Handle tree node selection."""
103 if event.node.data and event.node.data.get("type") == "function":
104 self.selected_function = event.node.data["func"]
105 # Enable select button
106 select_btn = self.query_one("#select_btn", Button)
107 select_btn.disabled = False
108 else:
109 self.selected_function = None
110 # Disable select button
111 select_btn = self.query_one("#select_btn", Button)
112 select_btn.disabled = True
114 def on_button_pressed(self, event: Button.Pressed) -> None:
115 """Handle button presses."""
116 if event.button.id == "select_btn" and self.selected_function:
117 if self.on_result_callback:
118 self.on_result_callback(self.selected_function)
119 self.close_window()
120 elif event.button.id == "cancel_btn":
121 if self.on_result_callback:
122 self.on_result_callback(None)
123 self.close_window()
125 def _filter_functions(self, search_term: str) -> None:
126 """Filter functions based on search term."""
127 tree = self.query_one("#function_tree", Tree)
129 if not search_term.strip():
130 # Show all functions
131 tree.clear()
132 tree.root.label = "Functions"
133 self._populate_tree(tree, self.functions_by_backend)
134 else:
135 # Filter functions
136 search_lower = search_term.lower()
137 filtered_functions = {}
139 for backend, functions in self.functions_by_backend.items():
140 matching_functions = [
141 (func, display_name) for func, display_name in functions
142 if search_lower in display_name.lower()
143 ]
144 if matching_functions:
145 filtered_functions[backend] = matching_functions
147 tree.clear()
148 tree.root.label = f"Functions (filtered: {search_term})"
149 self._populate_tree(tree, filtered_functions)
151 def _populate_tree(self, tree: Tree, functions_by_backend: dict) -> None:
152 """Populate tree with function data."""
153 # Expand the root node to show all categories
154 tree.root.expand()
156 for backend, functions in functions_by_backend.items():
157 backend_node = tree.root.add(f"{backend} ({len(functions)} functions)")
158 backend_node.data = {"type": "backend", "name": backend}
160 # Expand all backend nodes by default
161 backend_node.expand()
163 for func, display_name in functions:
164 func_node = backend_node.add(display_name)
165 func_node.data = {"type": "function", "func": func, "name": display_name}