use crate::utils::debouncer::Debouncer;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{
layout::Rect,
style::{Color, Style},
widgets::{Block, Borders, Paragraph},
Frame,
};
use tui_input::{backend::crossterm::EventHandler, Input};
#[derive(Debug, Clone)]
pub enum DebouncedInputAction {
Continue,
InputChanged(String),
ExecuteDebounced(String),
Confirm(String),
Cancel,
PassThrough,
}
#[derive(Debug, Clone)]
pub struct DebouncedInputConfig {
pub debounce_ms: u64,
pub title: String,
pub style: Style,
pub show_debounce_indicator: bool,
}
impl Default for DebouncedInputConfig {
fn default() -> Self {
Self {
debounce_ms: 300,
title: "Search".to_string(),
style: Style::default().fg(Color::Yellow),
show_debounce_indicator: true,
}
}
}
pub struct DebouncedInput {
input: Input,
debouncer: Debouncer,
last_executed_pattern: Option<String>,
config: DebouncedInputConfig,
active: bool,
}
impl Default for DebouncedInput {
fn default() -> Self {
Self::new()
}
}
impl DebouncedInput {
#[must_use]
pub fn new() -> Self {
Self::with_config(DebouncedInputConfig::default())
}
#[must_use]
pub fn with_config(config: DebouncedInputConfig) -> Self {
Self {
input: Input::default(),
debouncer: Debouncer::new(config.debounce_ms),
last_executed_pattern: None,
config,
active: false,
}
}
pub fn activate(&mut self) {
self.active = true;
self.input.reset();
self.debouncer.reset();
self.last_executed_pattern = None;
}
pub fn deactivate(&mut self) {
self.active = false;
self.debouncer.reset();
}
#[must_use]
pub fn is_active(&self) -> bool {
self.active
}
#[must_use]
pub fn value(&self) -> &str {
self.input.value()
}
pub fn set_value(&mut self, value: String) {
self.input = Input::default().with_value(value);
}
#[must_use]
pub fn cursor(&self) -> usize {
self.input.cursor()
}
pub fn set_config(&mut self, config: DebouncedInputConfig) {
self.debouncer = Debouncer::new(config.debounce_ms);
self.config = config;
}
pub fn handle_key(&mut self, key: KeyEvent) -> DebouncedInputAction {
if !self.active {
return DebouncedInputAction::PassThrough;
}
match key.code {
KeyCode::Esc => {
self.deactivate();
DebouncedInputAction::Cancel
}
KeyCode::Enter => {
let pattern = self.input.value().to_string();
self.deactivate();
DebouncedInputAction::Confirm(pattern)
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
DebouncedInputAction::PassThrough
}
_ => {
self.input.handle_event(&crossterm::event::Event::Key(key));
let current_pattern = self.input.value().to_string();
if self.last_executed_pattern.as_ref() == Some(¤t_pattern) {
DebouncedInputAction::Continue
} else {
self.debouncer.trigger();
DebouncedInputAction::InputChanged(current_pattern)
}
}
}
}
pub fn check_debounce(&mut self) -> Option<String> {
if self.debouncer.should_execute() {
let pattern = self.input.value().to_string();
if self.last_executed_pattern.as_ref() == Some(&pattern) {
None
} else {
self.last_executed_pattern = Some(pattern.clone());
Some(pattern)
}
} else {
None
}
}
pub fn render(&self, f: &mut Frame, area: Rect) {
let title = if self.config.show_debounce_indicator && self.debouncer.is_pending() {
format!("{} (typing...)", self.config.title)
} else {
self.config.title.clone()
};
let block = Block::default()
.borders(Borders::ALL)
.title(title)
.border_style(self.config.style);
let input_widget = Paragraph::new(self.input.value())
.block(block)
.style(self.config.style);
f.render_widget(input_widget, area);
if self.active {
f.set_cursor_position((area.x + self.input.cursor() as u16 + 1, area.y + 1));
}
}
pub fn set_title(&mut self, title: String) {
self.config.title = title;
}
pub fn set_style(&mut self, style: Style) {
self.config.style = style;
}
}
pub struct DebouncedInputBuilder {
config: DebouncedInputConfig,
}
impl Default for DebouncedInputBuilder {
fn default() -> Self {
Self::new()
}
}
impl DebouncedInputBuilder {
#[must_use]
pub fn new() -> Self {
Self {
config: DebouncedInputConfig::default(),
}
}
#[must_use]
pub fn debounce_ms(mut self, ms: u64) -> Self {
self.config.debounce_ms = ms;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.config.title = title.into();
self
}
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.config.style = style;
self
}
#[must_use]
pub fn show_indicator(mut self, show: bool) -> Self {
self.config.show_debounce_indicator = show;
self
}
#[must_use]
pub fn build(self) -> DebouncedInput {
DebouncedInput::with_config(self.config)
}
}