turbo-vision 1.1.0

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
// (C) 2025 - Enzo Lombardi

//! Change Directory Dialog - specialized dialog for directory selection
//!
//! Matches Borland: TChDirDialog (tchdirdi.cc)
//!
//! A dialog for navigating and selecting directories with a tree view,
//! input line, and control buttons.
//!
//! Layout (widened from Borland for more space):
//! - Dialog bounds: 5, 2, 75, 21 (70 wide x 19 tall)
//! - Directory name input at top
//! - Directory tree listbox in middle
//! - Buttons (OK, Chdir, Revert) on right side

use crate::app::Application;
use crate::core::command::{CommandId, CM_OK};
use crate::core::event::{Event, EventType};
use crate::core::geometry::{Point, Rect};
use crate::core::history::HistoryManager;
use crate::terminal::Terminal;
use super::dialog::Dialog;
use super::input_line::InputLine;
use super::label::Label;
use super::button::Button;
use super::dir_listbox::DirListBox;
use super::scrollbar::ScrollBar;
use super::history::History;
use super::msgbox::message_box_error;
use super::{View, ViewId};
use super::list_viewer::ListViewer;
use std::path::PathBuf;
use std::cell::RefCell;
use std::rc::Rc;

// Custom commands for ChDirDialog
const CM_CHANGE_DIR: CommandId = 200;
const CM_REVERT: CommandId = 201;

// History ID for directory paths
// Matches Borland: histId parameter in TChDirDialog constructor
const DEFAULT_HISTORY_ID: u16 = 10;

/// Wrapper that allows ScrollBar to be a child view
struct SharedScrollBar(Rc<RefCell<ScrollBar>>);

impl View for SharedScrollBar {
    fn bounds(&self) -> Rect {
        self.0.borrow().bounds()
    }

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

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

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

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

}

/// Wrapper that allows DirListBox to be a child view with shared access
/// Also broadcasts CM_FILE_FOCUSED when focused item changes (like FileList does)
/// Manages scrollbars connected to the listbox
struct SharedDirListBox {
    inner: Rc<RefCell<DirListBox>>,
    dir_input_data: Rc<RefCell<String>>,
    last_focused_path: Option<PathBuf>,
    v_scrollbar: Rc<RefCell<ScrollBar>>,
    h_scrollbar: Rc<RefCell<ScrollBar>>,
}

impl SharedDirListBox {
    fn new(
        inner: Rc<RefCell<DirListBox>>,
        dir_input_data: Rc<RefCell<String>>,
        v_scrollbar: Rc<RefCell<ScrollBar>>,
        h_scrollbar: Rc<RefCell<ScrollBar>>,
    ) -> Self {
        // Initialize with current focused entry
        let last_focused_path = inner.borrow().get_focused_entry().map(|e| e.path.clone());
        Self {
            inner,
            dir_input_data,
            last_focused_path,
            v_scrollbar,
            h_scrollbar,
        }
    }

    /// Update scrollbar positions based on listbox state
    fn update_scrollbars(&mut self) {
        // Get values from list state (can't hold reference due to borrow checker)
        let total_items;
        let top_item;
        let visible_items;
        {
            let listbox = self.inner.borrow();
            let list_state = listbox.list_state();
            total_items = list_state.range;
            top_item = list_state.top_item;
            visible_items = listbox.bounds().height_clamped() as usize;
        }

        // Update vertical scrollbar
        // Matches Borland: TScrollBar::setParams(value, min, max, pgSize, arStep)
        self.v_scrollbar.borrow_mut().set_params(
            top_item as i32,
            0,
            total_items.saturating_sub(visible_items) as i32,
            visible_items as i32,
            1,
        );

        // Horizontal scrollbar is typically not needed for directory names
        // but we'll set it to 0 for consistency
        self.h_scrollbar.borrow_mut().set_params(0, 0, 0, 1, 1);
    }
}

impl View for SharedDirListBox {
    fn bounds(&self) -> Rect {
        self.inner.borrow().bounds()
    }

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

    fn draw(&mut self, terminal: &mut Terminal) {
        // Update scrollbars before drawing
        self.update_scrollbars();

        self.inner.borrow_mut().draw(terminal);
    }

