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