diff --git a/README.md b/README.md index 3712ef1b..d50fab57 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Put the package in your `Cargo.toml`. ```toml [dependencies] -promkit = "0.3.4" +promkit = "0.3.5" ``` ## Features diff --git a/promkit/Cargo.toml b/promkit/Cargo.toml index 46b77b34..158d3a51 100644 --- a/promkit/Cargo.toml +++ b/promkit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "promkit" -version = "0.3.4" +version = "0.3.5" authors = ["ynqa "] edition = "2021" description = "A toolkit for building your own interactive command-line tools" diff --git a/promkit/src/engine.rs b/promkit/src/engine.rs deleted file mode 100644 index 03921a55..00000000 --- a/promkit/src/engine.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::{fmt, io::Write}; - -use crate::{ - crossterm::{ - cursor::{self, MoveTo}, - execute, queue, - style::Print, - terminal::{self, Clear, ClearType, ScrollUp}, - }, - error::{Error, Result}, -}; - -/// Provides functionality for executing terminal commands -/// and managing terminal state. -/// -/// The `Engine` struct provides methods for interacting -/// with the terminal, such as moving the cursor, -/// clearing the screen, writing text, and more. -#[derive(Clone)] -pub struct Engine { - out: W, -} - -impl Engine { - /// Constructs a new `Engine` with the specified output writer. - /// - /// # Arguments - /// - /// * `out` - The output writer where terminal commands will be executed. - pub fn new(out: W) -> Self { - Self { out } - } - - /// Retrieves the current position of the cursor in the terminal. - /// - /// # Returns - /// - /// Returns a `Result` containing the current cursor position as `(u16, u16)`, - /// or an `Error` if unable to retrieve the position. - pub fn position(&self) -> Result<(u16, u16)> { - cursor::position().map_err(Error::from) - } - - /// Retrieves the size of the terminal. - /// - /// # Returns - /// - /// Returns a `Result` containing the terminal size as `(u16, u16)`, - /// or an `Error` if unable to retrieve the size. - pub fn size(&self) -> Result<(u16, u16)> { - terminal::size().map_err(Error::from) - } - - /// Clears the terminal screen from the cursor's current position downwards. - /// - /// # Returns - /// - /// Returns a `Result` indicating the success of the operation, - /// or an `Error` if it fails. - pub fn clear_from_cursor_down(&mut self) -> Result { - queue!(self.out, Clear(ClearType::FromCursorDown)).map_err(Error::from) - } - - pub fn clear_current_line(&mut self) -> Result { - queue!(self.out, Clear(ClearType::CurrentLine)).map_err(Error::from) - } - - /// Writes a string to the terminal. - /// - /// # Arguments - /// - /// * `string` - The string to be written. - /// - /// # Returns - /// - /// Returns a `Result` indicating the success of the operation, - /// or an `Error` if it fails. - pub fn write(&mut self, string: D) -> Result { - execute!(self.out, Print(string)).map_err(Error::from) - } - - /// Moves the cursor to the specified position in the terminal. - /// - /// # Arguments - /// - /// * `pos` - The target position as `(u16, u16)`. - /// - /// # Returns - /// - /// Returns a `Result` indicating the success of the operation, - /// or an `Error` if it fails. - pub fn move_to(&mut self, pos: (u16, u16)) -> Result { - queue!(self.out, MoveTo(pos.0, pos.1)).map_err(Error::from) - } - - /// Scrolls the terminal content up by a specified number of lines. - /// - /// # Arguments - /// - /// * `times` - The number of lines to scroll up. - /// - /// # Returns - /// - /// Returns a `Result` indicating the success of the operation, - /// or an `Error` if it fails. - pub fn scroll_up(&mut self, times: u16) -> Result { - queue!(self.out, ScrollUp(times)).map_err(Error::from) - } - - /// Moves the cursor to the next line in the terminal. - /// - /// # Returns - /// - /// Returns a `Result` indicating the success of the operation, - /// or an `Error` if it fails. - pub fn move_to_next_line(&mut self) -> Result { - queue!(self.out, cursor::MoveToNextLine(1)).map_err(Error::from) - } - - pub fn move_to_prev_line(&mut self, times: u16) -> Result { - queue!(self.out, cursor::MoveToPreviousLine(times)).map_err(Error::from) - } -} - -#[cfg(test)] -mod test { - mod write { - use super::super::*; - - #[test] - fn test() { - let out = vec![]; - let mut engine = Engine::new(out); - assert!(engine.write("abcde").is_ok()); - assert_eq!( - String::from_utf8(strip_ansi_escapes::strip(engine.out)).unwrap(), - "abcde" - ); - } - } -} diff --git a/promkit/src/lib.rs b/promkit/src/lib.rs index 216befd4..627b5c70 100644 --- a/promkit/src/lib.rs +++ b/promkit/src/lib.rs @@ -11,7 +11,7 @@ //! //! ```toml //! [dependencies] -//! promkit = "0.3.4" +//! promkit = "0.3.5" //! ``` //! //! ## Features @@ -19,13 +19,13 @@ //! - Support cross-platform both UNIX and Windows owing to [crossterm](https://github.com/crossterm-rs/crossterm) //! - Various building methods //! - Preset; Support for quickly setting up a UI by providing simple parameters. -//! - [Readline](https://github.com/ynqa/promkit/tree/v0.3.4#readline) -//! - [Confirm](https://github.com/ynqa/promkit/tree/v0.3.4#confirm) -//! - [Password](https://github.com/ynqa/promkit/tree/v0.3.4#password) -//! - [Select](https://github.com/ynqa/promkit/tree/v0.3.4#select) -//! - [QuerySelect](https://github.com/ynqa/promkit/tree/v0.3.4#queryselect) -//! - [Checkbox](https://github.com/ynqa/promkit/tree/v0.3.4#checkbox) -//! - [Tree](https://github.com/ynqa/promkit/tree/v0.3.4#tree) +//! - [Readline](https://github.com/ynqa/promkit/tree/v0.3.5#readline) +//! - [Confirm](https://github.com/ynqa/promkit/tree/v0.3.5#confirm) +//! - [Password](https://github.com/ynqa/promkit/tree/v0.3.5#password) +//! - [Select](https://github.com/ynqa/promkit/tree/v0.3.5#select) +//! - [QuerySelect](https://github.com/ynqa/promkit/tree/v0.3.5#queryselect) +//! - [Checkbox](https://github.com/ynqa/promkit/tree/v0.3.5#checkbox) +//! - [Tree](https://github.com/ynqa/promkit/tree/v0.3.5#tree) //! - Combining various UI components. //! - They are provided with the same interface, allowing users to choose and //! assemble them according to their preferences. @@ -39,7 +39,7 @@ //! //! ## Examples/Demos //! -//! See [here](https://github.com/ynqa/promkit/tree/v0.3.4#examplesdemos) +//! See [here](https://github.com/ynqa/promkit/tree/v0.3.5#examplesdemos) //! //! ## Why *promkit*? //! @@ -97,7 +97,6 @@ pub use serde_json; mod core; pub use core::*; -pub mod engine; mod error; pub use error::{Error, Result}; pub mod grapheme; @@ -120,7 +119,6 @@ use crate::{ execute, terminal::{disable_raw_mode, enable_raw_mode}, }, - engine::Engine, pane::Pane, terminal::Terminal, }; @@ -210,9 +208,13 @@ pub struct Prompt { impl Drop for Prompt { fn drop(&mut self) { - execute!(io::stdout(), cursor::MoveToNextLine(1)).ok(); - execute!(io::stdout(), cursor::Show).ok(); - execute!(io::stdout(), event::DisableMouseCapture).ok(); + execute!( + io::stdout(), + cursor::Show, + event::DisableMouseCapture, + cursor::MoveToNextLine(1), + ) + .ok(); disable_raw_mode().ok(); } } @@ -251,25 +253,34 @@ impl Prompt { /// /// Returns a `Result` containing the produced result or an error. pub fn run(&mut self) -> Result { - let mut engine = Engine::new(io::stdout()); - enable_raw_mode()?; execute!(io::stdout(), cursor::Hide)?; - let size = engine.size()?; + let size = crossterm::terminal::size()?; let panes = self.renderer.create_panes(size.0); - let mut terminal = Terminal::start_session(&mut engine, &panes)?; - terminal.draw(&mut engine, panes)?; + let mut terminal = Terminal::start_session(&panes)?; + terminal.draw(&panes)?; loop { let ev = event::read()?; - if (self.evaluator)(&ev, &mut self.renderer)? == PromptSignal::Quit { - break; + match &ev { + Event::Resize(_, _) => { + terminal.position = (0, 0); + crossterm::execute!( + io::stdout(), + crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge), + )?; + } + _ => { + if (self.evaluator)(&ev, &mut self.renderer)? == PromptSignal::Quit { + break; + } + } } - let size = engine.size()?; - terminal.draw(&mut engine, self.renderer.create_panes(size.0))?; + let size = crossterm::terminal::size()?; + terminal.draw(&self.renderer.create_panes(size.0))?; } (self.producer)(&*self.renderer) diff --git a/promkit/src/terminal.rs b/promkit/src/terminal.rs index 6439f12e..37c5af65 100644 --- a/promkit/src/terminal.rs +++ b/promkit/src/terminal.rs @@ -1,40 +1,33 @@ -use std::io::Write; +use std::io::{self, Write}; -use crate::{engine::Engine, error::Result, pane::Pane}; +use crate::{ + crossterm::{cursor, style, terminal}, + error::Result, + pane::Pane, + Error, +}; -/// Represents a terminal session, -/// managing the display of panes within the terminal window. pub struct Terminal { /// The current cursor position within the terminal. - position: (u16, u16), + pub position: (u16, u16), } impl Terminal { - /// Starts a new terminal session, initializing the cursor position. - /// - /// # Arguments - /// - /// * `engine` - A mutable reference to the Engine, - /// which manages terminal operations. - /// - /// # Returns - /// - /// A result containing the new Terminal instance or an error. - pub fn start_session(engine: &mut Engine, panes: &[Pane]) -> Result { - let position = engine.position()?; - let size = engine.size()?; + pub fn start_session(panes: &[Pane]) -> Result { + let position = cursor::position()?; + let size = terminal::size()?; // If the cursor is not at the beginning of a line (position.0 != 0), // there are two scenarios to consider: // 1. If the cursor is also at the last line of the terminal (size.1 == position.1 + 1), - // the terminal is scrolled up by one line to make room (engine.scroll_up(1)?;). + // the terminal is scrolled up by one line to make room. // 2. Regardless of whether a scroll occurred, move the cursor to the beginning of the next line - // to ensure the next output starts correctly (engine.move_to_next_line()?;). + // to ensure the next output starts correctly. if position.0 != 0 { if size.1 == position.1 + 1 { - engine.scroll_up(1)?; + crossterm::queue!(io::stdout(), terminal::ScrollUp(1))?; } - engine.move_to_next_line()?; + crossterm::queue!(io::stdout(), cursor::MoveToNextLine(1))?; } // Calculate the total number of rows required by all panes. @@ -47,70 +40,68 @@ impl Terminal { // and then move the cursor up by the same number of lines // to maintain its relative position. if size.1 == position.1 + 1 { - engine.scroll_up(lines as u16)?; - engine.move_to_prev_line(lines as u16)?; + crossterm::queue!( + io::stdout(), + terminal::ScrollUp(lines as u16), + cursor::MoveToPreviousLine(lines as u16), + )?; } + io::stdout().flush()?; + Ok(Self { - position: engine.position()?, + position: cursor::position()?, }) } - /// Draws the provided panes within the terminal window. - /// - /// # Arguments - /// - /// * `engine` - A mutable reference to the Engine, - /// which manages terminal operations. - /// * `panes` - A vector of Pane instances to be displayed. - /// - /// # Returns - /// - /// A result indicating success or an error. - /// - /// # Errors - /// - /// Returns an error if the terminal window - /// does not have enough vertical space to render the UI. - pub fn draw(&mut self, engine: &mut Engine, panes: Vec) -> Result { - let terminal_height = engine.size()?.1 as usize; + pub fn draw(&mut self, panes: &[Pane]) -> Result { + let height = terminal::size()?.1; + let viewable_panes = panes .iter() .filter(|pane| !pane.is_empty()) .collect::>(); - if terminal_height < viewable_panes.len() { - return engine - .write("Terminal window does not have enough vertical space to render UI."); + if height < viewable_panes.len() as u16 { + return crossterm::execute!( + io::stdout(), + terminal::Clear(terminal::ClearType::FromCursorDown), + style::Print("⚠️ Insufficient Space"), + ) + .map_err(Error::from); } - engine.move_to(self.position)?; - engine.clear_from_cursor_down()?; + crossterm::queue!( + io::stdout(), + cursor::MoveTo(self.position.0, self.position.1), + terminal::Clear(terminal::ClearType::FromCursorDown), + )?; let mut used = 0; - let mut current_cursor_y = engine.size()?.1.saturating_sub(self.position.1); + let mut current_cursor_y = height.saturating_sub(self.position.1); for (i, pane) in viewable_panes.iter().enumerate() { let rows = pane.extract( 1.max( - terminal_height + (height as usize) // -1 in this context signifies the exclusion of the current pane. .saturating_sub(used + viewable_panes.len() - 1 - i), ), ); used += rows.len(); for (i, row) in rows.iter().enumerate() { - engine.write(row.styled_display())?; + crossterm::queue!(io::stdout(), style::Print(row.styled_display()))?; + current_cursor_y = current_cursor_y.saturating_sub(1); if i != rows.len() - 1 && current_cursor_y == 0 { - engine.scroll_up(1)?; + crossterm::queue!(io::stdout(), terminal::ScrollUp(1))?; self.position.1 = self.position.1.saturating_sub(1); } - engine.move_to_next_line()?; + crossterm::queue!(io::stdout(), cursor::MoveToNextLine(1))?; } } - Ok(()) + io::stdout().flush().map_err(Error::from) } }