@@ -6,7 +6,7 @@ use garfield::core::{ |
| 6 | 6 | trash_files, restore_from_trash, |
| 7 | 7 | }; |
| 8 | 8 | use garfield::ui::pane::SplitDirection; |
| 9 | | -use garfield::ui::{AddressBar, Breadcrumb, ConfirmDialog, ConflictAction, ConflictDialog, ContextMenu, ContextMenuAction, ContextType, DialogResult, HelpModal, Pane, ProgressDialog, Sidebar, StatusBar, TabBar, TabInfo, Toolbar, ToolbarAction, ViewMode, TAB_BAR_HEIGHT, TOOLBAR_HEIGHT}; |
| 9 | +use garfield::ui::{AddressBar, Breadcrumb, ConfirmDialog, ConflictAction, ConflictDialog, ContextMenu, ContextMenuAction, ContextType, DialogResult, HelpModal, InputDialog, InputResult, Pane, ProgressDialog, Sidebar, StatusBar, TabBar, TabInfo, Toolbar, ToolbarAction, ViewMode, TAB_BAR_HEIGHT, TOOLBAR_HEIGHT}; |
| 10 | 10 | use anyhow::Result; |
| 11 | 11 | use gartk_core::{InputEvent, Key, MouseButton, Point, Rect, Theme}; |
| 12 | 12 | use gartk_render::{Renderer, Surface, TextStyle}; |
@@ -82,6 +82,10 @@ pub struct App { |
| 82 | 82 | progress_dialog: ProgressDialog, |
| 83 | 83 | /// Context menu for right-click actions. |
| 84 | 84 | context_menu: ContextMenu, |
| 85 | + /// Input dialog for text entry. |
| 86 | + input_dialog: InputDialog, |
| 87 | + /// Path pending "Open With" custom application. |
| 88 | + pending_open_with_path: Option<PathBuf>, |
| 85 | 89 | /// Paths pending delete confirmation. |
| 86 | 90 | pending_delete_paths: Vec<PathBuf>, |
| 87 | 91 | /// Undo/redo stack for file operations. |
@@ -201,6 +205,9 @@ impl App { |
| 201 | 205 | // Create context menu (full window bounds for positioning) |
| 202 | 206 | let context_menu = ContextMenu::new(Rect::new(0, 0, width, height)); |
| 203 | 207 | |
| 208 | + // Create input dialog (full window bounds) |
| 209 | + let input_dialog = InputDialog::new(Rect::new(0, 0, width, height)); |
| 210 | + |
| 204 | 211 | // Content area bounds (for panes) |
| 205 | 212 | let content_bounds = Rect::new( |
| 206 | 213 | sidebar_w as i32, |
@@ -250,6 +257,8 @@ impl App { |
| 250 | 257 | conflict_dialog, |
| 251 | 258 | progress_dialog, |
| 252 | 259 | context_menu, |
| 260 | + input_dialog, |
| 261 | + pending_open_with_path: None, |
| 253 | 262 | pending_delete_paths: Vec::new(), |
| 254 | 263 | undo_stack: UndoStack::new(), |
| 255 | 264 | pending_paste: None, |
@@ -359,6 +368,14 @@ impl App { |
| 359 | 368 | return; |
| 360 | 369 | } |
| 361 | 370 | |
| 371 | + // Check input dialog |
| 372 | + if self.input_dialog.is_visible() { |
| 373 | + if let Some(result) = self.input_dialog.on_click(pos) { |
| 374 | + self.handle_input_result(result); |
| 375 | + } |
| 376 | + return; |
| 377 | + } |
| 378 | + |
| 362 | 379 | // Check help modal (clicking outside closes it) |
| 363 | 380 | if self.help_modal.on_click(pos) { |
| 364 | 381 | return; |
@@ -612,6 +629,12 @@ impl App { |
| 612 | 629 | return; |
| 613 | 630 | } |
| 614 | 631 | |
| 632 | + // Handle input dialog hover |
| 633 | + if self.input_dialog.is_visible() { |
| 634 | + self.input_dialog.on_mouse_move(pos); |
| 635 | + return; |
| 636 | + } |
| 637 | + |
| 615 | 638 | // Handle context menu hover |
| 616 | 639 | if self.context_menu.is_visible() { |
| 617 | 640 | self.context_menu.on_mouse_move(pos); |
@@ -710,6 +733,14 @@ impl App { |
| 710 | 733 | return; |
| 711 | 734 | } |
| 712 | 735 | |
| 736 | + // Handle input dialog when visible |
| 737 | + if self.input_dialog.is_visible() { |
| 738 | + if let Some(result) = self.input_dialog.handle_key(key) { |
| 739 | + self.handle_input_result(result); |
| 740 | + } |
| 741 | + return; |
| 742 | + } |
| 743 | + |
| 713 | 744 | // Handle help modal when visible |
| 714 | 745 | if self.help_modal.is_visible() { |
| 715 | 746 | match key { |
@@ -1516,6 +1547,19 @@ impl App { |
| 1516 | 1547 | } |
| 1517 | 1548 | } |
| 1518 | 1549 | |
| 1550 | + /// Handle input dialog result. |
| 1551 | + fn handle_input_result(&mut self, result: InputResult) { |
| 1552 | + match result { |
| 1553 | + InputResult::Submitted(value) => { |
| 1554 | + // Currently only used for "Open With" custom application |
| 1555 | + self.open_with_custom(&value); |
| 1556 | + } |
| 1557 | + InputResult::Cancelled => { |
| 1558 | + self.pending_open_with_path = None; |
| 1559 | + } |
| 1560 | + } |
| 1561 | + } |
| 1562 | + |
| 1519 | 1563 | /// Handle conflict dialog result. |
| 1520 | 1564 | fn handle_conflict_action(&mut self, action: ConflictAction) { |
| 1521 | 1565 | let pending = match self.pending_paste.take() { |
@@ -1820,29 +1864,49 @@ impl App { |
| 1820 | 1864 | |
| 1821 | 1865 | /// Open selected item with a specific application. |
| 1822 | 1866 | fn open_with(&mut self, app: &str) { |
| 1823 | | - if app.is_empty() { |
| 1824 | | - self.status_bar.set_status_message("Application picker not implemented"); |
| 1867 | + let paths = self.get_selected_paths(); |
| 1868 | + let Some(path) = paths.first().cloned() else { |
| 1869 | + return; |
| 1870 | + }; |
| 1871 | + |
| 1872 | + // Handle $CUSTOM - show input dialog |
| 1873 | + if app == "$CUSTOM" { |
| 1874 | + self.pending_open_with_path = Some(path); |
| 1875 | + self.input_dialog.show("Open With", "Enter application name:", ""); |
| 1825 | 1876 | return; |
| 1826 | 1877 | } |
| 1827 | 1878 | |
| 1828 | | - let paths = self.get_selected_paths(); |
| 1829 | | - if let Some(path) = paths.first() { |
| 1830 | | - // Resolve special application identifiers |
| 1831 | | - let resolved_app = match app { |
| 1832 | | - "$EDITOR" => self.resolve_text_editor(), |
| 1833 | | - "$FILEMANAGER" => self.resolve_file_manager(), |
| 1834 | | - other => Some(other.to_string()), |
| 1835 | | - }; |
| 1879 | + // Resolve special application identifiers |
| 1880 | + let resolved_app = match app { |
| 1881 | + "$EDITOR" => self.resolve_text_editor(), |
| 1882 | + other => Some(other.to_string()), |
| 1883 | + }; |
| 1836 | 1884 | |
| 1837 | | - let Some(app_cmd) = resolved_app else { |
| 1838 | | - self.status_bar.set_status_message("No suitable application found"); |
| 1839 | | - return; |
| 1840 | | - }; |
| 1885 | + let Some(app_cmd) = resolved_app else { |
| 1886 | + self.status_bar.set_status_message("No suitable application found"); |
| 1887 | + return; |
| 1888 | + }; |
| 1841 | 1889 | |
| 1842 | | - match std::process::Command::new(&app_cmd).arg(path).spawn() { |
| 1843 | | - Ok(_) => self.status_bar.set_status_message(format!("Opened with {}", app_cmd)), |
| 1844 | | - Err(e) => self.status_bar.set_status_message(format!("Failed to open with {}: {}", app_cmd, e)), |
| 1845 | | - } |
| 1890 | + match std::process::Command::new(&app_cmd).arg(&path).spawn() { |
| 1891 | + Ok(_) => self.status_bar.set_status_message(format!("Opened with {}", app_cmd)), |
| 1892 | + Err(e) => self.status_bar.set_status_message(format!("Failed to open with {}: {}", app_cmd, e)), |
| 1893 | + } |
| 1894 | + } |
| 1895 | + |
| 1896 | + /// Open file with custom application (from input dialog). |
| 1897 | + fn open_with_custom(&mut self, app_name: &str) { |
| 1898 | + let Some(path) = self.pending_open_with_path.take() else { |
| 1899 | + return; |
| 1900 | + }; |
| 1901 | + |
| 1902 | + if app_name.is_empty() { |
| 1903 | + self.status_bar.set_status_message("No application specified"); |
| 1904 | + return; |
| 1905 | + } |
| 1906 | + |
| 1907 | + match std::process::Command::new(app_name).arg(&path).spawn() { |
| 1908 | + Ok(_) => self.status_bar.set_status_message(format!("Opened with {}", app_name)), |
| 1909 | + Err(e) => self.status_bar.set_status_message(format!("Failed to open with {}: {}", app_name, e)), |
| 1846 | 1910 | } |
| 1847 | 1911 | } |
| 1848 | 1912 | |
@@ -1872,27 +1936,6 @@ impl App { |
| 1872 | 1936 | Some("xdg-open".to_string()) |
| 1873 | 1937 | } |
| 1874 | 1938 | |
| 1875 | | - /// Resolve file manager from environment or common file managers. |
| 1876 | | - fn resolve_file_manager(&self) -> Option<String> { |
| 1877 | | - // Check XDG default |
| 1878 | | - if let Ok(fm) = std::env::var("FILE_MANAGER") { |
| 1879 | | - if !fm.is_empty() && self.command_exists(&fm) { |
| 1880 | | - return Some(fm); |
| 1881 | | - } |
| 1882 | | - } |
| 1883 | | - |
| 1884 | | - // Try common file managers (excluding garfield to avoid recursion) |
| 1885 | | - let managers = ["nautilus", "dolphin", "thunar", "pcmanfm", "nemo", "caja"]; |
| 1886 | | - for manager in managers { |
| 1887 | | - if self.command_exists(manager) { |
| 1888 | | - return Some(manager.to_string()); |
| 1889 | | - } |
| 1890 | | - } |
| 1891 | | - |
| 1892 | | - // Fallback to xdg-open |
| 1893 | | - Some("xdg-open".to_string()) |
| 1894 | | - } |
| 1895 | | - |
| 1896 | 1939 | /// Check if a command exists in PATH. |
| 1897 | 1940 | fn command_exists(&self, cmd: &str) -> bool { |
| 1898 | 1941 | // Extract just the command name (in case it's a full path or has args) |
@@ -2437,6 +2480,7 @@ impl App { |
| 2437 | 2480 | self.conflict_dialog.set_bounds(Rect::new(0, 0, width, height)); |
| 2438 | 2481 | self.progress_dialog.set_bounds(Rect::new(0, 0, width, height)); |
| 2439 | 2482 | self.context_menu.set_bounds(Rect::new(0, 0, width, height)); |
| 2483 | + self.input_dialog.set_bounds(Rect::new(0, 0, width, height)); |
| 2440 | 2484 | } |
| 2441 | 2485 | |
| 2442 | 2486 | /// Render the application. |
@@ -2506,6 +2550,9 @@ impl App { |
| 2506 | 2550 | // Draw conflict dialog overlay (on top of everything) |
| 2507 | 2551 | self.conflict_dialog.render(&self.renderer)?; |
| 2508 | 2552 | |
| 2553 | + // Draw input dialog overlay (on top of everything) |
| 2554 | + self.input_dialog.render(&self.renderer)?; |
| 2555 | + |
| 2509 | 2556 | // Draw context menu overlay |
| 2510 | 2557 | self.context_menu.render(&self.renderer)?; |
| 2511 | 2558 | |