1use crate::styles::{paint, DIM, WHITE};
4use std::path::{Path, PathBuf};
5
6use super::core::{Frame, Key, Prompt, RenderCtx, Step};
7use super::error::{PromptError, Result};
8use super::theme;
9
10pub struct PathPrompt {
12 label: String,
13 initial: Option<String>,
14 directory: bool,
15 max_items: usize,
16 buf: String,
17 cur: usize,
18}
19
20pub fn path(label: impl Into<String>) -> PathPrompt {
30 PathPrompt {
31 label: label.into(),
32 initial: None,
33 directory: false,
34 max_items: 8,
35 buf: String::new(),
36 cur: 0,
37 }
38}
39
40impl PathPrompt {
41 pub fn initial(mut self, v: impl Into<String>) -> Self {
43 self.initial = Some(v.into());
44 self
45 }
46 pub fn directory(mut self, v: bool) -> Self {
48 self.directory = v;
49 self
50 }
51 pub fn max_items(mut self, n: usize) -> Self {
53 self.max_items = n.max(1);
54 self
55 }
56
57 pub fn run(mut self) -> Result<PathBuf> {
59 self.buf = self.initial.clone().unwrap_or_else(|| {
60 std::env::current_dir()
61 .map(|p| p.to_string_lossy().into_owned())
62 .unwrap_or_default()
63 });
64 if !self.buf.ends_with('/') {
65 self.buf.push('/');
66 }
67 super::core::run(self)
68 }
69
70 fn entries(&self) -> Vec<Entry> {
71 let (dir, fragment) = split_path(&self.buf);
72 let mut out = Vec::new();
73 if let Ok(rd) = std::fs::read_dir(&dir) {
74 for ent in rd.flatten() {
75 let name = ent.file_name().to_string_lossy().into_owned();
76 if !name.starts_with(fragment) {
77 continue;
78 }
79 let is_dir = ent.file_type().map(|t| t.is_dir()).unwrap_or(false);
80 if self.directory && !is_dir {
81 continue;
82 }
83 out.push(Entry {
84 name,
85 path: ent.path(),
86 is_dir,
87 });
88 }
89 }
90 out.sort_by(|a, b| b.is_dir.cmp(&a.is_dir).then(a.name.cmp(&b.name)));
91 out.truncate(self.max_items);
92 out
93 }
94}
95
96impl Prompt for PathPrompt {
97 type Output = PathBuf;
98
99 fn handle(&mut self, key: Key) -> Step<PathBuf> {
100 match key {
101 Key::Char('\t') => {
102 let entries = self.entries();
103 if let Some(e) = entries.get(self.cur) {
104 self.buf = e.path.to_string_lossy().into_owned();
105 if e.is_dir {
106 self.buf.push('/');
107 }
108 self.cur = 0;
109 }
110 Step::Continue
111 }
112 Key::Char(c) => {
113 self.buf.push(c);
114 self.cur = 0;
115 Step::Continue
116 }
117 Key::Backspace => {
118 self.buf.pop();
119 self.cur = 0;
120 Step::Continue
121 }
122 Key::Up => {
123 if self.cur > 0 {
124 self.cur -= 1;
125 }
126 Step::Continue
127 }
128 Key::Down => {
129 if self.cur + 1 < self.entries().len() {
130 self.cur += 1;
131 }
132 Step::Continue
133 }
134 Key::Enter => {
135 let p = PathBuf::from(&self.buf);
136 if self.directory && !p.is_dir() {
137 return Step::Reject("Not a directory".into());
138 }
139 Step::Submit(p)
140 }
141 Key::Escape | Key::Interrupt => Step::Cancel,
142 _ => Step::Continue,
143 }
144 }
145
146 fn render(&self, _ctx: RenderCtx) -> Frame {
147 let c = super::settings::colors();
148 let mut f: Frame = Vec::new();
149 f.push(theme::label(&self.label));
150 f.push(format!(
151 "{} {}",
152 paint(c.dim, "│"),
153 paint(c.input, &self.buf)
154 ));
155 let entries = self.entries();
156 if entries.is_empty() {
157 f.push(format!(
158 "{} {}",
159 paint(c.dim, "│"),
160 paint(c.dim, "(no matches)")
161 ));
162 } else {
163 for (i, e) in entries.iter().enumerate() {
164 let active = i == self.cur;
165 let glyph = if e.is_dir { "/" } else { " " };
166 let style = if active { WHITE } else { DIM };
167 let prefix = if active {
168 paint(c.accent, "❯")
169 } else {
170 paint(c.dim, " ")
171 };
172 f.push(format!(
173 "{} {} {}{}",
174 paint(c.dim, "│"),
175 prefix,
176 paint(style, &e.name),
177 paint(c.dim, glyph)
178 ));
179 }
180 }
181 f.push(theme::hint(
182 "Tab to complete · ↑ ↓ to navigate · Enter to confirm",
183 ));
184 f.push(theme::frame_bot(None));
185 f
186 }
187
188 fn render_answered(&self, value: &PathBuf) -> Frame {
189 theme::answered(&self.label, &value.display().to_string())
190 .split("\r\n")
191 .map(String::from)
192 .collect()
193 }
194
195 fn run_fallback(self) -> Result<PathBuf> {
196 use std::io::Write;
197 let mut out = std::io::stderr();
198 write!(out, " {}: ", self.label).map_err(PromptError::Io)?;
199 out.flush().map_err(PromptError::Io)?;
200 let line = super::engine::fallback::read_line_raw()?;
201 Ok(PathBuf::from(line))
202 }
203}
204
205struct Entry {
206 name: String,
207 path: PathBuf,
208 is_dir: bool,
209}
210
211fn split_path(buf: &str) -> (PathBuf, &str) {
212 if let Some(pos) = buf.rfind('/') {
213 let (dir, rest) = buf.split_at(pos + 1);
214 let dir = if dir.is_empty() {
215 "/".into()
216 } else {
217 PathBuf::from(dir)
218 };
219 (dir, rest)
220 } else {
221 (Path::new(".").to_path_buf(), buf)
222 }
223}