diff --git a/promkit/src/lib.rs b/promkit/src/lib.rs index 72922934..22730030 100644 --- a/promkit/src/lib.rs +++ b/promkit/src/lib.rs @@ -223,12 +223,13 @@ pub trait Renderer: Finalizer { /// event handling, and result production for a prompt. pub struct Prompt { pub renderer: T, + pub writer: Box, } impl Drop for Prompt { fn drop(&mut self) { execute!( - io::stdout(), + self.writer, cursor::Show, event::DisableMouseCapture, cursor::MoveToNextLine(1), @@ -250,11 +251,11 @@ impl Prompt { /// Returns a `Result` containing the produced result or an error. pub fn run(&mut self) -> anyhow::Result { enable_raw_mode()?; - execute!(io::stdout(), cursor::Hide)?; + execute!(self.writer, cursor::Hide)?; let size = crossterm::terminal::size()?; let panes = self.renderer.create_panes(size.0, size.1); - let mut terminal = Terminal::start_session(&panes)?; + let mut terminal = Terminal::start_session(&panes, &mut self.writer)?; terminal.draw(&panes)?; loop { @@ -262,11 +263,7 @@ impl Prompt { match &ev { Event::Resize(_, _) => { - terminal.position = (0, 0); - crossterm::execute!( - io::stdout(), - crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge), - )?; + terminal.on_resize()?; } _ => { if self.renderer.evaluate(&ev)? == PromptSignal::Quit { diff --git a/promkit/src/preset/checkbox.rs b/promkit/src/preset/checkbox.rs index de4c6851..77d1c1f6 100644 --- a/promkit/src/preset/checkbox.rs +++ b/promkit/src/preset/checkbox.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, fmt::Display}; +use std::{cell::RefCell, fmt::Display, io}; use crate::{ checkbox, @@ -20,6 +20,8 @@ pub struct Checkbox { title_state: text::State, /// State for the checkbox list itself. checkbox_state: checkbox::State, + /// Writer to which promptkit write its contents + writer: Box, } impl Checkbox { @@ -48,6 +50,7 @@ impl Checkbox { lines: Default::default(), }, keymap: ActiveKeySwitcher::new("default", self::keymap::default), + writer: Box::new(io::stdout()), } } @@ -69,6 +72,7 @@ impl Checkbox { lines: Default::default(), }, keymap: ActiveKeySwitcher::new("default", self::keymap::default), + writer: Box::new(io::stdout()), } } @@ -119,6 +123,12 @@ impl Checkbox { self } + /// Sets writer. + pub fn writer(mut self, writer: W) -> Self { + self.writer = Box::new(writer); + self + } + /// Displays the checkbox prompt and waits for user input. /// Returns a `Result` containing the `Prompt` result, /// which is a list of selected options. @@ -129,6 +139,7 @@ impl Checkbox { title_snapshot: Snapshot::::new(self.title_state), checkbox_snapshot: Snapshot::::new(self.checkbox_state), }, + writer: self.writer, }) } } diff --git a/promkit/src/preset/form.rs b/promkit/src/preset/form.rs index 786cd51e..0045fde7 100644 --- a/promkit/src/preset/form.rs +++ b/promkit/src/preset/form.rs @@ -1,4 +1,4 @@ -use std::cell::RefCell; +use std::{cell::RefCell, io}; use crate::{ core::Cursor, @@ -17,6 +17,8 @@ pub struct Form { text_editor_states: Vec, /// Overwrite the default styles of text editor states when unselected. overwrite_styles: Vec, + /// Writer to which promptkit write its contents + writer: Box, } impl Form { @@ -42,9 +44,16 @@ impl Form { keymap: ActiveKeySwitcher::new("default", self::keymap::default as keymap::Keymap), text_editor_states, overwrite_styles, + writer: Box::new(io::stdout()), } } + /// Sets writer. + pub fn writer(mut self, writer: W) -> Self { + self.writer = Box::new(writer); + self + } + pub fn prompt(self) -> anyhow::Result> { let default_styles = self .text_editor_states @@ -62,6 +71,9 @@ impl Form { overwrite_styles: self.overwrite_styles, }; renderer.overwrite_styles(); - Ok(Prompt { renderer }) + Ok(Prompt { + renderer, + writer: self.writer, + }) } } diff --git a/promkit/src/preset/json.rs b/promkit/src/preset/json.rs index 2149664e..d89e2633 100644 --- a/promkit/src/preset/json.rs +++ b/promkit/src/preset/json.rs @@ -1,4 +1,4 @@ -use std::cell::RefCell; +use std::{cell::RefCell, io}; use crate::{ crossterm::style::{Attribute, Attributes, Color, ContentStyle}, @@ -17,6 +17,8 @@ pub struct Json { keymap: ActiveKeySwitcher, title_state: text::State, json_state: json::State, + /// Writer to which promptkit write its contents + writer: Box, } impl Json { @@ -47,6 +49,7 @@ impl Json { indent: 2, }, keymap: ActiveKeySwitcher::new("default", self::keymap::default), + writer: Box::new(io::stdout()), } } @@ -91,6 +94,12 @@ impl Json { self } + /// Sets writer. + pub fn writer(mut self, writer: W) -> Self { + self.writer = Box::new(writer); + self + } + /// Creates a prompt based on the current configuration of the `Json` instance. pub fn prompt(self) -> anyhow::Result> { Ok(Prompt { @@ -99,6 +108,7 @@ impl Json { title_snapshot: Snapshot::::new(self.title_state), json_snapshot: Snapshot::::new(self.json_state), }, + writer: self.writer, }) } } diff --git a/promkit/src/preset/listbox.rs b/promkit/src/preset/listbox.rs index e27f8f79..f1fd27d9 100644 --- a/promkit/src/preset/listbox.rs +++ b/promkit/src/preset/listbox.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, fmt::Display}; +use std::{cell::RefCell, fmt::Display, io}; use crate::{ crossterm::style::{Attribute, Attributes, Color, ContentStyle}, @@ -19,6 +19,8 @@ pub struct Listbox { title_state: text::State, /// State for the selectable list itself. listbox_state: listbox::State, + /// Writer to which promptkit write its contents + writer: Box, } impl Listbox { @@ -45,6 +47,7 @@ impl Listbox { lines: Default::default(), }, keymap: ActiveKeySwitcher::new("default", self::keymap::default), + writer: Box::new(io::stdout()), } } @@ -89,6 +92,12 @@ impl Listbox { self } + /// Sets writer. + pub fn writer(mut self, writer: W) -> Self { + self.writer = Box::new(writer); + self + } + /// Displays the select prompt and waits for user input. /// Returns a `Result` containing the `Prompt` result, /// which is the selected option. @@ -99,6 +108,7 @@ impl Listbox { title_snapshot: Snapshot::::new(self.title_state), listbox_snapshot: Snapshot::::new(self.listbox_state), }, + writer: self.writer, }) } } diff --git a/promkit/src/preset/query_selector.rs b/promkit/src/preset/query_selector.rs index a2c679a0..f17673b5 100644 --- a/promkit/src/preset/query_selector.rs +++ b/promkit/src/preset/query_selector.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, fmt::Display}; +use std::{cell::RefCell, fmt::Display, io}; use crate::{ crossterm::style::{Attribute, Attributes, Color, ContentStyle}, @@ -28,6 +28,8 @@ pub struct QuerySelector { /// A filter function to apply to the list box items /// based on the text editor input. filter: render::Filter, + /// Writer to which promptkit write its contents + writer: Box, } impl QuerySelector { @@ -74,6 +76,7 @@ impl QuerySelector { }, keymap: ActiveKeySwitcher::new("default", self::keymap::default), filter, + writer: Box::new(io::stdout()), } } @@ -154,6 +157,12 @@ impl QuerySelector { self } + /// Sets writer. + pub fn writer(mut self, writer: W) -> Self { + self.writer = Box::new(writer); + self + } + /// Displays the query select prompt and waits for user input. /// Returns a `Result` containing the `Prompt` result, /// which is the selected option. @@ -166,6 +175,7 @@ impl QuerySelector { listbox_snapshot: Snapshot::::new(self.listbox_state), filter: self.filter, }, + writer: self.writer, }) } } diff --git a/promkit/src/preset/readline.rs b/promkit/src/preset/readline.rs index 85875c5b..84e6e960 100644 --- a/promkit/src/preset/readline.rs +++ b/promkit/src/preset/readline.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, collections::HashSet}; +use std::{cell::RefCell, collections::HashSet, io}; use crate::{ crossterm::style::{Attribute, Attributes, Color, ContentStyle}, @@ -34,6 +34,8 @@ pub struct Readline { validator: Option>, /// State for displaying error messages based on input validation. error_message_state: text::State, + /// Writer to which promptkit write its contents + writer: Box, } impl Default for Readline { @@ -80,6 +82,7 @@ impl Default for Readline { .attrs(Attributes::from(Attribute::Bold)) .build(), }, + writer: Box::new(io::stdout()), } } } @@ -172,6 +175,12 @@ impl Readline { self } + /// Sets writer. + pub fn writer(mut self, writer: W) -> Self { + self.writer = Box::new(writer); + self + } + /// Initiates the prompt process, /// displaying the configured UI elements and handling user input. pub fn prompt(self) -> anyhow::Result> { @@ -185,6 +194,7 @@ impl Readline { validator: self.validator, error_message_snapshot: Snapshot::::new(self.error_message_state), }, + writer: self.writer, }) } } diff --git a/promkit/src/preset/tree.rs b/promkit/src/preset/tree.rs index 0904ba85..99a067de 100644 --- a/promkit/src/preset/tree.rs +++ b/promkit/src/preset/tree.rs @@ -1,4 +1,4 @@ -use std::cell::RefCell; +use std::{cell::RefCell, io}; use crate::{ crossterm::style::{Attribute, Attributes, Color, ContentStyle}, @@ -21,6 +21,8 @@ pub struct Tree { title_state: text::State, /// State for the tree itself. tree_state: tree::State, + /// Writer to which promptkit write its contents + writer: Box, } impl Tree { @@ -47,6 +49,7 @@ impl Tree { lines: Default::default(), indent: 2, }, + writer: Box::new(io::stdout()), } } @@ -103,6 +106,12 @@ impl Tree { self } + /// Sets writer. + pub fn writer(mut self, writer: W) -> Self { + self.writer = Box::new(writer); + self + } + /// Displays the tree prompt and waits for user input. /// Returns a `Result` containing the `Prompt` result, /// which is a list of selected options. @@ -113,6 +122,7 @@ impl Tree { title_snapshot: Snapshot::::new(self.title_state), tree_snapshot: Snapshot::::new(self.tree_state), }, + writer: self.writer, }) } } diff --git a/promkit/src/terminal.rs b/promkit/src/terminal.rs index d15e0f2d..0b834e50 100644 --- a/promkit/src/terminal.rs +++ b/promkit/src/terminal.rs @@ -1,17 +1,27 @@ -use std::io::{self, Write}; +use std::io::Write; use crate::{ crossterm::{cursor, style, terminal}, pane::Pane, }; -pub struct Terminal { +pub struct Terminal<'a, W: Write> { /// The current cursor position within the terminal. pub position: (u16, u16), + pub writer: &'a mut W, } -impl Terminal { - pub fn start_session(panes: &[Pane]) -> anyhow::Result { +impl<'a, W: Write> Terminal<'a, W> { + pub fn on_resize(&mut self) -> anyhow::Result<()> { + self.position = (0, 0); + crossterm::execute!( + self.writer, + crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge), + )?; + Ok(()) + } + + pub fn start_session(panes: &[Pane], writer: &'a mut W) -> anyhow::Result { let position = cursor::position()?; let size = terminal::size()?; @@ -23,9 +33,9 @@ impl Terminal { // to ensure the next output starts correctly. if position.0 != 0 { if size.1 == position.1 + 1 { - crossterm::queue!(io::stdout(), terminal::ScrollUp(1))?; + crossterm::queue!(writer, terminal::ScrollUp(1))?; } - crossterm::queue!(io::stdout(), cursor::MoveToNextLine(1))?; + crossterm::queue!(writer, cursor::MoveToNextLine(1))?; } // Calculate the total number of rows required by all panes. @@ -39,16 +49,17 @@ impl Terminal { // to maintain its relative position. if size.1 == position.1 + 1 { crossterm::queue!( - io::stdout(), + writer, terminal::ScrollUp(lines as u16), cursor::MoveToPreviousLine(lines as u16), )?; } - io::stdout().flush()?; + writer.flush()?; Ok(Self { position: cursor::position()?, + writer, }) } @@ -62,7 +73,7 @@ impl Terminal { if height < viewable_panes.len() as u16 { return crossterm::execute!( - io::stdout(), + self.writer, terminal::Clear(terminal::ClearType::FromCursorDown), style::Print("⚠️ Insufficient Space"), ) @@ -70,7 +81,7 @@ impl Terminal { } crossterm::queue!( - io::stdout(), + self.writer, cursor::MoveTo(self.position.0, self.position.1), terminal::Clear(terminal::ClearType::FromCursorDown), )?; @@ -88,7 +99,7 @@ impl Terminal { ); used += rows.len(); for (j, row) in rows.iter().enumerate() { - crossterm::queue!(io::stdout(), style::Print(row.styled_display()))?; + crossterm::queue!(self.writer, style::Print(row.styled_display()))?; current_cursor_y = current_cursor_y.saturating_sub(1); @@ -96,14 +107,14 @@ impl Terminal { || i != viewable_panes.len() - 1) && current_cursor_y == 0 { - crossterm::queue!(io::stdout(), terminal::ScrollUp(1))?; + crossterm::queue!(self.writer, terminal::ScrollUp(1))?; self.position.1 = self.position.1.saturating_sub(1); } - crossterm::queue!(io::stdout(), cursor::MoveToNextLine(1))?; + crossterm::queue!(self.writer, cursor::MoveToNextLine(1))?; } } - io::stdout().flush()?; + self.writer.flush()?; Ok(()) } }