turbo-vision 1.0.6

A Rust implementation of the classic Borland Turbo Vision text-mode UI framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
// (C) 2025 - Enzo Lombardi
//! File Dialog - A file selection dialog for opening files
//!
//! ## Current Implementation
//!
//! The FileDialog provides a classic file selection interface with:
//! - Input line for typing filenames
//! - ListBox showing files and directories in the current path
//! - **Mouse support**: Click on files to select, double-click to open
//! - **Keyboard navigation**: Arrow keys, PgUp/PgDn, Home/End
//! - Directory navigation (double-click directories or select and press Enter)
//! - Wildcard filtering (e.g., "*.rs" shows only Rust files)
//! - Parent directory navigation via ".."
//!
//! ## Usage
//!
//! Users can select files in multiple ways:
//! 1. **Click a file** once to select it (updates the input field)
//! 2. **Double-click a file** to select and open it
//! 3. **Use arrow keys** to navigate the list, press Enter to select
//! 4. **Type a filename** directly in the input box
//!
//! Directory navigation:
//! - Press Enter on a folder (`[dirname]`) to navigate into it
//! - Press Enter on `..` to go to parent directory
//! - Click on folders to navigate (single click selects, double-click or Enter opens)
//! - The dialog stays open while navigating directories
//!
//! Wildcard patterns:
//! - `"*"` - Shows all files
//! - `"*.rs"` - Shows only files ending with `.rs`
//! - `"*.toml"` - Shows only files ending with `.toml`
//! - `"test"` - Shows files containing "test" in their name
//!
//! **Note**: Directories are always shown regardless of the wildcard pattern.
//!
//! ## Implementation Notes
//!
//! The FileDialog tracks ListBox selection state by intercepting keyboard and mouse
//! events before passing them to the dialog. This allows it to:
//! - Maintain a shadow selection index
//! - Update the InputLine when files are selected
//! - Handle directory navigation seamlessly
//!
//! ### Architecture
//!
//! The Dialog/Window/Group hierarchy now provides `child_at_mut()` methods for accessing
//! child views. This architectural improvement allows components to:
//! - Query child view state after adding them to containers
//! - Modify child views dynamically
//! - Create more sophisticated interactions between parent and child views
//!
//! The current FileDialog implementation uses event interception for simplicity and
//! performance, but could alternatively use direct child access if needed for more
//! complex scenarios.

/// FileDialog - A file selection dialog for opening/saving files
///
/// ## Usage
///
/// ```rust,ignore
/// // Open dialog (default)
/// let mut dialog = FileDialog::new(bounds, "Open File", "*.rs", None).build();
///
/// // Save dialog with custom button label
/// let mut dialog = FileDialog::new(bounds, "Save File", "*", None)
///     .with_button_label("~S~ave")
///     .build();
///
/// // Execute and get selected file path
/// match dialog.execute(&mut app) {
///     Some(path) => println!("Selected: {}", path.display()),
///     None => println!("Canceled"),
/// }
/// ```
///
/// ## Dialog Close Behavior
///
/// The FileDialog closes (returns a file path) for exactly 3 reasons:
/// 1. **File is double-clicked** in the ListBox → Dialog closes, returns file path
/// 2. **File is selected and Enter/OK is pressed** → Dialog closes, returns file path
/// 3. **User cancels** (close button, Cancel button, or double ESC) → Dialog closes, returns None
///
/// ## Folder Navigation
///
/// When a folder is selected and opened (double-click or Enter while focused on folder):
/// - The dialog stays OPEN
/// - Current directory is updated
/// - File list is refreshed with new directory contents
/// - Selection moves to first item in new directory
///
/// This applies to: "..", regular folders "[dirname]", and folder paths like "subdir/*.rs"
///
/// ## Button Labels
///
/// Customize the button label using `with_button_label()`:
/// - Default: `"~O~pen"` (Open button)
/// - For save: `"~S~ave"`
/// - For export: `"~E~xport"`
/// - Any other text: `"~C~ustom"`
///
/// The `~` character indicates the hotkey underline in the button text.

