1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Ask-user prompt controller for the TUI.
//!
//! Displays a question with numbered options and tracks the user's selection.
//! The key handler is responsible for sending the answer through the response
//! channel stored in `App::ask_user_response_tx`.
/// Controller for displaying questions with selectable options or free-text input.
pub struct AskUserController {
question: String,
options: Vec<String>,
default: Option<String>,
selected: usize,
active: bool,
text_input: String,
}
impl AskUserController {
/// Create a new inactive ask-user controller.
pub fn new() -> Self {
Self {
question: String::new(),
options: Vec::new(),
default: None,
selected: 0,
active: false,
text_input: String::new(),
}
}
/// Whether the prompt is currently active.
pub fn active(&self) -> bool {
self.active
}
/// The question being asked.
pub fn question(&self) -> &str {
&self.question
}
/// The available options.
pub fn options(&self) -> &[String] {
&self.options
}
/// The currently selected index.
pub fn selected_index(&self) -> usize {
self.selected
}
/// The default value (used as fallback on cancel/Esc).
pub fn default_value(&self) -> Option<String> {
self.default.clone()
}
/// Whether the prompt has selectable options.
pub fn has_options(&self) -> bool {
!self.options.is_empty()
}
/// The current free-text input buffer.
pub fn text_input(&self) -> &str {
&self.text_input
}
/// Append a character to the free-text input.
pub fn push_char(&mut self, c: char) {
self.text_input.push(c);
}
/// Remove the last character from the free-text input.
pub fn pop_char(&mut self) {
self.text_input.pop();
}
/// Start the ask-user prompt.
pub fn start(&mut self, question: String, options: Vec<String>, default: Option<String>) {
self.question = question;
self.options = options;
self.default = default;
self.selected = 0;
self.active = true;
}
/// Move selection to the next option (wrapping).
pub fn next(&mut self) {
if !self.active || self.options.is_empty() {
return;
}
self.selected = (self.selected + 1) % self.options.len();
}
/// Move selection to the previous option (wrapping).
pub fn prev(&mut self) {
if !self.active || self.options.is_empty() {
return;
}
self.selected = (self.selected + self.options.len() - 1) % self.options.len();
}
/// Confirm the current selection and deactivate.
///
/// When options are present, returns the selected option text.
/// When no options exist, returns the free-text input (or default if input is empty).
/// Returns `None` only if there is nothing to confirm.
pub fn confirm(&mut self) -> Option<String> {
if !self.active {
return None;
}
if !self.options.is_empty() {
let answer = self.options[self.selected].clone();
self.cleanup();
return Some(answer);
}
// Free-text mode: use text input, fall back to default
let answer = if self.text_input.is_empty() {
self.default.clone()
} else {
Some(self.text_input.clone())
};
if answer.is_some() {
self.cleanup();
}
answer
}
/// Cancel the prompt and deactivate.
/// The caller is responsible for sending the fallback through the response channel.
pub fn cancel(&mut self) {
if !self.active {
return;
}
self.cleanup();
}
/// Reset to inactive state.
fn cleanup(&mut self) {
self.active = false;
self.question.clear();
self.options.clear();
self.default = None;
self.selected = 0;
self.text_input.clear();
}
}
impl Default for AskUserController {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "ask_user_tests.rs"]
mod tests;