git_worktree_manager/tui/
multi_select.rs1use std::io::IsTerminal;
6
7#[cfg(unix)]
8use super::arrow_select::{get_terminal_width, read_key, truncate, write_stderr, Key};
9
10pub fn multi_select(items: &[String], title: &str) -> Option<Vec<usize>> {
14 if items.is_empty() {
15 return Some(Vec::new());
16 }
17 if !std::io::stderr().is_terminal() {
18 return multi_select_fallback(items, title);
19 }
20
21 #[cfg(unix)]
22 {
23 if let Some(result) = multi_select_unix(items, title) {
24 return result;
25 }
26 }
27
28 multi_select_fallback(items, title)
29}
30
31#[cfg(unix)]
34fn multi_select_unix(items: &[String], title: &str) -> Option<Option<Vec<usize>>> {
35 use std::os::unix::io::AsRawFd;
36
37 let stdin = std::io::stdin();
38 let fd = stdin.as_raw_fd();
39
40 let mut old_termios: libc::termios = unsafe { std::mem::zeroed() };
42 if unsafe { libc::tcgetattr(fd, &mut old_termios) } != 0 {
43 return None;
44 }
45
46 let mut raw = old_termios;
48 unsafe { libc::cfmakeraw(&mut raw) };
49 if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) } != 0 {
50 return None;
51 }
52
53 write_stderr("\x1b[?25l");
55
56 let mut cursor = 0usize;
57 let mut checked: Vec<bool> = vec![false; items.len()];
58 let total_lines = items.len() + 3; render(items, &checked, cursor, title, true);
61
62 let result: Option<Vec<usize>> = loop {
63 match read_key(fd) {
64 Ok(Key::Up) => {
65 cursor = cursor.saturating_sub(1);
66 render(items, &checked, cursor, title, false);
67 }
68 Ok(Key::Down) => {
69 if cursor + 1 < items.len() {
70 cursor += 1;
71 }
72 render(items, &checked, cursor, title, false);
73 }
74 Ok(Key::Space) => {
75 checked[cursor] = !checked[cursor];
76 render(items, &checked, cursor, title, false);
77 }
78 Ok(Key::Enter) => {
79 break Some(
80 checked
81 .iter()
82 .enumerate()
83 .filter_map(|(i, &c)| if c { Some(i) } else { None })
84 .collect(),
85 );
86 }
87 Ok(Key::Escape) | Ok(Key::Quit) | Ok(Key::CtrlC) | Err(_) => {
88 break None;
89 }
90 _ => {}
91 }
92 };
93
94 write_stderr("\x1b[?25h");
96 super::arrow_select::cleanup(total_lines);
97 unsafe {
98 libc::tcsetattr(fd, libc::TCSANOW, &old_termios);
99 }
100
101 Some(result)
102}
103
104#[cfg(unix)]
105fn render(items: &[String], checked: &[bool], cursor: usize, title: &str, first: bool) {
106 let width = get_terminal_width();
107
108 if !first {
109 write_stderr("\x1b[u");
110 }
111 write_stderr("\x1b[s");
112
113 let line = format!(" \x1b[1m{title}\x1b[0m");
115 write_stderr(&format!("\x1b[2K{}\r\n", truncate(&line, width)));
116 write_stderr("\x1b[2K\r\n");
117
118 for (i, label) in items.iter().enumerate() {
119 write_stderr("\x1b[2K");
120 let mark = if checked[i] { "[x]" } else { "[ ]" };
121 let line = if i == cursor {
122 format!(" \x1b[1;7m > {mark} {label} \x1b[0m")
123 } else {
124 format!(" {mark} {label}")
125 };
126 write_stderr(&format!("{}\r\n", truncate(&line, width)));
127 }
128
129 write_stderr("\x1b[2K");
131 write_stderr(" \x1b[2m(Space: toggle, Enter: confirm, Esc/q: cancel)\x1b[0m\r\n");
132
133 write_stderr("\x1b[2K\r\n");
135 write_stderr("\x1b[2A");
137}
138
139fn multi_select_fallback(items: &[String], title: &str) -> Option<Vec<usize>> {
142 eprintln!("{}", title);
143 for (i, item) in items.iter().enumerate() {
144 eprintln!(" [{}] {}", i + 1, item);
145 }
146 eprintln!("Enter numbers (space- or comma-separated), 'all', or blank to cancel:");
147 let mut buf = String::new();
148 if std::io::stdin().read_line(&mut buf).is_err() {
149 return None;
150 }
151 let s = buf.trim();
152 if s.is_empty() {
153 return None;
154 }
155 if s.eq_ignore_ascii_case("all") {
156 return Some((0..items.len()).collect());
157 }
158 let mut out = Vec::new();
159 for part in s.split(|c: char| c == ',' || c.is_whitespace()) {
160 if part.is_empty() {
161 continue;
162 }
163 if let Ok(n) = part.parse::<usize>() {
164 if n >= 1 && n <= items.len() {
165 out.push(n - 1);
166 }
167 }
168 }
169 out.sort();
170 out.dedup();
171 Some(out)
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn empty_items_returns_empty_selection() {
180 let out = multi_select(&[], "title");
181 assert_eq!(out, Some(Vec::new()));
182 }
183}