use super::View;
use super::button::Button;
use super::dialog::Dialog;
use super::input_line::InputLine;
use super::label::Label;
use super::listbox::ListBox;
use crate::core::command::{CM_CANCEL, CM_FILE_FOCUSED, CM_OK, CommandId};
use crate::core::event::{Event, EventType};
use crate::core::geometry::Rect;
use crate::terminal::Terminal;
use std::cell::RefCell;
use std::fs;
use std::path::PathBuf;
use std::rc::Rc;

const CMD_FILE_SELECTED: u16 = 1000;

// Child indices in the dialog
const CHILD_LISTBOX: usize = 4; // ListBox
const CHILD_OK_BUTTON: usize = 5; // Open button

pub struct FileDialog {
    dialog: Dialog,
    current_path: PathBuf,
    wildcard: String,
    file_name_data: Rc<RefCell<String>>,
    files: Vec<String>,
    selected_file_index: usize, // Track ListBox selection
    title: String, // Store title for rebuilds
    button_label: String, // "Open", "Save", etc.
}

impl FileDialog {
    pub fn new(bounds: Rect, title: &str, wildcard: &str, initial_dir: Option<PathBuf>) -> Self {
        let dialog = Dialog::new(bounds, title);

        let current_path = initial_dir
            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));

        let file_name_data = Rc::new(RefCell::new(String::new()));

        Self {
            dialog,
            current_path,
            wildcard: wildcard.to_string(),
            file_name_data,
            files: Vec::new(),
            selected_file_index: 0,
            title: title.to_string(),
            button_label: "~O~pen".to_string(), // Default to "Open"
        }
    }

    /// Set the button label (e.g., "~S~ave" for save dialogs)
    pub fn with_button_label(mut self, label: &str) -> Self {
        self.button_label = label.to_string();
        self
    }

    pub fn build(mut self) -> Self {
        let bounds = self.dialog.bounds();
        let dialog_width = bounds.width();

        // Reserve space for buttons on the right
        let content_width = dialog_width - 15;

        // Label for file name input
        let name_label = Label::new(Rect::new(2, 1, 12, 1), "~N~ame:");
        self.dialog.add(Box::new(name_label));

        // File name input line
        let file_input = InputLine::new(
            Rect::new(12, 1, content_width, 2),
            255,
            self.file_name_data.clone(),
        );
        self.dialog.add(Box::new(file_input));

        // Current path label
        let path_str = format!(" {}", self.current_path.display());
        let path_label = Label::new(Rect::new(2, 3, content_width, 3), &path_str);
        self.dialog.add(Box::new(path_label));

        // Label for files list
        let files_label = Label::new(Rect::new(2, 5, 12, 5), "~F~iles:");
        self.dialog.add(Box::new(files_label));

        // File list box - leave space on right for buttons
        let mut file_list = ListBox::new(
            Rect::new(2, 6, content_width, bounds.height() - 2),
            CMD_FILE_SELECTED,
        );

        // Load directory contents first
        self.read_directory();

        // Populate the list box with files
        file_list.set_items(self.files.clone());
        self.dialog.add(Box::new(file_list));

        // Buttons on the right side (vertically stacked)
        let button_x = dialog_width - 14; // 15 = 12 (button width) + 2 (right margin) + 1 (end space)
        let mut button_y = 6;

        // Create button with appropriate label (Open, Save, etc.)
        // Pad the button label to maintain consistent button width
        let button_text = format!("  {}  ", self.button_label);
        let open_button = Button::new(
            Rect::new(button_x, button_y, button_x + 11, button_y + 2),
            &button_text,
            CM_OK,
            true,
        );
        self.dialog.add(Box::new(open_button));
        button_y += 3;

        let cancel_button = Button::new(
            Rect::new(button_x, button_y, button_x + 11, button_y + 2),
            " ~C~ancel ",
            CM_CANCEL,
            false,
        );
        self.dialog.add(Box::new(cancel_button));

        // Set focus to the listbox by default (better UX for file selection)
        self.dialog.set_initial_focus();
        // TODO: Need a way to set focus to a specific child index
        // For now, initial focus goes to first focusable (input), user can Tab to listbox

        self
    }

    pub fn execute(&mut self, app: &mut crate::app::Application) -> Option<PathBuf> {
        use crate::core::state::SF_MODAL;

        // Set modal flag - file dialogs are modal
        // Matches Borland: TFileDialog in modal state
        let old_state = self.dialog.state();
        self.dialog.set_state(old_state | SF_MODAL);

        loop {
            // Update OK button state based on input field
            self.update_ok_button_state();

            // Draw desktop first (background), then dialog on top
            // This matches Borland's pattern where getEvent() triggers full screen redraw
            app.desktop.draw(&mut app.terminal);

            // Draw menu bar and status line if present (so they appear on top)
            if let Some(ref mut menu_bar) = app.menu_bar {
                menu_bar.draw(&mut app.terminal);
            }
            if let Some(ref mut status_line) = app.status_line {
                status_line.draw(&mut app.terminal);
            }

            // Draw the file dialog on top of desktop/menu/status
            self.dialog.draw(&mut app.terminal);

            // Draw overlay widgets on top of everything (animations, etc.)
            // These continue to animate even during modal dialogs
            // Matches Borland: TProgram::idle() continues running during execView()
            for widget in &mut app.overlay_widgets {
                widget.draw(&mut app.terminal);
            }

            self.dialog.update_cursor(&mut app.terminal);
            let _ = app.terminal.flush();

            // Get event with 20ms timeout (matches magiblot's eventTimeoutMs)
            match app
                .terminal
                .poll_event(std::time::Duration::from_millis(20))
                .ok()
                .flatten()
            {
                Some(mut event) => {
                    // Event received - handle it immediately without calling idle()
                    // Matches magiblot: idle() is NOT called when events are present

                    // Handle double ESC to close (Cancel operation)
                    if event.what == EventType::Keyboard
                        && event.key_code == crate::core::event::KB_ESC_ESC
                    {
                        return None;
                    }

                    // Let the dialog (and its children) handle the event first
                    self.dialog.handle_event(&mut event);

                    // Check if dialog wants to close (e.g., close button clicked)
                    // Dialog::handle_event() calls end_modal() which sets the end_state
                    // Matches Borland: TDialog::execute() checks endState after handleEvent
                    let end_state = self.dialog.get_end_state();

                    // IMPORTANT: Only close dialog for CM_CANCEL and CM_CLOSE
                    // For CM_OK, we need to check if it's a wildcard pattern FIRST
                    // If it's a wildcard, we stay open and update the filter
                    // Only close if it's actually a file selection (after wildcard check)
                    if end_state == crate::core::command::CM_CANCEL
                        || end_state == crate::core::command::CM_CLOSE {
                        // CLOSE CONDITION 3: User cancels via cancel button or close button
                        return None;
                    }

                    // After event is processed, check if ListBox selection changed
                    // Matches Borland: TFileList::focusItem() broadcasts cmFileFocused when selection changes
                    // We read the ListBox selection after it has processed navigation events
                    self.sync_inputline_with_listbox();

                    // Check if dialog should close based on command
                    if event.what == EventType::Command {
                        match event.command {
                            CM_OK => {
                                // User clicked OK button or pressed Enter (while not in listbox)
                                // Matches Borland: TFileDialog::valid(cmFileOpen) (tfiledia.cc:251-302)
                                let file_name = self.file_name_data.borrow().clone();
                                if !file_name.is_empty() {
                                    // Check if input contains wildcards (*.txt, *.rs, etc)
                                    if self.contains_wildcards(&file_name) {
                                        // Borland pattern: Update wildcard and reload list, keep dialog open
                                        // Matches: strcpy(wildCard, name); fileList->readDirectory()
                                        self.wildcard = file_name.clone();
                                        self.read_directory();

                                        // Update ListBox items directly (don't rebuild entire dialog)
                                        // Downcast to ListBox to call set_items()
                                        if CHILD_LISTBOX < self.dialog.child_count() {
                                            let view = self.dialog.child_at_mut(CHILD_LISTBOX);
                                            if let Some(listbox) =
                                                view.as_any_mut().downcast_mut::<ListBox>()
                                            {
                                                listbox.set_items(self.files.clone());
                                                listbox.set_list_selection(0);
                                            }
                                        }

                                        // Update input field to show the wildcard pattern
                                        *self.file_name_data.borrow_mut() = self.wildcard.clone();

                                        // Reset selection tracking
                                        self.selected_file_index = 0;

                                        // CRITICAL: Clear the end_state that was set by Dialog.handle_event()
                                        // Dialog called end_modal(CM_OK) but we're staying open for wildcard filter
                                        self.dialog.set_end_state(0);

                                        // Force full redraw to ensure ListBox visual updates
                                        // The Terminal's double-buffering system needs this to guarantee
                                        // that all changed cells are resent to the actual terminal
                                        app.terminal.force_full_redraw();

                                        // Stay open - continue event loop
                                        continue;
                                    }

                                    // Check if it's a directory navigation request or file selection
                                    if let Some(path) =
                                        self.handle_selection(&file_name, &mut app.terminal)
                                    {
                                        // CLOSE CONDITION 2: File selected and OK pressed
                                        return Some(path);
                                    }
                                    // Directory/folder selected - navigate into it (stay open)
                                    // CRITICAL: Clear the end_state so the loop continues
                                    // Dialog called end_modal(CM_OK) but we're navigating into folder
                                    self.dialog.set_end_state(0);
                                    // Loop continues with new directory contents
                                } else {
                                    // If input is empty, do nothing (don't close dialog)
                                    // This effectively disables the OK button when input is empty
                                    // Clear end_state so dialog stays open
                                    self.dialog.set_end_state(0);
                                }
                            }
                            CM_CANCEL | crate::core::command::CM_CLOSE => {
                                // CLOSE CONDITION 3: User cancels via Cancel button
                                return None;
                            }
                            CMD_FILE_SELECTED => {
                                // User double-clicked or pressed Enter on an item in the listbox
                                // The input field has ALREADY been updated by sync_inputline_with_listbox()
                                // So we just read what's already there and handle it
                                let file_name = self.file_name_data.borrow().clone();

                                if !file_name.is_empty() {
                                    // Handle the selection (navigate into folder or return file)
                                    if let Some(path) =
                                        self.handle_selection(&file_name, &mut app.terminal)
                                    {
                                        // CLOSE CONDITION 1: File double-clicked or Enter pressed on file
                                        return Some(path);
                                    }
                                    // Folder/directory selected - navigate into it (stay open)
                                    // Loop continues with new directory contents
                                }
                            }
                            _ => {}
                        }
                    }
                }
                None => {
                    // Timeout with no events - call idle() to update animations, etc.
                    // Matches magiblot: idle() only called when truly idle
                    app.idle();
                }
            }
        }
    }

    /// Sync the InputLine with the current ListBox selection
    /// Matches Borland: TFileList::focusItem() broadcasts cmFileFocused when selection changes
    /// We read the ListBox selection after it has processed events
    fn sync_inputline_with_listbox(&mut self) {
        // Get the current selection from the ListBox
        if CHILD_LISTBOX >= self.dialog.child_count() {
            return;
        }

        let listbox = self.dialog.child_at(CHILD_LISTBOX);
        let new_selection = listbox.get_list_selection();

        // Only update if selection actually changed
        if new_selection != self.selected_file_index {
            self.selected_file_index = new_selection;

            // Get the selected item text
            if self.selected_file_index < self.files.len() {
                let selected = self.files[self.selected_file_index].clone();

                // Format the input field text based on selection type
                // Matches Borland: TFileInputLine::handleEvent() (tfileinp.cc:35-45)
                let display_text = if selected.starts_with('[') && selected.ends_with(']') {
                    // Directory selected - show "dirname/*.txt" format
                    let dir_name = &selected[1..selected.len() - 1];
                    format!("{}/{}", dir_name, self.wildcard)
                } else if selected == ".." {
                    // Parent directory - just show ".."
                    selected.clone()
                } else {
                    // Regular file - show just the filename
                    selected.clone()
                };

                // Update the shared data field directly (Borland pattern)
                // InputLine will observe this change via its broadcast handler
                *self.file_name_data.borrow_mut() = display_text;

                // Broadcast to notify InputLine to update its display
                // Matches Borland: message(owner, evBroadcast, cmFileFocused, this)
                // InputLine will only update display if NOT focused (prevents interrupting typing)
                let mut broadcast = Event::broadcast(CM_FILE_FOCUSED);
                self.dialog.handle_event(&mut broadcast);
            }
        }
    }

    fn handle_selection(&mut self, file_name: &str, terminal: &mut Terminal) -> Option<PathBuf> {
        // Determines whether a selection is:
        // - A folder to navigate into (returns None, dialog stays open)
        // - A file to return (returns Some(path), closes dialog)
        //
        // Returns None when a folder is selected → dialog stays open and refreshes
        // Returns Some(path) when a file is selected → closes dialog with file path

        // Matches Borland: TFileDialog::valid() parsing (tfiledia.cc:98-124)

        // Check if input contains directory/wildcard format (e.g., "dirname/*.txt")
        if let Some(slash_pos) = file_name.rfind('/') {
            let dir_part = &file_name[..slash_pos];
            let file_part = &file_name[slash_pos + 1..];

            // Navigate to the directory
            if !dir_part.is_empty() {
                self.current_path.push(dir_part);
            }

            // Update wildcard if file_part contains wildcards
            if self.contains_wildcards(file_part) {
                self.wildcard = file_part.to_string();
            }

            // rebuild_and_redraw will refresh the dialog with new directory contents
            self.rebuild_and_redraw(terminal);
            return None; // Stay open after navigating
        }

        if file_name == ".." {
            // Parent directory selected - navigate up one level
            if let Some(parent) = self.current_path.parent() {
                self.current_path = parent.to_path_buf();
                self.rebuild_and_redraw(terminal);
            }
            None // Stay open after navigating
        } else if file_name.starts_with('[') && file_name.ends_with(']') {
            // Folder selected ([dirname]) - navigate into it
            let dir_name = &file_name[1..file_name.len() - 1];
            self.current_path.push(dir_name);
            self.rebuild_and_redraw(terminal);
            None // Stay open after navigating
        } else {
            // Regular file selected - close dialog with path
            *self.file_name_data.borrow_mut() = file_name.to_string();
            Some(self.current_path.join(file_name)) // Close dialog, return file path
        }
    }

    fn update_ok_button_state(&mut self) {
        use crate::core::state::SF_DISABLED;

        let file_name = self.file_name_data.borrow().clone();

        // OK button should only be enabled when a regular file is selected
        // Disable for: empty input, directories ([dirname]), parent (..), or dir/wildcard paths (dirname/*.rs)
        let should_disable = file_name.is_empty()
            || file_name == ".."
            || file_name.starts_with('[') && file_name.ends_with(']')
            || file_name.contains('/');

        // Get the OK button and update its disabled state
        // Matches Borland's TView::setState(sfDisabled, enable) pattern
        if CHILD_OK_BUTTON < self.dialog.child_count() {
            let ok_button = self.dialog.child_at_mut(CHILD_OK_BUTTON);
            ok_button.set_state_flag(SF_DISABLED, should_disable);
        }
    }

    fn rebuild_and_redraw(&mut self, _terminal: &mut Terminal) {
        // Create a new dialog with updated file list
        let old_bounds = self.dialog.bounds();
        let old_title = self.title.clone();
        let old_button_label = self.button_label.clone();

        *self = Self::new(
            old_bounds,
            &old_title,
            &self.wildcard.clone(),
            Some(self.current_path.clone()),
        )
        .with_button_label(&old_button_label)
        .build();

        // Reset focus to listbox after directory navigation
        // Matches Borland: fileList->select() calls owner->setCurrent(this, normalSelect)
        // (tfiledia.cc:275,287 and tview.cc:658-664)
        // This properly updates both the Group's focused index AND the child's focus state
        if CHILD_LISTBOX < self.dialog.child_count() {
            self.dialog.set_focus_to_child(CHILD_LISTBOX);
            // Also ensure listbox selection is at index 0
            self.dialog
                .child_at_mut(CHILD_LISTBOX)
                .set_list_selection(0);
        }

        // Reset selection index
        self.selected_file_index = 0;

        // CRITICAL: Broadcast initial selection after directory navigation
        // Matches Borland: TFileList::readDirectory() broadcasts cmFileFocused after newList()
        // (tfilelis.cc:588-595) and TFileList::setState() broadcasts on focus (tfilelis.cc:146-149)
        if !self.files.is_empty() {
            let first_item = self.files[0].clone();

            // Format the display text for the input field
            // IMPORTANT: After applying a wildcard filter, keep the wildcard pattern
            // in the input field so the user can see what filter is active.
            // This matches user expectations: when they press OK with "*.txt",
            // the dialog applies the filter and shows "*.txt" in the input field.
            let display_text = if first_item.starts_with('[') && first_item.ends_with(']') {
                // Directory selected - show "dirname/*.txt" format
                let dir_name = &first_item[1..first_item.len() - 1];
                format!("{}/{}", dir_name, self.wildcard)
            } else if first_item == ".." {
                // Parent directory - just show ".."
                first_item.clone()
            } else {
                // Regular file - show the wildcard pattern if one was explicitly set
                // (user typed a wildcard and pressed OK to apply the filter)
                // Otherwise show the filename
                if self.wildcard.contains('*') || self.wildcard.contains('?') {
                    // Wildcard is active - show it in the input field
                    self.wildcard.clone()
                } else {
                    // Regular file selection mode - show the filename
                    first_item.clone()
                }
            };

            // Update the shared data field
            *self.file_name_data.borrow_mut() = display_text;

            // Broadcast to notify InputLine to update its display
            let mut broadcast = Event::broadcast(CM_FILE_FOCUSED);
            self.dialog.handle_event(&mut broadcast);
        } else {
            // No files - show the wildcard pattern if one was applied
            if self.wildcard.contains('*') || self.wildcard.contains('?') {
                *self.file_name_data.borrow_mut() = self.wildcard.clone();
            } else {
                *self.file_name_data.borrow_mut() = String::new();
            }
        }
    }

    fn read_directory(&mut self) {
        self.files.clear();

        // Add parent directory entry
        if self.current_path.parent().is_some() {
            self.files.push("..".to_string());
        }

        // Read directory contents
        if let Ok(entries) = fs::read_dir(&self.current_path) {
            let mut dirs = Vec::new();
            let mut regular_files = Vec::new();

            for entry in entries.flatten() {
                if let Ok(metadata) = entry.metadata() {
                    let name = entry.file_name().to_string_lossy().to_string();

                    if metadata.is_dir() {
                        dirs.push(format!("[{}]", name));
                    } else if self.matches_wildcard(&name) {
                        regular_files.push(name);
                    }
                }
            }

            // Sort and combine: directories first, then files
            dirs.sort();
            regular_files.sort();
            self.files.extend(dirs);
            self.files.extend(regular_files);
        }
    }

    fn contains_wildcards(&self, name: &str) -> bool {
        // Check if the name contains wildcard characters
        // Matches Borland: IsWild() checks for '*' and '?' (tfiledia.cc:42-47)
        name.contains('*') || name.contains('?')
    }

    fn matches_wildcard(&self, name: &str) -> bool {
        // "*.*" means all files (common DOS/Windows pattern)
        if self.wildcard == "*.*" || self.wildcard == "*" || self.wildcard.is_empty() {
            return true;
        }

        // Simple wildcard matching (*.ext)
        if let Some(ext) = self.wildcard.strip_prefix("*.") {
            // Check for the special case "*.* " which was already handled above
            // For "*.rs", check if name ends with ".rs"
            name.ends_with(&format!(".{}", ext))
        } else {
            // Contains matching (no wildcard, just substring)
            name.contains(&self.wildcard)
        }
    }

    pub fn get_selected_file(&self) -> Option<PathBuf> {
        let file_name = self.file_name_data.borrow().clone();
        if !file_name.is_empty() {
            Some(self.current_path.join(file_name))
        } else {
            None
        }
    }

    /// Get the current directory being browsed
    /// Useful for ChDirDialog to get the selected directory
    pub fn get_current_directory(&self) -> PathBuf {
        self.current_path.clone()
    }

    /// Get the end state (command that closed the dialog)
    pub fn get_end_state(&self) -> CommandId {
        self.dialog.get_end_state()
    }
}