    fn handle_event(&mut self, event: &mut Event) {
        // Track the old scroll position
        let old_top_item = self.inner.borrow().list_state().top_item;

        // Handle scrollbar events first (mouse clicks on scrollbars)
        // Matches Borland: TScroller::handleEvent() processes scrollbar events
        if event.what == EventType::MouseDown || event.what == EventType::MouseMove {
            let v_bounds = self.v_scrollbar.borrow().bounds();
            let h_bounds = self.h_scrollbar.borrow().bounds();

            if v_bounds.contains(event.mouse.pos) {
                // Let vertical scrollbar handle the event
                self.v_scrollbar.borrow_mut().handle_event(event);

                // Get the new scroll value and update listbox
                let new_top = self.v_scrollbar.borrow().get_value() as usize;
                {
                    let mut listbox = self.inner.borrow_mut();
                    listbox.list_state_mut().top_item = new_top;
                }

                // Update scrollbars to reflect new position
                self.update_scrollbars();
                return;
            } else if h_bounds.contains(event.mouse.pos) {
                // Let horizontal scrollbar handle the event
                self.h_scrollbar.borrow_mut().handle_event(event);
                // Horizontal scrolling not used for directory names
                return;
            }
        }

        // Track focused entry before event
        let path_before = self.inner.borrow().get_focused_entry().map(|e| e.path.clone());

        // Let DirListBox handle the event
        self.inner.borrow_mut().handle_event(event);

        // Check if scroll position changed (from keyboard navigation)
        let new_top_item = self.inner.borrow().list_state().top_item;
        if old_top_item != new_top_item {
            // Scroll position changed - update scrollbars
            self.update_scrollbars();
        }

        // Check if focused entry changed
        let path_after = self.inner.borrow().get_focused_entry().map(|e| e.path.clone());

        if path_before != path_after {
            // Focused entry changed - update input data
            // Matches Borland: message(owner, evBroadcast, cmFileFocused, this)
            if let Some(ref new_path) = path_after {
                *self.dir_input_data.borrow_mut() = new_path.to_string_lossy().to_string();
            }

            self.last_focused_path = path_after;
        }

        // Handle broadcast commands from Chdir and Revert buttons
        if event.what == EventType::Broadcast {
            match event.command {
                CM_CHANGE_DIR => {
                    // Navigate to the selected directory in listbox
                    // Matches Borland: gets focused item from dirList, updates current dir
                    // Extract path first to avoid overlapping borrows
                    let new_path = self.inner.borrow().get_focused_entry().map(|e| e.path.clone());

                    if let Some(new_path) = new_path {
                        // Update listbox to show the new directory
                        if self.inner.borrow_mut().change_dir(&new_path).is_ok() {
                            // Update input line with the new path
                            *self.dir_input_data.borrow_mut() = new_path.to_string_lossy().to_string();
                            // Update scrollbars after directory change
                            self.update_scrollbars();
                        }
                    }
                    event.clear();
                }
                CM_REVERT => {
                    // Revert to current working directory
                    // Matches Borland: resets dialog to show current directory
                    if let Ok(current_dir) = std::env::current_dir() {
                        *self.dir_input_data.borrow_mut() = current_dir.to_string_lossy().to_string();
                        // Update dir listbox to show current directory
                        let _ = self.inner.borrow_mut().change_dir(&current_dir);
                        // Update scrollbars after directory change
                        self.update_scrollbars();
                    }
                    event.clear();
                }
                _ => {}
            }
        }
    }

    fn can_focus(&self) -> bool {
        self.inner.borrow().can_focus()
    }

    fn state(&self) -> crate::core::state::StateFlags {
        self.inner.borrow().state()
    }

    fn set_state(&mut self, state: crate::core::state::StateFlags) {
        self.inner.borrow_mut().set_state(state);
    }

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

}

/// Change Directory Dialog
///
/// Matches Borland: TChDirDialog (tchdirdi.cc)
///
/// Dialog for selecting and changing to a directory. Shows a hierarchical
/// directory tree and allows navigation through directories.
pub struct ChDirDialog {
    dialog: Dialog,
    dir_input_data: Rc<RefCell<String>>,
    history_id: u16,
    #[allow(dead_code)] // Will be used for navigation implementation
    dir_list_id: ViewId,
    #[allow(dead_code)] // Will be used for input updates
    dir_input_id: ViewId,
    #[allow(dead_code)] // Will be used for button state management
    ok_button_id: ViewId,
    #[allow(dead_code)] // Will be used for button state management
    chdir_button_id: ViewId,
    selected_directory: Option<PathBuf>,
}

