From ed93d43cdaadb258dfec808a81a98ff196e32267 Mon Sep 17 00:00:00 2001 From: Cyle Witruk Date: Sat, 10 Aug 2024 15:30:51 +0200 Subject: [PATCH 1/2] add support for windowing in select --- examples/select_window_size.rs | 21 +++++++++++++++++++++ src/select.rs | 24 ++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 examples/select_window_size.rs diff --git a/examples/select_window_size.rs b/examples/select_window_size.rs new file mode 100644 index 0000000..e511c66 --- /dev/null +++ b/examples/select_window_size.rs @@ -0,0 +1,21 @@ +fn main() -> std::io::Result<()> { + let mut items: Vec<(String, String, String)> = Vec::new(); + + for i in 0..20 { + items.push(( + format!("Item {}", i), + i.to_string(), + format!("Hint {}", i), + )); + } + + let selected = cliclack::select("Select an item") + .items(&items) + .window_size(5) + .filter_mode() // Try filtering on "1" + .interact()?; + + cliclack::outro(format!("You selected: {}", selected))?; + + Ok(()) +} \ No newline at end of file diff --git a/src/select.rs b/src/select.rs index 8673dac..2336732 100644 --- a/src/select.rs +++ b/src/select.rs @@ -1,5 +1,5 @@ use std::cell::RefCell; -use std::io; +use std::{io, usize}; use std::{fmt::Display, rc::Rc}; use console::Key; @@ -33,6 +33,8 @@ pub struct Select { cursor: usize, initial_value: Option, filter: FilteredView>, + window_size: usize, + window_pos: usize, } impl Select @@ -47,6 +49,8 @@ where cursor: 0, initial_value: None, filter: FilteredView::default(), + window_size: usize::MAX, + window_pos: 0, } } @@ -82,6 +86,13 @@ where self } + /// Sets the window size. This is the maximum number of items to display + /// at once, triggering scrolling if necessary. + pub fn window_size(mut self, size: usize) -> Self { + self.window_size = size; + self + } + /// Starts the prompt interaction. pub fn interact(&mut self) -> io::Result { if self.items.is_empty() { @@ -118,11 +129,18 @@ impl PromptInteraction for Select { if self.cursor > 0 { self.cursor -= 1; } + if self.cursor < self.window_pos { + self.window_pos = self.cursor; + } } Key::ArrowDown | Key::ArrowRight => { - if !self.filter.items().is_empty() && self.cursor < self.filter.items().len() - 1 { + let filtered_item_count = self.filter.items().len(); + if !self.filter.items().is_empty() && self.cursor < filtered_item_count - 1 { self.cursor += 1; } + if self.cursor >= self.window_pos + self.window_size { + self.window_pos = self.cursor - self.window_size + 1; + } } Key::Enter => { return State::Submit(self.filter.items()[self.cursor].borrow().value.clone()); @@ -153,6 +171,8 @@ impl PromptInteraction for Select { .items() .iter() .enumerate() + .skip(self.window_pos) + .take(self.window_size) .map(|(i, item)| { let item = item.borrow(); theme.format_select_item(&state.into(), self.cursor == i, &item.label, &item.hint) From a5011d24c341630dad4538d8d962b5e5a596143a Mon Sep 17 00:00:00 2001 From: Cyle Witruk Date: Mon, 12 Aug 2024 16:17:11 +0200 Subject: [PATCH 2/2] add support for auto-sizing the select window size based on term size --- Cargo.toml | 1 + examples/select.rs | 23 +++++++++++++++++++++++ src/filter.rs | 9 +++++++++ src/select.rs | 20 ++++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 examples/select.rs diff --git a/Cargo.toml b/Cargo.toml index 0ce9105..0a89c95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ console = "0.15.8" indicatif = "0.17.8" once_cell = "1.18.0" strsim = "0.11.1" +termsize = "0.1.9" textwrap = "0.16.0" zeroize = {version = "1.6.0", features = ["derive"]} diff --git a/examples/select.rs b/examples/select.rs new file mode 100644 index 0000000..9fda336 --- /dev/null +++ b/examples/select.rs @@ -0,0 +1,23 @@ +fn main() -> std::io::Result<()> { + let mut items: Vec<(String, String, String)> = Vec::new(); + + for i in 0..20 { + items.push(( + format!("Item {}", i), + i.to_string(), + format!("Hint {}", i), + )); + } + + // Try this example with a terminal height both less than and greater than 10 + // to see the automatic window-size adjustment. + let selected = cliclack::select("Select an item") + .items(&items) + .window_size(10) // Specify a custom window-size + .filter_mode() // Try filtering on "1" + .interact()?; + + cliclack::outro(format!("You selected: {}", selected))?; + + Ok(()) +} \ No newline at end of file diff --git a/src/filter.rs b/src/filter.rs index b61d8ed..f02aaf5 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -25,18 +25,27 @@ impl Default for FilteredView { } impl FilteredView { + /// Enable filtering of the items in the view. pub fn enable(&mut self) { self.enabled = true; } + /// Set the items to be filtered. pub fn set(&mut self, items: Vec>>) { self.items = items; } + /// Return the filtered items in the view. pub fn items(&self) -> &[Rc>] { &self.items } + /// Get whether or not filtering is enabled. + pub fn is_enabled(&self) -> bool { + self.enabled + } + + /// Handle the key event and update the state of the view. pub fn on(&mut self, key: &Key, all_items: Vec>>) -> Option> { if !self.enabled { // Pass over the control. diff --git a/src/select.rs b/src/select.rs index 2336732..3ee94f6 100644 --- a/src/select.rs +++ b/src/select.rs @@ -3,6 +3,7 @@ use std::{io, usize}; use std::{fmt::Display, rc::Rc}; use console::Key; +use termsize::Size; use crate::{ filter::{FilteredView, LabeledItem}, @@ -35,6 +36,7 @@ pub struct Select { filter: FilteredView>, window_size: usize, window_pos: usize, + term_size: Option, } impl Select @@ -51,6 +53,7 @@ where filter: FilteredView::default(), window_size: usize::MAX, window_pos: 0, + term_size: termsize::get(), } } @@ -101,6 +104,23 @@ where "No items added to the list", )); } + + // If the window size hasn't been specified manually, calculate it + // based on the current size of the terminal. + if let Some(size) = &self.term_size { + // Determine the optimal maximum height of the window. + let mut max_height = size.rows as usize - 3; + if self.filter.is_enabled() { + max_height -= 1; + } + + // If the window size is not set or exceeds the maximum optimal height, + // use the optimal height instead. + if self.window_size == usize::MAX || self.window_size > max_height { + self.window_size = max_height; + } + } + if let Some(initial_value) = &self.initial_value { self.cursor = self .items