impl View for FileDialog {
    fn bounds(&self) -> Rect {
        self.dialog.bounds()
    }

    fn set_bounds(&mut self, bounds: Rect) {
        self.dialog.set_bounds(bounds);
    }

    fn draw(&mut self, terminal: &mut Terminal) {
        self.dialog.draw(terminal);
    }

    fn handle_event(&mut self, event: &mut Event) {
        self.dialog.handle_event(event);
    }

    fn get_palette(&self) -> Option<crate::core::palette::Palette> {
        self.dialog.get_palette()
    }
}

/// Builder for creating file dialogs with a fluent API.
///
/// # Examples
///
/// ```ignore
/// use turbo_vision::views::file_dialog::FileDialogBuilder;
/// use turbo_vision::core::geometry::Rect;
/// use std::path::PathBuf;
///
/// // Create a basic file dialog
/// let dialog = FileDialogBuilder::new()
///     .bounds(Rect::new(10, 5, 70, 20))
///     .title("Open File")
///     .wildcard("*.rs")
///     .build();
///
/// // Create a file dialog with initial directory
/// let dialog = FileDialogBuilder::new()
///     .bounds(Rect::new(10, 5, 70, 20))
///     .title("Select File")
///     .wildcard("*")
///     .initial_dir(PathBuf::from("/home/user/documents"))
///     .build();
/// ```
pub struct FileDialogBuilder {
    bounds: Option<Rect>,
    title: Option<String>,
    wildcard: String,
    initial_dir: Option<PathBuf>,
    button_label: String,
}

