sql_cli/widgets/
debounced_input.rs1use crate::utils::debouncer::Debouncer;
7use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
8use ratatui::{
9 layout::Rect,
10 style::{Color, Style},
11 widgets::{Block, Borders, Paragraph},
12 Frame,
13};
14use tui_input::{backend::crossterm::EventHandler, Input};
15
16#[derive(Debug, Clone)]
18pub enum DebouncedInputAction {
19 Continue,
21 InputChanged(String),
23 ExecuteDebounced(String),
25 Confirm(String),
27 Cancel,
29 PassThrough,
31}
32
33#[derive(Debug, Clone)]
35pub struct DebouncedInputConfig {
36 pub debounce_ms: u64,
38 pub title: String,
40 pub style: Style,
42 pub show_debounce_indicator: bool,
44}
45
46impl Default for DebouncedInputConfig {
47 fn default() -> Self {
48 Self {
49 debounce_ms: 300,
50 title: "Search".to_string(),
51 style: Style::default().fg(Color::Yellow),
52 show_debounce_indicator: true,
53 }
54 }
55}
56
57pub struct DebouncedInput {
59 input: Input,
61 debouncer: Debouncer,
63 last_executed_pattern: Option<String>,
65 config: DebouncedInputConfig,
67 active: bool,
69}
70
71impl Default for DebouncedInput {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl DebouncedInput {
78 #[must_use]
80 pub fn new() -> Self {
81 Self::with_config(DebouncedInputConfig::default())
82 }
83
84 #[must_use]
86 pub fn with_config(config: DebouncedInputConfig) -> Self {
87 Self {
88 input: Input::default(),
89 debouncer: Debouncer::new(config.debounce_ms),
90 last_executed_pattern: None,
91 config,
92 active: false,
93 }
94 }
95
96 pub fn activate(&mut self) {
98 self.active = true;
99 self.input.reset();
100 self.debouncer.reset();
101 self.last_executed_pattern = None;
102 }
103
104 pub fn deactivate(&mut self) {
106 self.active = false;
107 self.debouncer.reset();
108 }
109
110 #[must_use]
112 pub fn is_active(&self) -> bool {
113 self.active
114 }
115
116 #[must_use]
118 pub fn value(&self) -> &str {
119 self.input.value()
120 }
121
122 pub fn set_value(&mut self, value: String) {
124 self.input = Input::default().with_value(value);
125 }
126
127 #[must_use]
129 pub fn cursor(&self) -> usize {
130 self.input.cursor()
131 }
132
133 pub fn set_config(&mut self, config: DebouncedInputConfig) {
135 self.debouncer = Debouncer::new(config.debounce_ms);
136 self.config = config;
137 }
138
139 pub fn handle_key(&mut self, key: KeyEvent) -> DebouncedInputAction {
141 if !self.active {
142 return DebouncedInputAction::PassThrough;
143 }
144
145 match key.code {
146 KeyCode::Esc => {
147 self.deactivate();
148 DebouncedInputAction::Cancel
149 }
150 KeyCode::Enter => {
151 let pattern = self.input.value().to_string();
152 self.deactivate();
153 DebouncedInputAction::Confirm(pattern)
154 }
155 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
156 DebouncedInputAction::PassThrough
158 }
159 _ => {
160 self.input.handle_event(&crossterm::event::Event::Key(key));
162 let current_pattern = self.input.value().to_string();
163
164 if self.last_executed_pattern.as_ref() == Some(¤t_pattern) {
166 DebouncedInputAction::Continue
167 } else {
168 self.debouncer.trigger();
169 DebouncedInputAction::InputChanged(current_pattern)
170 }
171 }
172 }
173 }
174
175 pub fn check_debounce(&mut self) -> Option<String> {
178 if self.debouncer.should_execute() {
179 let pattern = self.input.value().to_string();
180 if self.last_executed_pattern.as_ref() == Some(&pattern) {
182 None
183 } else {
184 self.last_executed_pattern = Some(pattern.clone());
185 Some(pattern)
186 }
187 } else {
188 None
189 }
190 }
191
192 pub fn render(&self, f: &mut Frame, area: Rect) {
194 let title = if self.config.show_debounce_indicator && self.debouncer.is_pending() {
195 format!("{} (typing...)", self.config.title)
196 } else {
197 self.config.title.clone()
198 };
199
200 let block = Block::default()
201 .borders(Borders::ALL)
202 .title(title)
203 .border_style(self.config.style);
204
205 let input_widget = Paragraph::new(self.input.value())
206 .block(block)
207 .style(self.config.style);
208
209 f.render_widget(input_widget, area);
210
211 if self.active {
213 f.set_cursor_position((area.x + self.input.cursor() as u16 + 1, area.y + 1));
214 }
215 }
216
217 pub fn set_title(&mut self, title: String) {
219 self.config.title = title;
220 }
221
222 pub fn set_style(&mut self, style: Style) {
224 self.config.style = style;
225 }
226}
227
228pub struct DebouncedInputBuilder {
230 config: DebouncedInputConfig,
231}
232
233impl Default for DebouncedInputBuilder {
234 fn default() -> Self {
235 Self::new()
236 }
237}
238
239impl DebouncedInputBuilder {
240 #[must_use]
241 pub fn new() -> Self {
242 Self {
243 config: DebouncedInputConfig::default(),
244 }
245 }
246
247 #[must_use]
248 pub fn debounce_ms(mut self, ms: u64) -> Self {
249 self.config.debounce_ms = ms;
250 self
251 }
252
253 pub fn title(mut self, title: impl Into<String>) -> Self {
254 self.config.title = title.into();
255 self
256 }
257
258 #[must_use]
259 pub fn style(mut self, style: Style) -> Self {
260 self.config.style = style;
261 self
262 }
263
264 #[must_use]
265 pub fn show_indicator(mut self, show: bool) -> Self {
266 self.config.show_debounce_indicator = show;
267 self
268 }
269
270 #[must_use]
271 pub fn build(self) -> DebouncedInput {
272 DebouncedInput::with_config(self.config)
273 }
274}