atomcode_tuix/modals/
dir_picker.rs1use std::path::PathBuf;
10
11use anyhow::Result;
12use crossterm::event::{KeyCode, KeyModifiers};
13
14use super::{Modal, ModalAction};
15use crate::event_loop::commands::apply_cd;
16use crate::event_loop::{build_status, Buffer, LoopCtx};
17use crate::render::{MenuPayload, Renderer, UiLine};
18use crate::state::UiState;
19
20pub struct DirPicker {
21 pub dirs: Vec<PathBuf>,
25 pub current: PathBuf,
28 pub selected: usize,
30}
31
32impl DirPicker {
33 pub fn open(dirs: Vec<PathBuf>, current: PathBuf) -> Self {
34 Self {
35 dirs,
36 current,
37 selected: 0,
38 }
39 }
40
41 fn up(&mut self) {
42 self.selected = self.selected.saturating_sub(1);
43 }
44
45 fn down(&mut self) {
46 if self.dirs.is_empty() {
47 self.selected = 0;
48 return;
49 }
50 let max = self.dirs.len() - 1;
51 if self.selected < max {
52 self.selected += 1;
53 }
54 }
55
56 fn chosen(&self) -> Option<PathBuf> {
57 self.dirs.get(self.selected).cloned()
58 }
59}
60
61impl Modal for DirPicker {
62 fn handle_key(
63 &mut self,
64 code: KeyCode,
65 _mods: KeyModifiers,
66 buf: &mut Buffer,
67 state: &mut UiState,
68 ctx: &mut LoopCtx,
69 renderer: &mut dyn Renderer,
70 ) -> Result<ModalAction> {
71 match code {
72 KeyCode::Up => {
73 self.up();
74 self.draw(buf, state, ctx, renderer);
75 Ok(ModalAction::Continue)
76 }
77 KeyCode::Down => {
78 self.down();
79 self.draw(buf, state, ctx, renderer);
80 Ok(ModalAction::Continue)
81 }
82 KeyCode::Enter => {
83 let Some(path) = self.chosen() else {
84 return Ok(ModalAction::Continue);
85 };
86 if path == ctx.working_dir {
87 return Ok(ModalAction::Close);
90 }
91 if !path.is_dir() {
92 let p = path.display().to_string();
93 renderer.render(UiLine::Error(
94 crate::i18n::t(crate::i18n::Msg::DirNotExists { path: &p }).into_owned(),
95 ));
96 renderer.flush();
97 return Ok(ModalAction::Close);
98 }
99 apply_cd(ctx, path.clone());
100 let p = path.display().to_string();
101 renderer.render(UiLine::CommandOutput(
102 crate::i18n::t(crate::i18n::Msg::DirChanged { path: &p }).into_owned(),
103 ));
104 renderer.flush();
105 Ok(ModalAction::Close)
106 }
107 KeyCode::Esc => Ok(ModalAction::Close),
108 _ => Ok(ModalAction::Continue),
109 }
110 }
111
112 fn draw(&self, buf: &Buffer, state: &UiState, ctx: &LoopCtx, renderer: &mut dyn Renderer) {
113 let payload = build_menu_payload(self);
114 renderer.render(UiLine::InputPrompt {
115 buf: buf.text.clone(),
116 cursor_byte: buf.cursor,
117 menu: Some(payload),
118 status: build_status(state, ctx),
119 attachments: Vec::new(),
120 });
121 renderer.flush();
122 }
123}
124
125fn build_menu_payload(p: &DirPicker) -> MenuPayload {
126 let items: Vec<(String, String)> = p
127 .dirs
128 .iter()
129 .map(|d| {
130 let name = crate::platform::collapse_home(&d.to_string_lossy());
131 let desc = if d == &p.current {
132 crate::i18n::t(crate::i18n::Msg::DirCurrent).into_owned()
133 } else {
134 String::new()
135 };
136 (name, desc)
137 })
138 .collect();
139 MenuPayload {
140 items,
141 selected: p.selected,
142 kind: crate::render::MenuKind::SlashCommand,
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 fn pb(s: &str) -> PathBuf {
151 PathBuf::from(s)
152 }
153
154 #[test]
155 fn open_seeds_selection_at_zero() {
156 let p = DirPicker::open(vec![pb("/a"), pb("/b")], pb("/a"));
157 assert_eq!(p.selected, 0);
158 assert_eq!(p.dirs.len(), 2);
159 }
160
161 #[test]
162 fn down_and_up_stay_within_bounds() {
163 let mut p = DirPicker::open(vec![pb("/a"), pb("/b")], pb("/a"));
164 p.down();
165 assert_eq!(p.selected, 1);
166 p.down();
167 assert_eq!(p.selected, 1, "down at end stays put");
168 p.up();
169 assert_eq!(p.selected, 0);
170 p.up();
171 assert_eq!(p.selected, 0, "up at top stays put");
172 }
173
174 #[test]
175 fn chosen_returns_selected_path() {
176 let mut p = DirPicker::open(vec![pb("/a"), pb("/b"), pb("/c")], pb("/a"));
177 p.down();
178 assert_eq!(p.chosen(), Some(pb("/b")));
179 }
180
181 #[test]
182 fn menu_payload_marks_current_dir() {
183 let p = DirPicker::open(vec![pb("/a"), pb("/b")], pb("/b"));
184 let payload = build_menu_payload(&p);
185 assert_eq!(payload.items[0].1, "");
186 assert_eq!(payload.items[1].1, "current");
187 }
188}