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 DebouncedInput {
72 pub fn new() -> Self {
74 Self::with_config(DebouncedInputConfig::default())
75 }
76
77 pub fn with_config(config: DebouncedInputConfig) -> Self {
79 Self {
80 input: Input::default(),
81 debouncer: Debouncer::new(config.debounce_ms),
82 last_executed_pattern: None,
83 config,
84 active: false,
85 }
86 }
87
88 pub fn activate(&mut self) {
90 self.active = true;
91 self.input.reset();
92 self.debouncer.reset();
93 self.last_executed_pattern = None;
94 }
95
96 pub fn deactivate(&mut self) {
98 self.active = false;
99 self.debouncer.reset();
100 }
101
102 pub fn is_active(&self) -> bool {
104 self.active
105 }
106
107 pub fn value(&self) -> &str {
109 self.input.value()
110 }
111
112 pub fn set_value(&mut self, value: String) {
114 self.input = Input::default().with_value(value);
115 }
116
117 pub fn cursor(&self) -> usize {
119 self.input.cursor()
120 }
121
122 pub fn set_config(&mut self, config: DebouncedInputConfig) {
124 self.debouncer = Debouncer::new(config.debounce_ms);
125 self.config = config;
126 }
127
128 pub fn handle_key(&mut self, key: KeyEvent) -> DebouncedInputAction {
130 if !self.active {
131 return DebouncedInputAction::PassThrough;
132 }
133
134 match key.code {
135 KeyCode::Esc => {
136 self.deactivate();
137 DebouncedInputAction::Cancel
138 }
139 KeyCode::Enter => {
140 let pattern = self.input.value().to_string();
141 self.deactivate();
142 DebouncedInputAction::Confirm(pattern)
143 }
144 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
145 DebouncedInputAction::PassThrough
147 }
148 _ => {
149 self.input.handle_event(&crossterm::event::Event::Key(key));
151 let current_pattern = self.input.value().to_string();
152
153 if self.last_executed_pattern.as_ref() != Some(¤t_pattern) {
155 self.debouncer.trigger();
156 DebouncedInputAction::InputChanged(current_pattern)
157 } else {
158 DebouncedInputAction::Continue
159 }
160 }
161 }
162 }
163
164 pub fn check_debounce(&mut self) -> Option<String> {
167 if self.debouncer.should_execute() {
168 let pattern = self.input.value().to_string();
169 if self.last_executed_pattern.as_ref() != Some(&pattern) {
171 self.last_executed_pattern = Some(pattern.clone());
172 Some(pattern)
173 } else {
174 None
175 }
176 } else {
177 None
178 }
179 }
180
181 pub fn render(&self, f: &mut Frame, area: Rect) {
183 let title = if self.config.show_debounce_indicator && self.debouncer.is_pending() {
184 format!("{} (typing...)", self.config.title)
185 } else {
186 self.config.title.clone()
187 };
188
189 let block = Block::default()
190 .borders(Borders::ALL)
191 .title(title)
192 .border_style(self.config.style);
193
194 let input_widget = Paragraph::new(self.input.value())
195 .block(block)
196 .style(self.config.style);
197
198 f.render_widget(input_widget, area);
199
200 if self.active {
202 f.set_cursor_position((area.x + self.input.cursor() as u16 + 1, area.y + 1));
203 }
204 }
205
206 pub fn set_title(&mut self, title: String) {
208 self.config.title = title;
209 }
210
211 pub fn set_style(&mut self, style: Style) {
213 self.config.style = style;
214 }
215}
216
217pub struct DebouncedInputBuilder {
219 config: DebouncedInputConfig,
220}
221
222impl DebouncedInputBuilder {
223 pub fn new() -> Self {
224 Self {
225 config: DebouncedInputConfig::default(),
226 }
227 }
228
229 pub fn debounce_ms(mut self, ms: u64) -> Self {
230 self.config.debounce_ms = ms;
231 self
232 }
233
234 pub fn title(mut self, title: impl Into<String>) -> Self {
235 self.config.title = title.into();
236 self
237 }
238
239 pub fn style(mut self, style: Style) -> Self {
240 self.config.style = style;
241 self
242 }
243
244 pub fn show_indicator(mut self, show: bool) -> Self {
245 self.config.show_debounce_indicator = show;
246 self
247 }
248
249 pub fn build(self) -> DebouncedInput {
250 DebouncedInput::with_config(self.config)
251 }
252}