@@ -1,12 +1,13 @@ |
| 1 | //! Application state and event loop. | 1 | //! Application state and event loop. |
| 2 | | 2 | |
| | 3 | +use crate::PickerConfig; |
| 3 | use garfield::core::{ | 4 | use garfield::core::{ |
| 4 | Clipboard, ClipboardOperation, DragTarget, FileOperation, FileDragController, ImagePreviewLoader, PdfPreviewLoader, PreviewLoader, UndoStack, | 5 | Clipboard, ClipboardOperation, DragTarget, FileOperation, FileDragController, ImagePreviewLoader, PdfPreviewLoader, PreviewLoader, UndoStack, |
| 5 | copy_files, move_files, delete_files, create_directory, | 6 | copy_files, move_files, delete_files, create_directory, |
| 6 | - trash_files, restore_from_trash, | 7 | + trash_files, restore_from_trash, matches_any_filter, |
| 7 | }; | 8 | }; |
| 8 | use garfield::ui::pane::SplitDirection; | 9 | use garfield::ui::pane::SplitDirection; |
| 9 | -use garfield::ui::{AddressBar, AppPickerDialog, AppPickerResult, Breadcrumb, ConfirmDialog, ConflictAction, ConflictDialog, ContextMenu, ContextMenuAction, ContextType, DialogResult, HelpModal, IconSize, InputDialog, InputResult, Pane, ProgressDialog, Sidebar, StatusBar, TabBar, TabInfo, Toolbar, ToolbarAction, ViewMode, TAB_BAR_HEIGHT, TOOLBAR_HEIGHT}; | 10 | +use garfield::ui::{AddressBar, AppPickerDialog, AppPickerResult, Breadcrumb, ConfirmDialog, ConflictAction, ConflictDialog, ContextMenu, ContextMenuAction, ContextType, DialogResult, HelpModal, IconSize, InputDialog, InputResult, Pane, PaneToolbarClick, PickerToolbar, PickerToolbarClick, ProgressDialog, Sidebar, StatusBar, TabBar, TabBarClickResult, TabInfo, Toolbar, ToolbarAction, ViewMode, TAB_BAR_HEIGHT, TOOLBAR_HEIGHT, PICKER_TOOLBAR_HEIGHT}; |
| 10 | use anyhow::Result; | 11 | use anyhow::Result; |
| 11 | use gartk_core::{InputEvent, Key, MouseButton, Point, Rect, Theme}; | 12 | use gartk_core::{InputEvent, Key, MouseButton, Point, Rect, Theme}; |
| 12 | use gartk_render::{Renderer, TextStyle}; | 13 | use gartk_render::{Renderer, TextStyle}; |
@@ -104,6 +105,10 @@ pub struct App { |
| 104 | pdf_preview_loader: PdfPreviewLoader, | 105 | pdf_preview_loader: PdfPreviewLoader, |
| 105 | /// X11 clipboard manager for system clipboard integration. | 106 | /// X11 clipboard manager for system clipboard integration. |
| 106 | x11_clipboard: ClipboardManager, | 107 | x11_clipboard: ClipboardManager, |
| | 108 | + /// Picker mode configuration (None for normal browser). |
| | 109 | + picker_config: PickerConfig, |
| | 110 | + /// Picker toolbar (only used in picker mode). |
| | 111 | + picker_toolbar: Option<PickerToolbar>, |
| 107 | } | 112 | } |
| 108 | | 113 | |
| 109 | /// State for a paste operation with conflicts. | 114 | /// State for a paste operation with conflicts. |
@@ -120,31 +125,48 @@ struct PendingPaste { |
| 120 | | 125 | |
| 121 | impl App { | 126 | impl App { |
| 122 | /// Create a new application. | 127 | /// Create a new application. |
| 123 | - pub fn new(start_dir: Option<PathBuf>) -> Result<Self> { | 128 | + pub fn new(start_dir: Option<PathBuf>, picker_config: PickerConfig) -> Result<Self> { |
| 124 | // Connect to X11 | 129 | // Connect to X11 |
| 125 | let conn = Connection::connect(None)?; | 130 | let conn = Connection::connect(None)?; |
| 126 | | 131 | |
| 127 | // Get primary monitor for window sizing | 132 | // Get primary monitor for window sizing |
| 128 | let monitor = gartk_x11::primary_monitor(&conn)?; | 133 | let monitor = gartk_x11::primary_monitor(&conn)?; |
| 129 | | 134 | |
| 130 | - // Calculate window size (70% of screen) | 135 | + // Calculate window size (70% of screen, smaller for picker mode) |
| 131 | - let width = (monitor.rect.width as f64 * 0.7) as u32; | 136 | + let scale = if picker_config.is_picker() { 0.5 } else { 0.7 }; |
| 132 | - let height = (monitor.rect.height as f64 * 0.7) as u32; | 137 | + let width = (monitor.rect.width as f64 * scale) as u32; |
| | 138 | + let height = (monitor.rect.height as f64 * scale) as u32; |
| 133 | let x = monitor.rect.x + (monitor.rect.width as i32 - width as i32) / 2; | 139 | let x = monitor.rect.x + (monitor.rect.width as i32 - width as i32) / 2; |
| 134 | let y = monitor.rect.y + (monitor.rect.height as i32 - height as i32) / 2; | 140 | let y = monitor.rect.y + (monitor.rect.height as i32 - height as i32) / 2; |
| 135 | | 141 | |
| 136 | - // Create window | 142 | + // Window title depends on mode |
| 137 | - let window = Window::create( | 143 | + let title = if picker_config.is_picker() { |
| 138 | - conn.clone(), | 144 | + picker_config.title.clone().unwrap_or_else(|| { |
| 139 | - WindowConfig::default() | 145 | + if picker_config.mode.is_directory_mode() { |
| 140 | - .title("garfield") | 146 | + "Select Folder".to_string() |
| 141 | - .class("garfield") | 147 | + } else { |
| 142 | - .position(x, y) | 148 | + "Open File".to_string() |
| 143 | - .size(width, height) | 149 | + } |
| 144 | - .transparent(false), | 150 | + }) |
| 145 | - )?; | 151 | + } else { |
| | 152 | + "garfield".to_string() |
| | 153 | + }; |
| 146 | | 154 | |
| 147 | - window.focus()?; | 155 | + // Create window - use Dialog type for picker mode |
| | 156 | + let mut window_config = WindowConfig::default() |
| | 157 | + .title(&title) |
| | 158 | + .class("garfield") |
| | 159 | + .position(x, y) |
| | 160 | + .size(width, height) |
| | 161 | + .transparent(false); |
| | 162 | + |
| | 163 | + // Use Dialog window type for picker mode (better focus handling) |
| | 164 | + if picker_config.is_picker() { |
| | 165 | + window_config = window_config.window_type(gartk_x11::WindowType::Dialog); |
| | 166 | + } |
| | 167 | + |
| | 168 | + let window = Window::create(conn.clone(), window_config)?; |
| | 169 | + conn.flush()?; |
| 148 | | 170 | |
| 149 | // Create X11 clipboard manager for system clipboard integration | 171 | // Create X11 clipboard manager for system clipboard integration |
| 150 | let x11_clipboard = ClipboardManager::new(conn.clone(), window.id())?; | 172 | let x11_clipboard = ClipboardManager::new(conn.clone(), window.id())?; |
@@ -174,6 +196,26 @@ impl App { |
| 174 | ); | 196 | ); |
| 175 | let toolbar = Toolbar::new(toolbar_bounds); | 197 | let toolbar = Toolbar::new(toolbar_bounds); |
| 176 | | 198 | |
| | 199 | + // Create picker toolbar at BOTTOM of window (only if in picker mode) |
| | 200 | + let picker_toolbar = if picker_config.is_picker() { |
| | 201 | + let picker_toolbar_bounds = Rect::new( |
| | 202 | + sidebar_w as i32, |
| | 203 | + (height - PICKER_TOOLBAR_HEIGHT) as i32, |
| | 204 | + width - sidebar_w, |
| | 205 | + PICKER_TOOLBAR_HEIGHT, |
| | 206 | + ); |
| | 207 | + let mut pt = PickerToolbar::new(picker_toolbar_bounds, picker_config.accept_label.clone()); |
| | 208 | + // Set filter description if we have filters |
| | 209 | + let filters = picker_config.mode.filters(); |
| | 210 | + if !filters.is_empty() { |
| | 211 | + let desc = format!("Filter: {}", filters.join(", ")); |
| | 212 | + pt.set_filter_description(Some(desc)); |
| | 213 | + } |
| | 214 | + Some(pt) |
| | 215 | + } else { |
| | 216 | + None |
| | 217 | + }; |
| | 218 | + |
| 177 | // Create breadcrumb (below toolbar) | 219 | // Create breadcrumb (below toolbar) |
| 178 | let breadcrumb_bounds = Rect::new( | 220 | let breadcrumb_bounds = Rect::new( |
| 179 | sidebar_w as i32, | 221 | sidebar_w as i32, |
@@ -227,11 +269,17 @@ impl App { |
| 227 | let app_picker = AppPickerDialog::new(Rect::new(0, 0, width, height)); | 269 | let app_picker = AppPickerDialog::new(Rect::new(0, 0, width, height)); |
| 228 | | 270 | |
| 229 | // Content area bounds (for panes) | 271 | // Content area bounds (for panes) |
| | 272 | + // In picker mode, picker toolbar replaces status bar at bottom |
| | 273 | + let footer_height = if picker_config.is_picker() { |
| | 274 | + PICKER_TOOLBAR_HEIGHT |
| | 275 | + } else { |
| | 276 | + STATUS_BAR_HEIGHT |
| | 277 | + }; |
| 230 | let content_bounds = Rect::new( | 278 | let content_bounds = Rect::new( |
| 231 | sidebar_w as i32, | 279 | sidebar_w as i32, |
| 232 | header_height as i32, | 280 | header_height as i32, |
| 233 | width - sidebar_w, | 281 | width - sidebar_w, |
| 234 | - height - header_height - STATUS_BAR_HEIGHT, | 282 | + height - header_height - footer_height, |
| 235 | ); | 283 | ); |
| 236 | | 284 | |
| 237 | // Create root pane with initial tab | 285 | // Create root pane with initial tab |
@@ -286,6 +334,8 @@ impl App { |
| 286 | image_preview_loader: ImagePreviewLoader::new(), | 334 | image_preview_loader: ImagePreviewLoader::new(), |
| 287 | pdf_preview_loader: PdfPreviewLoader::new(), | 335 | pdf_preview_loader: PdfPreviewLoader::new(), |
| 288 | x11_clipboard, | 336 | x11_clipboard, |
| | 337 | + picker_config, |
| | 338 | + picker_toolbar, |
| 289 | }; | 339 | }; |
| 290 | | 340 | |
| 291 | app.update_status_bar(); | 341 | app.update_status_bar(); |
@@ -303,6 +353,96 @@ impl App { |
| 303 | self.root_pane.leaf_by_id_mut(self.focused_pane_id) | 353 | self.root_pane.leaf_by_id_mut(self.focused_pane_id) |
| 304 | } | 354 | } |
| 305 | | 355 | |
| | 356 | + /// Check if the current selection is valid for picker mode. |
| | 357 | + fn has_valid_picker_selection(&self) -> bool { |
| | 358 | + let Some(pane) = self.focused_pane() else { |
| | 359 | + return false; |
| | 360 | + }; |
| | 361 | + let Some(tab) = pane.active_tab() else { |
| | 362 | + return false; |
| | 363 | + }; |
| | 364 | + |
| | 365 | + let selected = tab.selected_entries(); |
| | 366 | + let filters = self.picker_config.mode.filters(); |
| | 367 | + |
| | 368 | + // In directory mode, we can always accept (use current directory if nothing selected) |
| | 369 | + if self.picker_config.mode.is_directory_mode() { |
| | 370 | + // If nothing selected, the current directory is the selection |
| | 371 | + if selected.is_empty() { |
| | 372 | + return true; |
| | 373 | + } |
| | 374 | + // Otherwise, at least one directory must be selected |
| | 375 | + return selected.iter().any(|e| e.is_dir()); |
| | 376 | + } |
| | 377 | + |
| | 378 | + // In file mode, we need at least one file selected that matches filters |
| | 379 | + // If multiple is disabled, we need exactly one matching file |
| | 380 | + let matching_file_count = selected.iter() |
| | 381 | + .filter(|e| !e.is_dir() && matches_any_filter(e, filters)) |
| | 382 | + .count(); |
| | 383 | + |
| | 384 | + if matching_file_count == 0 { |
| | 385 | + return false; |
| | 386 | + } |
| | 387 | + |
| | 388 | + if !self.picker_config.mode.allows_multiple() && matching_file_count > 1 { |
| | 389 | + return false; |
| | 390 | + } |
| | 391 | + |
| | 392 | + true |
| | 393 | + } |
| | 394 | + |
| | 395 | + /// Get the selected paths for picker mode. |
| | 396 | + fn get_picker_selection(&self) -> Vec<PathBuf> { |
| | 397 | + let Some(pane) = self.focused_pane() else { |
| | 398 | + return Vec::new(); |
| | 399 | + }; |
| | 400 | + let Some(tab) = pane.active_tab() else { |
| | 401 | + return Vec::new(); |
| | 402 | + }; |
| | 403 | + |
| | 404 | + let filters = self.picker_config.mode.filters(); |
| | 405 | + |
| | 406 | + if self.picker_config.mode.is_directory_mode() { |
| | 407 | + // In directory mode, return selected directories or current directory |
| | 408 | + let dirs: Vec<_> = tab.selected_entries().iter() |
| | 409 | + .filter(|e| e.is_dir()) |
| | 410 | + .map(|e| e.path.clone()) |
| | 411 | + .collect(); |
| | 412 | + |
| | 413 | + if dirs.is_empty() { |
| | 414 | + // Return current directory |
| | 415 | + vec![tab.current_path().to_path_buf()] |
| | 416 | + } else { |
| | 417 | + dirs |
| | 418 | + } |
| | 419 | + } else { |
| | 420 | + // In file mode, return selected files that match filters |
| | 421 | + tab.selected_entries().iter() |
| | 422 | + .filter(|e| !e.is_dir() && matches_any_filter(e, filters)) |
| | 423 | + .map(|e| e.path.clone()) |
| | 424 | + .collect() |
| | 425 | + } |
| | 426 | + } |
| | 427 | + |
| | 428 | + /// Output picker selection and exit. |
| | 429 | + fn accept_picker_selection(&mut self) { |
| | 430 | + let paths = self.get_picker_selection(); |
| | 431 | + |
| | 432 | + // Output paths to stdout (one per line) |
| | 433 | + for path in &paths { |
| | 434 | + println!("{}", path.display()); |
| | 435 | + } |
| | 436 | + |
| | 437 | + self.should_quit = true; |
| | 438 | + } |
| | 439 | + |
| | 440 | + /// Cancel picker and exit with no output. |
| | 441 | + fn cancel_picker(&mut self) { |
| | 442 | + // Exit with code 1 to indicate cancellation |
| | 443 | + self.should_quit = true; |
| | 444 | + } |
| | 445 | + |
| 306 | /// Run the application event loop. | 446 | /// Run the application event loop. |
| 307 | pub fn run(&mut self) -> Result<()> { | 447 | pub fn run(&mut self) -> Result<()> { |
| 308 | let mut event_loop = EventLoop::new(&self.window, EventLoopConfig::default())?; | 448 | let mut event_loop = EventLoop::new(&self.window, EventLoopConfig::default())?; |
@@ -527,19 +667,48 @@ impl App { |
| 527 | return; | 667 | return; |
| 528 | } | 668 | } |
| 529 | | 669 | |
| 530 | - // Handle tab bar close button clicks (non-drag) | 670 | + // Handle tab bar clicks (non-drag) |
| 531 | - if let Some((tab_index, is_close)) = self.tab_bar.on_click(pos) { | 671 | + match self.tab_bar.on_click(pos) { |
| 532 | - if is_close { | 672 | + TabBarClickResult::Tab(tab_index, is_close) => { |
| 533 | - self.close_tab(tab_index); | 673 | + if is_close { |
| | 674 | + self.close_tab(tab_index); |
| | 675 | + } |
| | 676 | + // Tab selection happens on mouse release if not dragged |
| | 677 | + return; |
| | 678 | + } |
| | 679 | + TabBarClickResult::NewTab => { |
| | 680 | + self.new_tab(); |
| | 681 | + return; |
| | 682 | + } |
| | 683 | + TabBarClickResult::None => {} |
| | 684 | + } |
| | 685 | + } |
| | 686 | + |
| | 687 | + // Check picker toolbar clicks (if in picker mode) |
| | 688 | + if let Some(picker_toolbar) = &self.picker_toolbar { |
| | 689 | + match picker_toolbar.on_click(pos) { |
| | 690 | + PickerToolbarClick::Accept => { |
| | 691 | + self.accept_picker_selection(); |
| | 692 | + return; |
| | 693 | + } |
| | 694 | + PickerToolbarClick::Cancel => { |
| | 695 | + self.cancel_picker(); |
| | 696 | + return; |
| 534 | } | 697 | } |
| 535 | - // Tab selection happens on mouse release if not dragged | 698 | + PickerToolbarClick::None => {} |
| | 699 | + } |
| | 700 | + } else { |
| | 701 | + // Check normal toolbar clicks |
| | 702 | + if let Some(action) = self.toolbar.on_click(pos) { |
| | 703 | + self.handle_toolbar_action(action); |
| 536 | return; | 704 | return; |
| 537 | } | 705 | } |
| 538 | } | 706 | } |
| 539 | | 707 | |
| 540 | - // Check toolbar clicks | 708 | + // Check pane toolbar clicks (view mode buttons) |
| 541 | - if let Some(action) = self.toolbar.on_click(pos) { | 709 | + if let PaneToolbarClick::ViewMode(_mode) = self.root_pane.on_toolbar_click(pos) { |
| 542 | - self.handle_toolbar_action(action); | 710 | + self.sync_toolbar_view(); |
| | 711 | + self.update_status_bar(); |
| 543 | return; | 712 | return; |
| 544 | } | 713 | } |
| 545 | | 714 | |
@@ -888,10 +1057,15 @@ impl App { |
| 888 | } | 1057 | } |
| 889 | | 1058 | |
| 890 | // Check hover states - only redraw if any changed | 1059 | // Check hover states - only redraw if any changed |
| 891 | - needs_redraw |= self.toolbar.on_mouse_move(pos); | 1060 | + if let Some(picker_toolbar) = &mut self.picker_toolbar { |
| | 1061 | + needs_redraw |= picker_toolbar.on_mouse_move(pos); |
| | 1062 | + } else { |
| | 1063 | + needs_redraw |= self.toolbar.on_mouse_move(pos); |
| | 1064 | + } |
| 892 | needs_redraw |= self.breadcrumb.on_mouse_move(pos); | 1065 | needs_redraw |= self.breadcrumb.on_mouse_move(pos); |
| 893 | needs_redraw |= self.sidebar.on_mouse_move(pos); | 1066 | needs_redraw |= self.sidebar.on_mouse_move(pos); |
| 894 | needs_redraw |= self.tab_bar.on_mouse_move(pos); | 1067 | needs_redraw |= self.tab_bar.on_mouse_move(pos); |
| | 1068 | + needs_redraw |= self.root_pane.on_toolbar_mouse_move(pos); |
| 895 | | 1069 | |
| 896 | let mut is_dragging = false; | 1070 | let mut is_dragging = false; |
| 897 | let mut selection_count = 0; | 1071 | let mut selection_count = 0; |
@@ -1003,6 +1177,12 @@ impl App { |
| 1003 | return; | 1177 | return; |
| 1004 | } | 1178 | } |
| 1005 | | 1179 | |
| | 1180 | + // Handle Escape in picker mode (cancel) |
| | 1181 | + if *key == Key::Escape && self.picker_config.is_picker() { |
| | 1182 | + self.cancel_picker(); |
| | 1183 | + return; |
| | 1184 | + } |
| | 1185 | + |
| 1006 | // F1 toggles help | 1186 | // F1 toggles help |
| 1007 | if *key == Key::F1 { | 1187 | if *key == Key::F1 { |
| 1008 | self.help_modal.show(); | 1188 | self.help_modal.show(); |
@@ -1350,13 +1530,7 @@ impl App { |
| 1350 | | 1530 | |
| 1351 | /// Create a new tab in the focused pane. | 1531 | /// Create a new tab in the focused pane. |
| 1352 | fn new_tab(&mut self) { | 1532 | fn new_tab(&mut self) { |
| 1353 | - let path = if let Some(pane) = self.focused_pane() { | 1533 | + let path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")); |
| 1354 | - pane.active_tab().map(|t| t.current_path().clone()) | | |
| 1355 | - } else { | | |
| 1356 | - None | | |
| 1357 | - }; | | |
| 1358 | - | | |
| 1359 | - let path = path.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))); | | |
| 1360 | | 1534 | |
| 1361 | if let Some(pane) = self.focused_pane_mut() { | 1535 | if let Some(pane) = self.focused_pane_mut() { |
| 1362 | pane.add_tab(path); | 1536 | pane.add_tab(path); |
@@ -1438,17 +1612,21 @@ impl App { |
| 1438 | | 1612 | |
| 1439 | /// Split the focused pane horizontally. | 1613 | /// Split the focused pane horizontally. |
| 1440 | fn split_horizontal(&mut self) { | 1614 | fn split_horizontal(&mut self) { |
| 1441 | - let path = if let Some(pane) = self.focused_pane() { | 1615 | + let (path, view_mode) = if let Some(pane) = self.focused_pane() { |
| 1442 | - pane.active_tab().map(|t| t.current_path().clone()) | 1616 | + if let Some(tab) = pane.active_tab() { |
| | 1617 | + (Some(tab.current_path().clone()), Some(tab.view_mode())) |
| | 1618 | + } else { |
| | 1619 | + (None, None) |
| | 1620 | + } |
| 1443 | } else { | 1621 | } else { |
| 1444 | - None | 1622 | + (None, None) |
| 1445 | }; | 1623 | }; |
| 1446 | | 1624 | |
| 1447 | let path = path.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))); | 1625 | let path = path.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))); |
| 1448 | let new_id = self.next_pane_id; | 1626 | let new_id = self.next_pane_id; |
| 1449 | | 1627 | |
| 1450 | if let Some(pane) = self.focused_pane_mut() { | 1628 | if let Some(pane) = self.focused_pane_mut() { |
| 1451 | - if pane.split(SplitDirection::Horizontal, path, new_id).is_some() { | 1629 | + if pane.split(SplitDirection::Horizontal, path, new_id, view_mode).is_some() { |
| 1452 | self.next_pane_id += 1; | 1630 | self.next_pane_id += 1; |
| 1453 | self.focused_pane_id = new_id; | 1631 | self.focused_pane_id = new_id; |
| 1454 | } | 1632 | } |
@@ -1461,17 +1639,21 @@ impl App { |
| 1461 | | 1639 | |
| 1462 | /// Split the focused pane vertically. | 1640 | /// Split the focused pane vertically. |
| 1463 | fn split_vertical(&mut self) { | 1641 | fn split_vertical(&mut self) { |
| 1464 | - let path = if let Some(pane) = self.focused_pane() { | 1642 | + let (path, view_mode) = if let Some(pane) = self.focused_pane() { |
| 1465 | - pane.active_tab().map(|t| t.current_path().clone()) | 1643 | + if let Some(tab) = pane.active_tab() { |
| | 1644 | + (Some(tab.current_path().clone()), Some(tab.view_mode())) |
| | 1645 | + } else { |
| | 1646 | + (None, None) |
| | 1647 | + } |
| 1466 | } else { | 1648 | } else { |
| 1467 | - None | 1649 | + (None, None) |
| 1468 | }; | 1650 | }; |
| 1469 | | 1651 | |
| 1470 | let path = path.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))); | 1652 | let path = path.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))); |
| 1471 | let new_id = self.next_pane_id; | 1653 | let new_id = self.next_pane_id; |
| 1472 | | 1654 | |
| 1473 | if let Some(pane) = self.focused_pane_mut() { | 1655 | if let Some(pane) = self.focused_pane_mut() { |
| 1474 | - if pane.split(SplitDirection::Vertical, path, new_id).is_some() { | 1656 | + if pane.split(SplitDirection::Vertical, path, new_id, view_mode).is_some() { |
| 1475 | self.next_pane_id += 1; | 1657 | self.next_pane_id += 1; |
| 1476 | self.focused_pane_id = new_id; | 1658 | self.focused_pane_id = new_id; |
| 1477 | } | 1659 | } |
@@ -1553,6 +1735,29 @@ impl App { |
| 1553 | /// Enter the selected entry. | 1735 | /// Enter the selected entry. |
| 1554 | fn enter_selected(&mut self) { | 1736 | fn enter_selected(&mut self) { |
| 1555 | self.status_bar.clear_status_message(); | 1737 | self.status_bar.clear_status_message(); |
| | 1738 | + |
| | 1739 | + // In picker mode, check if we should accept the selection instead of navigating |
| | 1740 | + if self.picker_config.is_picker() { |
| | 1741 | + if let Some(pane) = self.focused_pane() { |
| | 1742 | + if let Some(tab) = pane.active_tab() { |
| | 1743 | + let selected = tab.selected_entries(); |
| | 1744 | + if !selected.is_empty() { |
| | 1745 | + // If it's a directory in non-directory mode, navigate into it |
| | 1746 | + if !self.picker_config.mode.is_directory_mode() { |
| | 1747 | + let first = &selected[0]; |
| | 1748 | + if first.is_dir() { |
| | 1749 | + // Fall through to normal enter behavior |
| | 1750 | + } else { |
| | 1751 | + // File selected - accept it |
| | 1752 | + self.accept_picker_selection(); |
| | 1753 | + return; |
| | 1754 | + } |
| | 1755 | + } |
| | 1756 | + } |
| | 1757 | + } |
| | 1758 | + } |
| | 1759 | + } |
| | 1760 | + |
| 1556 | if let Some(pane) = self.focused_pane_mut() { | 1761 | if let Some(pane) = self.focused_pane_mut() { |
| 1557 | if let Some(tab) = pane.active_tab_mut() { | 1762 | if let Some(tab) = pane.active_tab_mut() { |
| 1558 | tab.enter_selected(); | 1763 | tab.enter_selected(); |
@@ -2962,12 +3167,13 @@ impl App { |
| 2962 | TAB_BAR_HEIGHT, | 3167 | TAB_BAR_HEIGHT, |
| 2963 | )); | 3168 | )); |
| 2964 | | 3169 | |
| 2965 | - self.toolbar.set_bounds(Rect::new( | 3170 | + let toolbar_bounds = Rect::new( |
| 2966 | sidebar_w as i32, | 3171 | sidebar_w as i32, |
| 2967 | TAB_BAR_HEIGHT as i32, | 3172 | TAB_BAR_HEIGHT as i32, |
| 2968 | width - sidebar_w, | 3173 | width - sidebar_w, |
| 2969 | TOOLBAR_HEIGHT, | 3174 | TOOLBAR_HEIGHT, |
| 2970 | - )); | 3175 | + ); |
| | 3176 | + self.toolbar.set_bounds(toolbar_bounds); |
| 2971 | | 3177 | |
| 2972 | let breadcrumb_bounds = Rect::new( | 3178 | let breadcrumb_bounds = Rect::new( |
| 2973 | sidebar_w as i32, | 3179 | sidebar_w as i32, |
@@ -2978,20 +3184,38 @@ impl App { |
| 2978 | self.breadcrumb.set_bounds(breadcrumb_bounds); | 3184 | self.breadcrumb.set_bounds(breadcrumb_bounds); |
| 2979 | self.address_bar.set_bounds(breadcrumb_bounds); | 3185 | self.address_bar.set_bounds(breadcrumb_bounds); |
| 2980 | | 3186 | |
| | 3187 | + // Calculate footer height (status bar + picker toolbar if present) |
| | 3188 | + let footer_height = if self.picker_toolbar.is_some() { |
| | 3189 | + PICKER_TOOLBAR_HEIGHT // Picker toolbar replaces status bar |
| | 3190 | + } else { |
| | 3191 | + STATUS_BAR_HEIGHT |
| | 3192 | + }; |
| | 3193 | + |
| 2981 | let content_bounds = Rect::new( | 3194 | let content_bounds = Rect::new( |
| 2982 | sidebar_w as i32, | 3195 | sidebar_w as i32, |
| 2983 | header_height as i32, | 3196 | header_height as i32, |
| 2984 | width - sidebar_w, | 3197 | width - sidebar_w, |
| 2985 | - height - header_height - STATUS_BAR_HEIGHT, | 3198 | + height - header_height - footer_height, |
| 2986 | ); | 3199 | ); |
| 2987 | self.root_pane.set_bounds(content_bounds); | 3200 | self.root_pane.set_bounds(content_bounds); |
| 2988 | | 3201 | |
| 2989 | - self.status_bar.set_bounds(Rect::new( | 3202 | + // Update picker toolbar bounds at bottom (if in picker mode) |
| 2990 | - sidebar_w as i32, | 3203 | + if let Some(ref mut picker_toolbar) = self.picker_toolbar { |
| 2991 | - (height - STATUS_BAR_HEIGHT) as i32, | 3204 | + picker_toolbar.set_bounds(Rect::new( |
| 2992 | - width - sidebar_w, | 3205 | + sidebar_w as i32, |
| 2993 | - STATUS_BAR_HEIGHT, | 3206 | + (height - PICKER_TOOLBAR_HEIGHT) as i32, |
| 2994 | - )); | 3207 | + width - sidebar_w, |
| | 3208 | + PICKER_TOOLBAR_HEIGHT, |
| | 3209 | + )); |
| | 3210 | + } else { |
| | 3211 | + // Only show status bar when not in picker mode |
| | 3212 | + self.status_bar.set_bounds(Rect::new( |
| | 3213 | + sidebar_w as i32, |
| | 3214 | + (height - STATUS_BAR_HEIGHT) as i32, |
| | 3215 | + width - sidebar_w, |
| | 3216 | + STATUS_BAR_HEIGHT, |
| | 3217 | + )); |
| | 3218 | + } |
| 2995 | | 3219 | |
| 2996 | self.help_modal.set_bounds(Rect::new(0, 0, width, height)); | 3220 | self.help_modal.set_bounds(Rect::new(0, 0, width, height)); |
| 2997 | self.confirm_dialog.set_bounds(Rect::new(0, 0, width, height)); | 3221 | self.confirm_dialog.set_bounds(Rect::new(0, 0, width, height)); |
@@ -3018,7 +3242,7 @@ impl App { |
| 3018 | // Draw tab bar | 3242 | // Draw tab bar |
| 3019 | self.tab_bar.render(&self.renderer)?; | 3243 | self.tab_bar.render(&self.renderer)?; |
| 3020 | | 3244 | |
| 3021 | - // Update and draw toolbar | 3245 | + // Update and draw toolbar (or picker toolbar) |
| 3022 | let (can_back, can_forward) = if let Some(pane) = self.focused_pane() { | 3246 | let (can_back, can_forward) = if let Some(pane) = self.focused_pane() { |
| 3023 | if let Some(tab) = pane.active_tab() { | 3247 | if let Some(tab) = pane.active_tab() { |
| 3024 | (tab.can_go_back(), tab.can_go_forward()) | 3248 | (tab.can_go_back(), tab.can_go_forward()) |
@@ -3028,6 +3252,8 @@ impl App { |
| 3028 | } else { | 3252 | } else { |
| 3029 | (false, false) | 3253 | (false, false) |
| 3030 | }; | 3254 | }; |
| | 3255 | + |
| | 3256 | + // Always render the regular toolbar |
| 3031 | self.toolbar.set_nav_state(can_back, can_forward); | 3257 | self.toolbar.set_nav_state(can_back, can_forward); |
| 3032 | self.toolbar.render(&self.renderer)?; | 3258 | self.toolbar.render(&self.renderer)?; |
| 3033 | | 3259 | |
@@ -3051,8 +3277,17 @@ impl App { |
| 3051 | // Draw pane content | 3277 | // Draw pane content |
| 3052 | self.root_pane.render(&self.renderer, Some(self.focused_pane_id))?; | 3278 | self.root_pane.render(&self.renderer, Some(self.focused_pane_id))?; |
| 3053 | | 3279 | |
| 3054 | - // Draw status bar | 3280 | + // Draw status bar or picker toolbar at bottom |
| 3055 | - self.status_bar.render(&self.renderer)?; | 3281 | + if self.picker_toolbar.is_some() { |
| | 3282 | + // Picker mode: draw picker toolbar instead of status bar |
| | 3283 | + let has_valid_selection = self.has_valid_picker_selection(); |
| | 3284 | + let picker_toolbar = self.picker_toolbar.as_mut().unwrap(); |
| | 3285 | + picker_toolbar.set_accept_enabled(has_valid_selection); |
| | 3286 | + picker_toolbar.render(&self.renderer)?; |
| | 3287 | + } else { |
| | 3288 | + // Normal mode: draw status bar |
| | 3289 | + self.status_bar.render(&self.renderer)?; |
| | 3290 | + } |
| 3056 | | 3291 | |
| 3057 | // Draw toolbar tooltip overlay (on top of other UI) | 3292 | // Draw toolbar tooltip overlay (on top of other UI) |
| 3058 | self.toolbar.render_tooltip_overlay(&self.renderer)?; | 3293 | self.toolbar.render_tooltip_overlay(&self.renderer)?; |