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

1"""Function selector window for selecting functions from the registry.""" 

2 

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 

8 

9from openhcs.textual_tui.windows.base_window import BaseOpenHCSWindow 

10from openhcs.textual_tui.services.function_registry_service import FunctionRegistryService 

11 

12 

13class FunctionSelectorWindow(BaseOpenHCSWindow): 

14 """Window for selecting functions from the registry.""" 

15 

16 DEFAULT_CSS = """ 

17 FunctionSelectorWindow { 

18 width: 75; height: 25; 

19 min-width: 75; min-height: 25; 

20 } 

21 """ 

22 

23 def __init__(self, current_function: Optional[Callable] = None, on_result_callback: Optional[Callable] = None, **kwargs): 

24 """Initialize function selector window. 

25  

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 

35 

36 # Load function data 

37 self._load_function_data() 

38 

39 super().__init__( 

40 window_id="function_selector", 

41 title="Select Function", 

42 mode="temporary", 

43 **kwargs 

44 ) 

45 

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() 

50 

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)) 

56 

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 ) 

65 

66 # Function tree 

67 yield self._build_function_tree() 

68 

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) 

73 

74 def _build_function_tree(self) -> Tree: 

75 """Build tree widget with functions grouped by backend.""" 

76 tree = Tree("Functions", id="function_tree") 

77 

78 # Expand the root node to show all categories 

79 tree.root.expand() 

80 

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} 

85 

86 # Expand all backend nodes by default 

87 backend_node.expand() 

88 

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} 

93 

94 return tree 

95 

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) 

100 

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 

113 

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() 

124 

125 def _filter_functions(self, search_term: str) -> None: 

126 """Filter functions based on search term.""" 

127 tree = self.query_one("#function_tree", Tree) 

128 

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 = {} 

138 

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 

146 

147 tree.clear() 

148 tree.root.label = f"Functions (filtered: {search_term})" 

149 self._populate_tree(tree, filtered_functions) 

150 

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() 

155 

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} 

159 

160 # Expand all backend nodes by default 

161 backend_node.expand() 

162 

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}