impl ChDirDialog {
    /// Create a new change directory dialog
    ///
    /// # Arguments
    /// * `history_id` - Optional history ID for storing directory history (defaults to 10)
    ///
    /// Matches Borland constructor:
    /// `TChDirDialog::TChDirDialog( ushort opts, ushort histId )`
    ///
    /// Dialog layout (widened from Borland for more space):
    /// - Dialog: TRect( 5, 2, 75, 21 ) = 70 wide x 19 tall (widened for more space)
    /// - Input line: TRect( 3, 3, 48, 4 )
    /// - Label "Directory name": (2, 2)
    /// - History button: TRect( 48, 3, 51, 4 )
    /// - Vertical scrollbar: TRect( 50, 6, 51, 16 )
    /// - Horizontal scrollbar: TRect( 3, 16, 50, 17 )
    /// - Dir listbox: TRect( 3, 6, 50, 16 )
    /// - Label "Directory tree": (2, 5)
    /// - OK button: TRect( 53, 6, 63, 8 )
    /// - Chdir button: TRect( 53, 9, 63, 11 )
    /// - Revert button: TRect( 53, 12, 63, 14 )
    pub fn new(history_id: Option<u16>) -> Self {
        let history_id = history_id.unwrap_or(DEFAULT_HISTORY_ID);
        // Widened dialog bounds: TRect( 5, 2, 75, 21 ) - 70 columns instead of 48
        // This is absolute screen coordinates, will be centered by ofCentered flag
        let dialog_bounds = Rect::new(5, 2, 75, 21);
        let mut dialog = Dialog::new(dialog_bounds, "Change Directory");

        // Get current directory for initial value
        let current_dir = std::env::current_dir()
            .unwrap_or_else(|_| PathBuf::from("/"))
            .to_string_lossy()
            .to_string();
        let dir_input_data = Rc::new(RefCell::new(current_dir.clone()));

        // Directory name input line - widened: TRect( 3, 3, 48, 4 )
        let input_bounds = Rect::new(3, 3, 48, 4);
        let dir_input = InputLine::new(input_bounds, 255, Rc::clone(&dir_input_data));
        let dir_input_id = dialog.add(Box::new(dir_input));

        // Label "Directory name" - Borland: (2, 2)
        let label_bounds = Rect::new(2, 2, 20, 2);
        let dir_label = Label::new(label_bounds, "Directory ~n~ame");
        dialog.add(Box::new(dir_label));

        // History button - adjusted: TRect( 48, 3, 51, 4 )
        // Shows a dropdown button (â–¼) that displays previous directories
        let history_button = History::new(Point::new(48, 3), history_id);
        dialog.add(Box::new(history_button));

        // Vertical scrollbar - adjusted: TRect( 50, 6, 51, 16 )
        let v_scrollbar_bounds = Rect::new(50, 6, 51, 16);
        let v_scrollbar = ScrollBar::new_vertical(v_scrollbar_bounds);
        let v_scrollbar_rc = Rc::new(RefCell::new(v_scrollbar));
        dialog.add(Box::new(SharedScrollBar(Rc::clone(&v_scrollbar_rc))));

        // Horizontal scrollbar - adjusted: TRect( 3, 16, 50, 17 )
        let h_scrollbar_bounds = Rect::new(3, 16, 50, 17);
        let h_scrollbar = ScrollBar::new_horizontal(h_scrollbar_bounds);
        let h_scrollbar_rc = Rc::new(RefCell::new(h_scrollbar));
        dialog.add(Box::new(SharedScrollBar(Rc::clone(&h_scrollbar_rc))));

        // Directory listbox - widened: TRect( 3, 6, 50, 16 )
        let listbox_bounds = Rect::new(3, 6, 50, 16);
        let current_path = PathBuf::from(&current_dir);
        let dir_list = DirListBox::new(listbox_bounds, &current_path);
        let dir_listbox = Rc::new(RefCell::new(dir_list));
        let shared_listbox = SharedDirListBox::new(
            Rc::clone(&dir_listbox),
            Rc::clone(&dir_input_data),
            Rc::clone(&v_scrollbar_rc),
            Rc::clone(&h_scrollbar_rc),
        );
        let dir_list_id = dialog.add(Box::new(shared_listbox));

        // Label "Directory tree" - Borland: (2, 5)
        let tree_label_bounds = Rect::new(2, 5, 20, 5);
        let tree_label = Label::new(tree_label_bounds, "Directory ~t~ree");
        dialog.add(Box::new(tree_label));

        // OK button - adjusted: TRect( 53, 6, 63, 8 )
        let ok_bounds = Rect::new(53, 6, 63, 8);
        let ok_button = Button::new(ok_bounds, "~O~K", CM_OK, true);
        let ok_button_id = dialog.add(Box::new(ok_button));

        // Chdir button - adjusted: TRect( 53, 9, 63, 11 )
        let chdir_bounds = Rect::new(53, 9, 63, 11);
        let mut chdir_button = Button::new(chdir_bounds, "~C~hdir", CM_CHANGE_DIR, false);
        chdir_button.set_broadcast(true);  // Broadcast instead of ending dialog
        chdir_button.set_selectable(false);  // Not part of focus cycle
        let chdir_button_id = dialog.add(Box::new(chdir_button));

        // Revert button - adjusted: TRect( 53, 12, 63, 14 )
        let revert_bounds = Rect::new(53, 12, 63, 14);
        let mut revert_button = Button::new(revert_bounds, "~R~evert", CM_REVERT, false);
        revert_button.set_broadcast(true);  // Broadcast instead of ending dialog
        revert_button.set_selectable(false);  // Not part of focus cycle
        dialog.add(Box::new(revert_button));

        // Help button is intentionally NOT implemented
        // Borland: TRect( 35, 15, 45, 17 ) - optional, requires help system
        // Will be added when application-wide help system is implemented

        Self {
            dialog,
            dir_input_data,
            history_id,
            dir_list_id,
            dir_input_id,
            ok_button_id,
            chdir_button_id,
            selected_directory: None,
        }
    }