impl FileDialogBuilder {
    /// Creates a new FileDialogBuilder with default values.
    pub fn new() -> Self {
        Self {
            bounds: None,
            title: None,
            wildcard: "*".to_string(),
            initial_dir: None,
            button_label: "~O~pen".to_string(),
        }
    }

    /// Sets the file dialog bounds (required).
    #[must_use]
    pub fn bounds(mut self, bounds: Rect) -> Self {
        self.bounds = Some(bounds);
        self
    }

    /// Sets the dialog title (required).
    #[must_use]
    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.title = Some(title.into());
        self
    }

    /// Sets the wildcard filter (default: "*").
    /// Examples: "*.rs", "*.toml", "*"
    #[must_use]
    pub fn wildcard(mut self, wildcard: impl Into<String>) -> Self {
        self.wildcard = wildcard.into();
        self
    }

    /// Sets the initial directory (optional).
    /// If not set, uses the current working directory.
    #[must_use]
    pub fn initial_dir(mut self, dir: PathBuf) -> Self {
        self.initial_dir = Some(dir);
        self
    }

    /// Sets the button label (optional, default: "~O~pen").
    /// Examples: "~S~ave", "~E~xport", "~C~hoose"
    /// The ~ character indicates the hotkey underline.
    #[must_use]
    pub fn button_label(mut self, label: impl Into<String>) -> Self {
        self.button_label = label.into();
        self
    }

    /// Builds the FileDialog.
    ///
    /// # Panics
    ///
    /// Panics if required fields (bounds, title) are not set.
    pub fn build(self) -> FileDialog {
        let bounds = self.bounds.expect("FileDialog bounds must be set");
        let title = self.title.expect("FileDialog title must be set");
        FileDialog::new(bounds, &title, &self.wildcard, self.initial_dir)
            .with_button_label(&self.button_label)
            .build()
    }

    /// Builds the FileDialog as a Box.
    pub fn build_boxed(self) -> Box<FileDialog> {
        Box::new(self.build())
    }
}

impl Default for FileDialogBuilder {
    fn default() -> Self {
        Self::new()
    }
}