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
//! Backend-agnostic app code for the `FolderPickerController` demo
//! ([`tui_folder_picker`] / [`gtk_folder_picker`]).
//!
//! [`FolderPickerApp`] demonstrates a self-contained `AppLogic` that:
//! - Opens the `FolderPickerController` on startup (rooted at `env::current_dir()`).
//! - Shows the picker as a centred palette modal.
//! - Confirms a selection (`Enter` on a non-`..` entry) and displays the
//! chosen path in a status bar.
//! - Navigates into subdirectories or up with `..` / `-`.
//! - Dismisses the picker with `Esc`, then shows the last-confirmed path.
//!
//! Controls (while picker is open):
//! - Type to fuzzy-filter entries.
//! - `↑` / `k` and `↓` / `j` to move selection.
//! - `Enter` on `..` or `-` key — navigate up.
//! - `Enter` on any other entry — confirm that path.
//! - `Backspace` — delete last query character.
//! - `Esc` — dismiss picker.
//!
//! Controls (after dismiss):
//! - `o` — reopen picker.
//! - `q` / `Esc` — quit.
use std::path::PathBuf;
use quadraui::{
AppLogic, Backend, Color, FolderPickerController, FolderPickerEvent, InteractionState, Key,
NamedKey, Reaction, Rect, StatusBar, StatusBarSegment, UiEvent, WidgetId, PALETTE_CHROME_ROWS,
};
pub struct FolderPickerApp {
picker: Option<FolderPickerController>,
confirmed_path: Option<PathBuf>,
status: String,
/// Directory the picker is (re)opened at — captured once so `o`
/// reopens where the app started rather than re-reading the
/// process's working directory.
root: PathBuf,
}
impl FolderPickerApp {
pub fn new() -> Self {
// Fallback to "." (current dir relative) if `current_dir()` fails.
// `PathBuf::from("/")` would be invalid on Windows, so we keep this
// platform-neutral. The picker will still walk something sensible
// from the process's working directory.
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
Self::with_root(cwd)
}
/// Open the picker at an explicit `root` instead of the process's
/// working directory.
///
/// The runnable examples use [`Self::new`]; this exists so tests can
/// drive the demo against a directory they control (path length,
/// entry names) rather than inheriting whatever the checkout
/// happens to sit under.
pub fn with_root(root: impl Into<PathBuf>) -> Self {
let root = root.into();
let picker = FolderPickerController::new(root.clone(), vec![], false);
Self {
picker: Some(picker),
confirmed_path: None,
status: "Open Folder picker — navigate and press Enter to confirm".into(),
root,
}
}
/// Returns the popup rect centred in the viewport.
///
/// Sizing mirrors vimcode's TUI picker: 60% width (min 50), 55% height
/// (min 15). In pixel-unit backends the numbers are larger but the
/// proportions are the same — the line_height is factored in via the
/// AppLogic choosing cell-like units.
fn popup_rect(backend: &dyn Backend) -> Rect {
let vp = backend.viewport();
let w = (vp.width * 0.6).max(50.0);
let h = (vp.height * 0.55).max(15.0 * backend.line_height());
let x = (vp.width - w) / 2.0;
let y = (vp.height - h) / 2.0;
Rect::new(x, y, w, h)
}
fn status_bar(&self) -> StatusBar {
StatusBar {
id: WidgetId::new("status"),
left_segments: vec![StatusBarSegment {
text: format!(" {} ", self.status),
fg: Color::rgb(220, 220, 220),
bg: Color::rgb(40, 60, 100),
bold: false,
action_id: None,
}],
right_segments: if let Some(ref p) = self.confirmed_path {
// Only the final component — the full path is already in
// the left segment. A right segment wider than the bar is
// laid out at column 0 (see `StatusBar::layout`: the
// highest-priority right segment is kept even when it
// alone overflows) and painted *after* the left segments,
// so an absolute path longer than the terminal is wide
// would blank the "Confirmed: …" message entirely — which
// is exactly what happens when the checkout lives under a
// long directory.
let leaf = p
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| p.display().to_string());
vec![StatusBarSegment {
text: format!(" ✓ {leaf} "),
fg: Color::rgb(150, 240, 150),
bg: Color::rgb(30, 80, 30),
bold: false,
action_id: None,
}]
} else {
vec![]
},
}
}
}
impl Default for FolderPickerApp {
fn default() -> Self {
Self::new()
}
}
impl AppLogic for FolderPickerApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
let vp = backend.viewport();
let lh = backend.line_height();
// Status bar at the bottom.
let bar_h = lh * 1.5;
let bar_rect = Rect::new(0.0, vp.height - bar_h, vp.width, bar_h);
let bar = self.status_bar();
let _ = backend.draw_status_bar_interactive(bar_rect, &bar, &InteractionState::new());
// Folder picker modal (when open).
if let Some(ref picker) = self.picker {
let popup_rect = Self::popup_rect(backend);
picker.render(popup_rect, backend);
}
}
fn handle(&mut self, event: UiEvent, backend: &mut dyn Backend) -> Reaction {
if let Some(ref mut picker) = self.picker {
let popup_rect = Self::popup_rect(backend);
let lh = backend.line_height();
// Compute visible_rows from the popup height minus chrome.
let popup_h_rows = if lh > 0.0 {
(popup_rect.height / lh) as usize
} else {
24
};
let visible_rows = popup_h_rows.saturating_sub(PALETTE_CHROME_ROWS);
let ev = picker.handle(&event, visible_rows);
match ev {
FolderPickerEvent::Confirmed { path } => {
self.confirmed_path = Some(path.clone());
self.status = format!(
"Confirmed: {} (press 'o' to reopen, q/Esc to quit)",
path.display()
);
self.picker = None;
return Reaction::Redraw;
}
FolderPickerEvent::Cancelled => {
self.status = "Dismissed — press 'o' to reopen, q/Esc to quit".into();
self.picker = None;
return Reaction::Redraw;
}
FolderPickerEvent::Consumed => return Reaction::Redraw,
FolderPickerEvent::Ignored => {}
}
} else {
// Picker is closed — handle reopen / quit.
if let UiEvent::KeyPressed { ref key, .. } = event {
match key {
Key::Char('q') | Key::Named(NamedKey::Escape) => {
return Reaction::Exit;
}
Key::Char('o') => {
// Reopen at the root this app was constructed with
// (the process's working directory for `new()`).
self.picker = Some(FolderPickerController::new(
self.root.clone(),
vec![],
false,
));
self.status =
"Open Folder picker — navigate and press Enter to confirm".into();
return Reaction::Redraw;
}
_ => {}
}
}
}
match event {
UiEvent::WindowResized { .. } => Reaction::Redraw,
_ => Reaction::Continue,
}
}
}