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/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/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 8673dac..3ee94f6 100644 --- a/src/select.rs +++ b/src/select.rs @@ -1,8 +1,9 @@ use std::cell::RefCell; -use std::io; +use std::{io, usize}; use std::{fmt::Display, rc::Rc}; use console::Key; +use termsize::Size; use crate::{ filter::{FilteredView, LabeledItem}, @@ -33,6 +34,9 @@ pub struct Select { cursor: usize, initial_value: Option, filter: FilteredView>, + window_size: usize, + window_pos: usize, + term_size: Option, } impl Select @@ -47,6 +51,9 @@ where cursor: 0, initial_value: None, filter: FilteredView::default(), + window_size: usize::MAX, + window_pos: 0, + term_size: termsize::get(), } } @@ -82,6 +89,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() { @@ -90,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 @@ -118,11 +149,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 +191,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)