diff --git a/Cargo.lock b/Cargo.lock index 8e21b60c..63620001 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,62 +146,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" - [[package]] name = "crossterm" version = "0.27.0" @@ -721,7 +665,6 @@ version = "0.28.0" dependencies = [ "arboard", "chrono", - "crossbeam", "crossterm", "fd-lock", "gethostname 0.4.3", diff --git a/Cargo.toml b/Cargo.toml index 1b392cad..28a009da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ chrono = { version = "0.4.19", default-features = false, features = [ "clock", "serde", ] } -crossbeam = { version = "0.8.2", optional = true } crossterm = { version = "0.27.0", features = ["serde"] } fd-lock = "3.0.3" itertools = "0.12.0" @@ -41,7 +40,7 @@ tempfile = "3.3.0" [features] bashisms = [] -external_printer = ["crossbeam"] +external_printer = [] sqlite = ["rusqlite/bundled", "serde_json"] sqlite-dynlib = ["rusqlite", "serde_json"] system_clipboard = ["arboard"] diff --git a/examples/external_printer.rs b/examples/external_printer.rs index 633a1238..bbac7843 100644 --- a/examples/external_printer.rs +++ b/examples/external_printer.rs @@ -3,41 +3,45 @@ // cargo run --example external_printer --features=external_printer use { - reedline::ExternalPrinter, - reedline::{DefaultPrompt, Reedline, Signal}, + reedline::{DefaultPrompt, ExternalPrinter, Reedline, Signal}, std::thread, std::thread::sleep, std::time::Duration, }; fn main() { - let printer = ExternalPrinter::default(); - // make a clone to use it in a different thread - let p_clone = printer.clone(); - // get the Sender to have full sending control - let p_sender = printer.sender(); + let printer = ExternalPrinter::new(); + + // Get a clone of the sender to use in a separate thread. + let sender = printer.sender(); + + // Note that the senders can also be cloned. + // let sender_clone = sender.clone(); // external printer that prints a message every second thread::spawn(move || { let mut i = 1; loop { sleep(Duration::from_secs(1)); - assert!(p_clone - .print(format!("Message {i} delivered.\nWith two lines!")) + assert!(sender + .send(format!("Message {i} delivered.\nWith two lines!").into()) .is_ok()); i += 1; } }); + let sender = printer.sender(); + // external printer that prints a bunch of messages after 3 seconds thread::spawn(move || { sleep(Duration::from_secs(3)); for _ in 0..10 { sleep(Duration::from_millis(1)); - assert!(p_sender.send("Fast Hello !".to_string()).is_ok()); + assert!(sender.send("Hello!".into()).is_ok()); } }); + // create a `Reedline` struct and assign the external printer let mut line_editor = Reedline::create().with_external_printer(printer); let prompt = DefaultPrompt::default(); diff --git a/src/engine.rs b/src/engine.rs index e7bcceb7..cebd9932 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,18 +3,14 @@ use std::path::PathBuf; use itertools::Itertools; use nu_ansi_term::{Color, Style}; +#[cfg(feature = "external_printer")] +use crate::ExternalPrinter; use crate::{enums::ReedlineRawEvent, CursorConfig}; #[cfg(feature = "bashisms")] use crate::{ history::SearchFilter, menu_functions::{parse_selection_char, ParseAction}, }; -#[cfg(feature = "external_printer")] -use { - crate::external_printer::ExternalPrinter, - crossbeam::channel::TryRecvError, - std::io::{Error, ErrorKind}, -}; use { crate::{ completion::{Completer, DefaultCompleter}, @@ -154,7 +150,7 @@ pub struct Reedline { kitty_protocol: KittyProtocolGuard, #[cfg(feature = "external_printer")] - external_printer: Option>, + external_printer: Option, } struct BufferEditor { @@ -679,22 +675,24 @@ impl Reedline { let mut paste_enter_state = false; #[cfg(feature = "external_printer")] - if let Some(ref external_printer) = self.external_printer { - // get messages from printer as crlf separated "lines" - let messages = Self::external_messages(external_printer)?; - if !messages.is_empty() { - // print the message(s) - self.painter.print_external_message( - messages, - self.editor.line_buffer(), - prompt, - )?; + if let Some(printer) = &self.external_printer { + let any_messages = self.painter.print_external_messages( + printer.receiver(), + self.editor.line_buffer(), + prompt, + )?; + + if any_messages { self.repaint(prompt)?; } } let mut latest_resize = None; - loop { + + // There could be multiple events queued up! + // pasting text, resizes, blocking this thread (e.g. during debugging) + // We should be able to handle all of them as quickly as possible without causing unnecessary output steps. + while event::poll(Duration::from_millis(POLL_WAIT))? { match event::read()? { Event::Resize(x, y) => { latest_resize = Some((x, y)); @@ -723,13 +721,6 @@ impl Reedline { } } } - - // There could be multiple events queued up! - // pasting text, resizes, blocking this thread (e.g. during debugging) - // We should be able to handle all of them as quickly as possible without causing unnecessary output steps. - if !event::poll(Duration::from_millis(POLL_WAIT))? { - break; - } } if let Some((x, y)) = latest_resize { @@ -1730,38 +1721,23 @@ impl Reedline { ) } - /// Adds an external printer + /// Returns a reference to the external printer, or none if one was not set /// /// ## Required feature: /// `external_printer` #[cfg(feature = "external_printer")] - pub fn with_external_printer(mut self, printer: ExternalPrinter) -> Self { - self.external_printer = Some(printer); - self + pub fn external_printer(&self) -> Option<&ExternalPrinter> { + self.external_printer.as_ref() } + /// Add a new external printer, or overwrite the existing one + /// + /// ## Required feature: + /// `external_printer` #[cfg(feature = "external_printer")] - fn external_messages(external_printer: &ExternalPrinter) -> Result> { - let mut messages = Vec::new(); - loop { - let result = external_printer.receiver().try_recv(); - match result { - Ok(line) => { - let lines = line.lines().map(String::from).collect::>(); - messages.extend(lines); - } - Err(TryRecvError::Empty) => { - break; - } - Err(TryRecvError::Disconnected) => { - return Err(Error::new( - ErrorKind::NotConnected, - TryRecvError::Disconnected, - )); - } - } - } - Ok(messages) + pub fn with_external_printer(mut self, printer: ExternalPrinter) -> Self { + self.external_printer = Some(printer); + self } fn submit_buffer(&mut self, prompt: &dyn Prompt) -> io::Result { diff --git a/src/external_printer.rs b/src/external_printer.rs index f4c71e7c..36f55a15 100644 --- a/src/external_printer.rs +++ b/src/external_printer.rs @@ -1,72 +1,70 @@ -//! To print messages while editing a line -//! -//! See example: -//! -//! ``` shell -//! cargo run --example external_printer --features=external_printer -//! ``` -#[cfg(feature = "external_printer")] -use { - crossbeam::channel::{bounded, Receiver, SendError, Sender}, - std::fmt::Display, -}; +use std::sync::mpsc::{self, Receiver, SyncSender}; -#[cfg(feature = "external_printer")] -pub const EXTERNAL_PRINTER_DEFAULT_CAPACITY: usize = 20; - -/// An ExternalPrinter allows to print messages of text while editing a line. -/// The message is printed as a new line, the line-edit will continue below the -/// output. +/// An external printer allows one to print messages of text while editing a line. +/// The message is printed as a new line, and the line-edit will continue below the output. +/// +/// Use [`sender`](Self::sender) to receive a [`SyncSender`] for use in other threads. /// /// ## Required feature: /// `external_printer` -#[cfg(feature = "external_printer")] -#[derive(Debug, Clone)] -pub struct ExternalPrinter -where - T: Display, -{ - sender: Sender, - receiver: Receiver, +#[derive(Debug)] +pub struct ExternalPrinter { + sender: SyncSender>, + receiver: Receiver>, } -#[cfg(feature = "external_printer")] -impl ExternalPrinter -where - T: Display, -{ - /// Creates an ExternalPrinter to store lines with a max_cap - pub fn new(max_cap: usize) -> Self { - let (sender, receiver) = bounded::(max_cap); +impl ExternalPrinter { + /// The default maximum number of lines that can be queued up for printing + pub const DEFAULT_CAPACITY: usize = 20; + + /// Create a new `ExternalPrinter` with the [default capacity](Self::DEFAULT_CAPACITY) + pub fn new() -> Self { + Self::default() + } + + /// Create a new `ExternalPrinter` with the given capacity + /// + /// The capacity determines the maximum number of lines that can be queued up for printing + /// before subsequent [`send`](SyncSender::send) calls on the [`sender`](Self::sender) will block. + pub fn with_capacity(capacity: usize) -> Self { + let (sender, receiver) = mpsc::sync_channel(capacity); Self { sender, receiver } } - /// Gets a Sender to use the printer externally by sending lines to it - pub fn sender(&self) -> Sender { + + /// Returns a new [`SyncSender`] which can be used in other threads to queue messages to print + pub fn sender(&self) -> SyncSender> { self.sender.clone() } - /// Receiver to get messages if any - pub fn receiver(&self) -> &Receiver { + + pub(crate) fn receiver(&self) -> &Receiver> { &self.receiver } +} - /// Convenience method if the whole Printer is cloned, blocks if max_cap is reached. - /// - pub fn print(&self, line: T) -> Result<(), SendError> { - self.sender.send(line) +impl Default for ExternalPrinter { + fn default() -> Self { + Self::with_capacity(Self::DEFAULT_CAPACITY) } +} - /// Convenience method to get a line if any, doesn´t block. - pub fn get_line(&self) -> Option { - self.receiver.try_recv().ok() +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn impls_send() { + fn impls_send(_: &T) {} + + let printer = ExternalPrinter::new(); + impls_send(&printer); + impls_send(&printer.sender()) } -} -#[cfg(feature = "external_printer")] -impl Default for ExternalPrinter -where - T: Display, -{ - fn default() -> Self { - Self::new(EXTERNAL_PRINTER_DEFAULT_CAPACITY) + #[test] + fn receives_message() { + let printer = ExternalPrinter::new(); + let sender = printer.sender(); + assert!(sender.send(b"some text".into()).is_ok()); + assert_eq!(printer.receiver().recv(), Ok(b"some text".into())) } } diff --git a/src/lib.rs b/src/lib.rs index 4d14261b..c408151e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -284,9 +284,12 @@ pub use menu::{ mod terminal_extensions; pub use terminal_extensions::kitty_protocol_available; -mod utils; - +#[cfg(feature = "external_printer")] mod external_printer; +#[cfg(feature = "external_printer")] +pub use external_printer::ExternalPrinter; + +mod utils; pub use utils::{ get_reedline_default_keybindings, get_reedline_edit_commands, get_reedline_keybinding_modifiers, get_reedline_keycodes, get_reedline_prompt_edit_modes, @@ -298,5 +301,3 @@ pub use crossterm::{ event::{KeyCode, KeyModifiers}, style::Color, }; -#[cfg(feature = "external_printer")] -pub use external_printer::ExternalPrinter; diff --git a/src/painting/painter.rs b/src/painting/painter.rs index 4986a961..ab683581 100644 --- a/src/painting/painter.rs +++ b/src/painting/painter.rs @@ -16,7 +16,7 @@ use { std::io::{Result, Write}, }; #[cfg(feature = "external_printer")] -use {crate::LineBuffer, crossterm::cursor::MoveUp}; +use {crate::LineBuffer, crossterm::cursor::MoveUp, std::sync::mpsc::Receiver}; // Returns a string that skips N number of lines with the next offset of lines // An offset of 0 would return only one line after skipping the required lines @@ -488,17 +488,22 @@ impl Painter { self.stdout.flush() } - /// Prints an external message + /// Prints external messages from a channel receiver, returning true if there were any messages /// /// This function doesn't flush the buffer. So buffer should be flushed /// afterwards perhaps by repainting the prompt via `repaint_buffer()`. #[cfg(feature = "external_printer")] - pub(crate) fn print_external_message( + pub(crate) fn print_external_messages( &mut self, - messages: Vec, + receiver: &Receiver>, line_buffer: &LineBuffer, prompt: &dyn Prompt, - ) -> Result<()> { + ) -> Result { + let mut messages = receiver.try_iter().peekable(); + if messages.peek().is_none() { + return Ok(false); + } + // adding 3 seems to be right for first line-wrap let prompt_len = prompt.render_prompt_right().len() + 3; let mut buffer_num_lines = 0_u16; @@ -522,23 +527,43 @@ impl Painter { if buffer_num_lines > 1 { self.stdout.queue(MoveUp(buffer_num_lines - 1))?; } + let erase_line = format!("\r{}\r", " ".repeat(self.screen_width().into())); - for line in messages { - self.stdout.queue(Print(&erase_line))?; - // Note: we don't use `print_line` here because we don't want to - // flush right now. The subsequent repaint of the prompt will cause - // immediate flush anyways. And if we flush here, every external - // print causes visible flicker. - self.stdout.queue(Print(line))?.queue(Print("\r\n"))?; - let new_start = self.prompt_start_row.saturating_add(1); - let height = self.screen_height(); - if new_start >= height { - self.prompt_start_row = height - 1; - } else { - self.prompt_start_row = new_start; + for mut message in messages { + // add a new line for next message + // messages that already end in '\n' will have a blank line between it and the next message. + message.push(b'\n'); + + for line in message.split_inclusive(|&b| b == b'\n') { + let line = line + .strip_suffix(&[b'\n']) + .map(|line| line.strip_suffix(&[b'\r']).unwrap_or(line)) + .unwrap_or(line); + + self.stdout.queue(Print(&erase_line))?; + + // Note: we don't flush here. + // The subsequent repaint of the prompt will cause immediate flush anyways. + // And if we flush here, every external print causes visible flicker. + // + // crossterm's `Print` command returns true for `is_ansi_code_supported`. + // This means crossterm will use `Print`'s implementation of `Command::write_ansi` + // without doing any special handling for ANSI sequences. + // So, it's ok for us to do something similar by calling `write_all` directly + // without any special handling. + self.stdout.write_all(line)?; // self.stdout.queue(Print(line))?; + self.stdout.queue(Print("\r\n"))?; + + let new_start = self.prompt_start_row.saturating_add(1); + let height = self.screen_height(); + if new_start >= height { + self.prompt_start_row = height - 1; + } else { + self.prompt_start_row = new_start; + } } } - Ok(()) + Ok(true) } /// Queue scroll of `num` lines to `self.stdout`.