Coverage for openhcs/textual_tui/widgets/file_list.py: 0.0%

98 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 05:57 +0000

1"""File list widget for directory browsing.""" 

2 

3import logging 

4from pathlib import Path 

5from typing import List, Optional, Callable 

6from textual.containers import ScrollableContainer 

7from textual.widgets import Static 

8from textual.app import ComposeResult 

9from textual.reactive import reactive 

10from textual import events 

11 

12from openhcs.constants.constants import Backend 

13from openhcs.textual_tui.services.file_browser_service import FileItem, FileBrowserService, SelectionMode 

14 

15logger = logging.getLogger(__name__) 

16 

17 

18class FileListWidget(ScrollableContainer): 

19 """Widget for displaying and navigating file listings.""" 

20 

21 current_path = reactive(Path) 

22 listing = reactive(list) 

23 focused_index = reactive(0) 

24 backend = reactive(Backend.DISK) # Use Backend enum, not string 

25 

26 def __init__(self, 

27 file_browser_service: FileBrowserService, 

28 initial_path: Path, 

29 backend: Backend = Backend.DISK, 

30 selection_mode: SelectionMode = SelectionMode.FILES_ONLY, 

31 on_selection: Optional[Callable] = None, 

32 **kwargs): 

33 super().__init__(**kwargs) 

34 self.file_browser_service = file_browser_service 

35 self.current_path = initial_path 

36 self.backend = backend 

37 self.selection_mode = selection_mode 

38 self.on_selection = on_selection 

39 self.listing = [] 

40 self.focused_index = 0 

41 

42 # Load initial directory (display will be updated on mount) 

43 self.listing = self.file_browser_service.load_directory(self.current_path, self.backend) 

44 self.focused_index = 0 

45 logger.debug(f"FileListWidget: Loaded {len(self.listing)} items from {self.current_path}") 

46 

47 def compose(self) -> ComposeResult: 

48 """Compose the file list.""" 

49 # Create a single static widget that we'll update dynamically 

50 yield Static(self._get_listing_display(), id="file_listing") 

51 

52 def on_mount(self) -> None: 

53 """Update display when widget is mounted.""" 

54 self._update_display() 

55 

56 def _get_listing_display(self) -> str: 

57 """Get the formatted listing display.""" 

58 if not self.listing: 

59 return "[center](empty directory)[/center]" 

60 

61 lines = [] 

62 for i, item in enumerate(self.listing): 

63 icon = "📁" if item.is_dir else "📄" 

64 prefix = ">" if i == self.focused_index else " " 

65 style = "[reverse]" if i == self.focused_index else "" 

66 end_style = "[/reverse]" if i == self.focused_index else "" 

67 lines.append(f"{prefix} {style}{icon} {item.name}{end_style}") 

68 

69 return "\n".join(lines) 

70 

71 def load_current_directory(self) -> None: 

72 """Load the current directory contents.""" 

73 self.listing = self.file_browser_service.load_directory(self.current_path, self.backend) 

74 self.focused_index = 0 

75 self._update_display() 

76 

77 def navigate_to(self, path: Path) -> None: 

78 """Navigate to a new directory.""" 

79 self.current_path = path 

80 self.load_current_directory() 

81 

82 def navigate_up(self) -> None: 

83 """Navigate to parent directory.""" 

84 parent = self.current_path.parent 

85 if parent != self.current_path: 

86 self.navigate_to(parent) 

87 

88 def get_focused_item(self) -> Optional[FileItem]: 

89 """Get the currently focused item.""" 

90 if 0 <= self.focused_index < len(self.listing): 

91 return self.listing[self.focused_index] 

92 return None 

93 

94 def can_select_focused(self) -> bool: 

95 """Check if focused item can be selected.""" 

96 item = self.get_focused_item() 

97 if item is None: 

98 return False 

99 return self.file_browser_service.can_select_item(item, self.selection_mode) 

100 

101 def on_key(self, event: events.Key) -> None: 

102 """Handle keyboard navigation.""" 

103 if event.key == "up": 

104 self.focused_index = max(0, self.focused_index - 1) 

105 self._update_display() 

106 elif event.key == "down": 

107 self.focused_index = min(len(self.listing) - 1, self.focused_index + 1) 

108 self._update_display() 

109 elif event.key == "enter": 

110 self._handle_selection() 

111 elif event.key == "backspace": 

112 self.navigate_up() 

113 

114 def _handle_selection(self) -> None: 

115 """Handle item selection.""" 

116 item = self.get_focused_item() 

117 if item is None: 

118 return 

119 

120 if item.is_dir: 

121 # Navigate into directory 

122 self.navigate_to(item.path) 

123 elif self.can_select_focused(): 

124 # Select file 

125 if self.on_selection: 

126 self.on_selection(item.path) 

127 

128 def _update_display(self) -> None: 

129 """Update the file listing display.""" 

130 try: 

131 listing_widget = self.query_one("#file_listing", Static) 

132 display_text = self._get_listing_display() 

133 listing_widget.update(display_text) 

134 logger.debug(f"FileListWidget: Updated display with {len(self.listing)} items") 

135 except Exception as e: 

136 logger.debug(f"FileListWidget: Failed to update display: {e}") 

137 # Widget might not be mounted yet 

138 pass 

139 

140 def watch_current_path(self, new_path: Path) -> None: 

141 """React to path changes.""" 

142 self.load_current_directory() 

143 

144 def watch_listing(self, new_listing: List[FileItem]) -> None: 

145 """React to listing changes.""" 

146 self._update_display() 

147 

148 def watch_focused_index(self, new_index: int) -> None: 

149 """React to focus changes.""" 

150 self._update_display()