xacli_components/components/
select.rs1use std::io::{stdout, Write};
2
3use crossterm::{
4 cursor,
5 event::{self, Event, KeyCode, KeyModifiers},
6 queue,
7 style::Print,
8 terminal::{self, ClearType},
9};
10
11use crate::{ComponentError, Result};
12
13pub struct Select<T> {
14 prompt: String,
15 options: Vec<(String, T)>,
16}
17
18impl<T> Select<T> {
19 pub fn new(prompt: impl Into<String>) -> Self {
20 Self {
21 prompt: prompt.into(),
22 options: Vec::new(),
23 }
24 }
25
26 pub fn option(mut self, label: impl Into<String>, value: T) -> Self {
27 self.options.push((label.into(), value));
28 self
29 }
30
31 pub fn options(mut self, options: Vec<(String, T)>) -> Self {
32 self.options = options;
33 self
34 }
35
36 pub fn run(self) -> Result<T> {
37 if self.options.is_empty() {
38 return Err(ComponentError::InvalidInput("No options provided".into()));
39 }
40
41 let mut stdout = stdout();
42 terminal::enable_raw_mode()?;
43
44 let result = self.run_inner(&mut stdout);
45
46 terminal::disable_raw_mode()?;
47
48 result
49 }
50
51 fn run_inner(mut self, stdout: &mut impl Write) -> Result<T> {
52 let mut selected = 0;
53 let mut first_render = true;
54
55 self.render(stdout, selected, first_render)?;
56 first_render = false;
57
58 loop {
59 if let Event::Key(key) = event::read()? {
60 match key.code {
61 KeyCode::Enter => {
62 self.clear_render(stdout)?;
64 queue!(
66 stdout,
67 cursor::MoveToColumn(0),
68 Print("✓ "),
69 Print(&self.options[selected].0),
70 Print("\r\n")
71 )?;
72 stdout.flush()?;
73 return Ok(self.options.remove(selected).1);
74 }
75 KeyCode::Esc => {
76 self.clear_render(stdout)?;
77 return Err(ComponentError::Interrupted);
78 }
79 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
80 self.clear_render(stdout)?;
81 return Err(ComponentError::Interrupted);
82 }
83 KeyCode::Up | KeyCode::Char('k') => {
84 if selected > 0 {
85 selected -= 1;
86 self.render(stdout, selected, first_render)?;
87 }
88 }
89 KeyCode::Down | KeyCode::Char('j') => {
90 if selected < self.options.len() - 1 {
91 selected += 1;
92 self.render(stdout, selected, first_render)?;
93 }
94 }
95 KeyCode::Home => {
96 selected = 0;
97 self.render(stdout, selected, first_render)?;
98 }
99 KeyCode::End => {
100 selected = self.options.len() - 1;
101 self.render(stdout, selected, first_render)?;
102 }
103 _ => {}
104 }
105 }
106 }
107 }
108
109 fn render(&self, stdout: &mut impl Write, selected: usize, first_render: bool) -> Result<()> {
110 if !first_render {
111 for _ in 0..self.options.len() + 1 {
113 queue!(
114 stdout,
115 cursor::MoveUp(1),
116 terminal::Clear(ClearType::CurrentLine),
117 )?;
118 }
119 }
120
121 queue!(stdout, cursor::MoveToColumn(0))?;
122
123 queue!(stdout, Print(&self.prompt), Print("\r\n"))?;
125
126 for (i, (label, _)) in self.options.iter().enumerate() {
128 let marker = if i == selected { ">" } else { " " };
129 queue!(
130 stdout,
131 cursor::MoveToColumn(0),
132 Print(marker),
133 Print(" "),
134 Print(label),
135 Print("\r\n"),
136 )?;
137 }
138
139 stdout.flush()?;
140 Ok(())
141 }
142
143 fn clear_render(&self, stdout: &mut impl Write) -> Result<()> {
144 for _ in 0..self.options.len() + 1 {
145 queue!(
146 stdout,
147 cursor::MoveUp(1),
148 terminal::Clear(ClearType::CurrentLine),
149 )?;
150 }
151 queue!(stdout, cursor::MoveToColumn(0))?;
152 stdout.flush()?;
153 Ok(())
154 }
155}