    /// Execute the dialog modally
    ///
    /// Returns the selected directory if OK was pressed, None if cancelled
    ///
    /// Matches Borland: user interacts with dialog, OK/Cancel to exit
    /// The valid() method validates the directory before closing
    /// History is automatically updated on success
    pub fn execute(&mut self, app: &mut Application) -> Option<PathBuf> {
        loop {
            let end_state = self.dialog.execute(app);

            if end_state == CM_OK {
                // Validate the directory path from the input line
                let dir_path = self.dir_input_data.borrow().clone();
                let path = PathBuf::from(&dir_path);

                // Try to change directory (validates that it exists and is accessible)
                if let Err(e) = std::env::set_current_dir(&path) {
                    // Invalid directory - show error and re-execute dialog
                    // Matches Borland: valid() returns False, shows error, and keeps dialog open
                    let error_msg = format!("Invalid directory\n\n{}", e);
                    message_box_error(app, &error_msg);

                    // Re-execute the dialog to let user try again
                    continue;
                }

                // Valid directory - add to history and return success
                // Matches Borland: historyAdd is called in handleEvent on cmReleasedFocus
                HistoryManager::add(self.history_id, dir_path.clone());

                self.selected_directory = Some(path.clone());
                return Some(path);
            } else {
                // User cancelled
                return None;
            }
        }
    }

    /// Get the selected directory
    pub fn get_directory(&self) -> Option<PathBuf> {
        self.selected_directory.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 ChDirDialog {
    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 can_focus(&self) -> bool {
        true
    }

    fn state(&self) -> crate::core::state::StateFlags {
        self.dialog.state()
    }

    fn set_state(&mut self, state: crate::core::state::StateFlags) {
        self.dialog.set_state(state);
    }

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

/// Builder for creating change directory dialogs with a fluent API.
pub struct ChDirDialogBuilder {
    history_id: Option<u16>,
}

impl ChDirDialogBuilder {
    /// Creates a new ChDirDialogBuilder
    pub fn new() -> Self {
        Self { history_id: None }
    }

    /// Sets a custom history ID for directory history
    #[must_use]
    pub fn history_id(mut self, history_id: u16) -> Self {
        self.history_id = Some(history_id);
        self
    }

    /// Builds the ChDirDialog with Borland standard layout
    pub fn build(self) -> ChDirDialog {
        ChDirDialog::new(self.history_id)
    }

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

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