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
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""File list widget for directory browsing."""
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
12from openhcs.constants.constants import Backend
13from openhcs.textual_tui.services.file_browser_service import FileItem, FileBrowserService, SelectionMode
15logger = logging.getLogger(__name__)
18class FileListWidget(ScrollableContainer):
19 """Widget for displaying and navigating file listings."""
21 current_path = reactive(Path)
22 listing = reactive(list)
23 focused_index = reactive(0)
24 backend = reactive(Backend.DISK) # Use Backend enum, not string
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
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}")
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")
52 def on_mount(self) -> None:
53 """Update display when widget is mounted."""
54 self._update_display()
56 def _get_listing_display(self) -> str:
57 """Get the formatted listing display."""
58 if not self.listing:
59 return "[center](empty directory)[/center]"
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}")
69 return "\n".join(lines)
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()
77 def navigate_to(self, path: Path) -> None:
78 """Navigate to a new directory."""
79 self.current_path = path
80 self.load_current_directory()
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)
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
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)
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()
114 def _handle_selection(self) -> None:
115 """Handle item selection."""
116 item = self.get_focused_item()
117 if item is None:
118 return
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)
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
140 def watch_current_path(self, new_path: Path) -> None:
141 """React to path changes."""
142 self.load_current_directory()
144 def watch_listing(self, new_listing: List[FileItem]) -> None:
145 """React to listing changes."""
146 self._update_display()
148 def watch_focused_index(self, new_index: int) -> None:
149 """React to focus changes."""
150 self._update_display()