#[cfg(feature = "tui-example")]
use quickleaf::{Cache, Filter, ListProps, Order};
#[cfg(feature = "tui-example")]
use std::time::Duration;
#[cfg(feature = "tui-example")]
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph},
Frame, Terminal,
};
#[cfg(feature = "tui-example")]
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
#[cfg(feature = "tui-example")]
use std::io;
#[cfg(feature = "tui-example")]
#[derive(Debug, Clone)]
enum MenuItem {
Insert,
InsertWithTTL,
Get,
Remove,
List,
ListPaginated,
Filter,
Clear,
CleanupExpired,
Stats,
Exit,
}
#[cfg(feature = "tui-example")]
impl MenuItem {
fn all() -> Vec<MenuItem> {
vec![
MenuItem::Insert,
MenuItem::InsertWithTTL,
MenuItem::Get,
MenuItem::Remove,
MenuItem::List,
MenuItem::ListPaginated,
MenuItem::Filter,
MenuItem::Clear,
MenuItem::CleanupExpired,
MenuItem::Stats,
MenuItem::Exit,
]
}
fn name(&self) -> &str {
match self {
MenuItem::Insert => "๐ Insert Key-Value",
MenuItem::InsertWithTTL => "โฐ Insert with TTL",
MenuItem::Get => "๐ Get Value",
MenuItem::Remove => "๐๏ธ Remove Key",
MenuItem::List => "๐ List All Items",
MenuItem::ListPaginated => "๐ List (Paginated)",
MenuItem::Filter => "๐ Filter Items",
MenuItem::Clear => "๐งน Clear Cache",
MenuItem::CleanupExpired => "โป๏ธ Cleanup Expired",
MenuItem::Stats => "๐ Cache Statistics",
MenuItem::Exit => "๐ช Exit",
}
}
fn description(&self) -> &str {
match self {
MenuItem::Insert => "Insert a new key-value pair into the cache",
MenuItem::InsertWithTTL => "Insert a key-value pair with Time To Live",
MenuItem::Get => "Retrieve a value by its key",
MenuItem::Remove => "Remove a key-value pair from the cache",
MenuItem::List => "List all items in the cache",
MenuItem::ListPaginated => "List items with pagination (limit and start_after_key)",
MenuItem::Filter => "Filter items by prefix, suffix, or pattern",
MenuItem::Clear => "Clear all items from the cache",
MenuItem::CleanupExpired => "Remove all expired items from the cache",
MenuItem::Stats => "View cache statistics and information",
MenuItem::Exit => "Exit the application",
}
}
}
#[cfg(feature = "tui-example")]
struct App {
cache: Cache,
selected_menu: usize,
input_mode: bool,
input_buffer: String,
second_input_buffer: String,
third_input_buffer: String,
messages: Vec<String>,
current_action: Option<MenuItem>,
input_stage: usize, }
#[cfg(feature = "tui-example")]
impl App {
fn new() -> Result<Self, Box<dyn std::error::Error>> {
let db_path = std::env::var("QUICKLEAF_DB_PATH")
.unwrap_or_else(|_| "./quickleaf_tui_cache.db".to_string());
println!("Using cache database at: {}", db_path);
let cache = Cache::with_persist(&db_path, 1000)?;
Ok(Self {
cache,
selected_menu: 0,
input_mode: false,
input_buffer: String::new(),
second_input_buffer: String::new(),
third_input_buffer: String::new(),
messages: vec!["Welcome to Quickleaf Interactive TUI! ๐".to_string()],
current_action: None,
input_stage: 0,
})
}
fn add_message(&mut self, msg: String) {
self.messages.push(msg);
if self.messages.len() > 10 {
self.messages.remove(0);
}
}
fn execute_action(&mut self) {
match self.current_action.as_ref() {
Some(MenuItem::Insert) => {
if self.input_stage == 0 {
self.input_stage = 1;
self.add_message("Enter value:".to_string());
} else {
let key = self.input_buffer.clone();
let value = self.second_input_buffer.clone();
self.cache.insert(&key, value.as_str());
self.add_message(format!("โ
Inserted: {} = {}", key, value));
self.reset_input();
}
}
Some(MenuItem::InsertWithTTL) => match self.input_stage {
0 => {
self.input_stage = 1;
self.add_message("Enter value:".to_string());
}
1 => {
self.input_stage = 2;
self.add_message("Enter TTL in seconds:".to_string());
}
2 => {
let key = self.input_buffer.clone();
let value = self.second_input_buffer.clone();
if let Ok(ttl_secs) = self.third_input_buffer.parse::<u64>() {
self.cache.insert_with_ttl(
&key,
value.as_str(),
Duration::from_secs(ttl_secs),
);
self.add_message(format!(
"โ
Inserted with TTL: {} = {} ({}s)",
key, value, ttl_secs
));
} else {
self.add_message("โ Invalid TTL value".to_string());
}
self.reset_input();
}
_ => {}
},
Some(MenuItem::Get) => {
let key = self.input_buffer.clone();
let value_opt = self.cache.get(&key).cloned();
match value_opt {
Some(value) => {
self.add_message(format!("โ
Found: {} = {:?}", key, value));
}
None => {
self.add_message(format!("โ Key not found: {}", key));
}
}
self.reset_input();
}
Some(MenuItem::Remove) => {
let key = self.input_buffer.clone();
match self.cache.remove(&key) {
Ok(_) => {
self.add_message(format!("โ
Removed: {}", key));
}
Err(_) => {
self.add_message(format!("โ Failed to remove: {}", key));
}
}
self.reset_input();
}
Some(MenuItem::List) => {
let items = self
.cache
.list(ListProps::default().order(Order::Asc))
.unwrap_or_default()
.into_iter()
.map(|(k, v)| (k, v.clone()))
.collect::<Vec<_>>();
if items.is_empty() {
self.add_message("๐ Cache is empty".to_string());
} else {
self.add_message(format!("๐ Cache contains {} items:", items.len()));
for (key, value) in items.iter().take(5) {
self.add_message(format!(" โข {} = {:?}", key, value));
}
if items.len() > 5 {
self.add_message(format!(" ... and {} more items", items.len() - 5));
}
}
self.reset_input();
}
Some(MenuItem::ListPaginated) => match self.input_stage {
0 => {
self.input_stage = 1;
self.add_message("Enter start_after_key (or press Enter to skip):".to_string());
}
1 => {
let limit = self.input_buffer.parse::<usize>().unwrap_or(10);
let start_after = if self.second_input_buffer.is_empty() {
None
} else {
Some(self.second_input_buffer.clone())
};
let mut props = ListProps::default()
.order(Order::Asc)
.limit(limit);
if let Some(key) = start_after {
props = props.start_after_key(&key);
}
match self.cache.list(props) {
Ok(items) => {
let items: Vec<_> = items.into_iter()
.map(|(k, v)| (k, v.clone()))
.collect();
if items.is_empty() {
self.add_message("๐ No items found with given pagination".to_string());
} else {
self.add_message(format!("๐ Showing {} items (limit: {}):", items.len(), limit));
for (key, value) in &items {
self.add_message(format!(" โข {} = {:?}", key, value));
}
if !items.is_empty() {
let last_key = &items.last().unwrap().0;
self.add_message(format!("๐ก To continue, use start_after_key: '{}'", last_key));
}
}
}
Err(e) => {
self.add_message(format!("โ Error: {:?}", e));
}
}
self.reset_input();
}
_ => {}
},
Some(MenuItem::Filter) => {
let prefix = self.input_buffer.clone();
let items = self
.cache
.list(
ListProps::default()
.filter(Filter::StartWith(prefix.clone()))
.order(Order::Asc),
)
.unwrap_or_default()
.into_iter()
.map(|(k, v)| (k, v.clone()))
.collect::<Vec<_>>();
if items.is_empty() {
self.add_message(format!("๐ No items found with prefix: {}", prefix));
} else {
self.add_message(format!(
"๐ Found {} items with prefix '{}':",
items.len(),
prefix
));
for (key, value) in items.iter().take(5) {
self.add_message(format!(" โข {} = {:?}", key, value));
}
}
self.reset_input();
}
Some(MenuItem::Clear) => {
self.cache.clear();
self.add_message("๐งน Cache cleared!".to_string());
self.reset_input();
}
Some(MenuItem::CleanupExpired) => {
let removed = self.cache.cleanup_expired();
self.add_message(format!("โป๏ธ Cleaned up {} expired items", removed));
self.reset_input();
}
Some(MenuItem::Stats) => {
let len = self.cache.len();
let capacity = self.cache.capacity();
self.add_message(format!("๐ Cache Statistics:"));
self.add_message(format!(" โข Items: {}/{}", len, capacity));
self.add_message(format!(" โข Capacity: {}", capacity));
self.add_message(format!(
" โข Usage: {:.1}%",
(len as f64 / capacity as f64) * 100.0
));
self.add_message(format!(" โข Persistence: tui_cache.db (SQLite)"));
self.reset_input();
}
_ => {}
}
}
fn reset_input(&mut self) {
self.input_mode = false;
self.input_buffer.clear();
self.second_input_buffer.clear();
self.third_input_buffer.clear();
self.current_action = None;
self.input_stage = 0;
}
fn get_input_prompt(&self) -> String {
match (&self.current_action, self.input_stage) {
(Some(MenuItem::Insert), 0) => "Enter key: ".to_string(),
(Some(MenuItem::Insert), 1) => "Enter value: ".to_string(),
(Some(MenuItem::InsertWithTTL), 0) => "Enter key: ".to_string(),
(Some(MenuItem::InsertWithTTL), 1) => "Enter value: ".to_string(),
(Some(MenuItem::InsertWithTTL), 2) => "Enter TTL (seconds): ".to_string(),
(Some(MenuItem::Get), _) => "Enter key to get: ".to_string(),
(Some(MenuItem::Remove), _) => "Enter key to remove: ".to_string(),
(Some(MenuItem::Filter), _) => "Enter prefix to filter: ".to_string(),
(Some(MenuItem::ListPaginated), 0) => "Enter limit (default 10): ".to_string(),
(Some(MenuItem::ListPaginated), 1) => "Enter start_after_key (press Enter to skip): ".to_string(),
_ => "Input: ".to_string(),
}
}
fn get_current_input(&self) -> &str {
match self.input_stage {
0 => &self.input_buffer,
1 => &self.second_input_buffer,
2 => &self.third_input_buffer,
_ => &self.input_buffer,
}
}
fn append_to_current_input(&mut self, c: char) {
match self.input_stage {
0 => self.input_buffer.push(c),
1 => self.second_input_buffer.push(c),
2 => self.third_input_buffer.push(c),
_ => {}
}
}
fn pop_from_current_input(&mut self) {
match self.input_stage {
0 => {
self.input_buffer.pop();
}
1 => {
self.second_input_buffer.pop();
}
2 => {
self.third_input_buffer.pop();
}
_ => {}
}
}
}
#[cfg(feature = "tui-example")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut app = match App::new() {
Ok(app) => app,
Err(e) => {
eprintln!("Failed to initialize application: {}", e);
eprintln!("Make sure the database path is writable.");
eprintln!(
"You can set QUICKLEAF_DB_PATH environment variable to use a different path."
);
return Err(e);
}
};
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let res = run_app(&mut terminal, &mut app);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("Error: {:?}", err);
}
Ok(())
}
#[cfg(feature = "tui-example")]
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
if app.input_mode {
match key.code {
KeyCode::Enter => {
app.execute_action();
}
KeyCode::Char(c) => {
app.append_to_current_input(c);
}
KeyCode::Backspace => {
app.pop_from_current_input();
}
KeyCode::Esc => {
app.reset_input();
}
_ => {}
}
} else {
match key.code {
KeyCode::Char('q') => {
return Ok(());
}
KeyCode::Up => {
if app.selected_menu > 0 {
app.selected_menu -= 1;
}
}
KeyCode::Down => {
let menu_items = MenuItem::all();
if app.selected_menu < menu_items.len() - 1 {
app.selected_menu += 1;
}
}
KeyCode::Enter => {
let menu_items = MenuItem::all();
let selected = &menu_items[app.selected_menu];
match selected {
MenuItem::Exit => {
return Ok(());
}
MenuItem::List
| MenuItem::Clear
| MenuItem::CleanupExpired
| MenuItem::Stats => {
app.current_action = Some(selected.clone());
app.execute_action();
}
_ => {
app.input_mode = true;
app.current_action = Some(selected.clone());
app.input_stage = 0;
}
}
}
_ => {}
}
}
}
}
}
}
#[cfg(feature = "tui-example")]
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.split(f.area());
let menu_items = MenuItem::all();
let items: Vec<ListItem> = menu_items
.iter()
.enumerate()
.map(|(i, item)| {
let style = if i == app.selected_menu {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
ListItem::new(item.name()).style(style)
})
.collect();
let menu = List::new(items).block(
Block::default()
.title(" ๐ Quickleaf Menu ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Green)),
);
f.render_widget(menu, chunks[0]);
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(4), Constraint::Min(10), Constraint::Length(3), ])
.split(chunks[1]);
let selected_item = &menu_items[app.selected_menu];
let description = Paragraph::new(selected_item.description())
.block(
Block::default()
.title(" Description ")
.borders(Borders::ALL),
)
.style(Style::default().fg(Color::White))
.alignment(Alignment::Left);
f.render_widget(description, right_chunks[0]);
let messages: Vec<ListItem> = app
.messages
.iter()
.map(|msg| ListItem::new(msg.as_str()))
.collect();
let messages_list = List::new(messages)
.block(Block::default().title(" Output ").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow));
f.render_widget(messages_list, right_chunks[1]);
if app.input_mode {
let input_text = format!("{}{}", app.get_input_prompt(), app.get_current_input());
let input = Paragraph::new(input_text)
.block(
Block::default()
.title(" Input (ESC to cancel, ENTER to submit) ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)),
)
.style(Style::default().fg(Color::White));
f.render_widget(Clear, right_chunks[2]);
f.render_widget(input, right_chunks[2]);
} else {
let help = Paragraph::new("โ/โ: Navigate | Enter: Select | q: Quit")
.block(Block::default().title(" Help ").borders(Borders::ALL))
.style(Style::default().fg(Color::Gray))
.alignment(Alignment::Center);
f.render_widget(help, right_chunks[2]);
}
}
#[cfg(not(feature = "tui-example"))]
fn main() {
println!("โ This example requires the 'tui-example' feature to be enabled.");
println!(" Run with: cargo run --example tui_interactive --features tui-example");
}