diff --git a/Cargo.toml b/Cargo.toml index 87141f86..04bb1fd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,9 @@ derive-more = ["dep:derive_more"] #! ### Optional Features +## Enable non-psudo-terminal +no-tty = ["dep:crossbeam-channel", "tokio"] + ## Enables documentation for crate features. document-features = ["dep:document-features"] @@ -61,31 +64,46 @@ document-features = { version = "0.2.11", optional = true } futures-core = { version = "0.3", optional = true, default-features = false } parking_lot = "0.12" serde = { version = "1.0", features = ["derive"], optional = true } +crossbeam-channel = { version = "0.5", optional = true } +tokio = { version = "1", optional = true, features = ["sync"] } + # Windows dependencies [target.'cfg(windows)'.dependencies] crossterm_winapi = { version = "0.9.1", optional = true } -winapi = { version = "0.3.9", optional = true, features = ["winuser", "winerror"] } +winapi = { version = "0.3.9", optional = true, features = [ + "winuser", + "winerror", +] } # UNIX dependencies -[target.'cfg(unix)'.dependencies] +[target.'cfg(all(unix,not(feature="no-tty")))'.dependencies] filedescriptor = { version = "0.8", optional = true } # Default to using rustix for UNIX systems, but provide an option to use libc for backwards # compatibility. libc = { version = "0.2", default-features = false, optional = true } mio = { version = "1.0", features = ["os-poll"], optional = true } -rustix = { version = "1", default-features = false, features = ["std", "stdio", "termios"] } +rustix = { version = "1", default-features = false, features = [ + "std", + "stdio", + "termios", +] } signal-hook = { version = "0.3.17", optional = true } -signal-hook-mio = { version = "0.2.4", features = ["support-v1_0"], optional = true } +signal-hook-mio = { version = "0.2.4", features = [ + "support-v1_0", +], optional = true } [dev-dependencies] async-std = "1.13" -futures = "0.3" +futures = "0.3.31" futures-timer = "3.0" serde_json = "1.0" serial_test = "3.0.0" temp-env = "0.3.6" tokio = { version = "1.44", features = ["full"] } +russh = "0.53.0" +rand_core = "0.6" +anyhow = "1" # Examples [[example]] @@ -120,6 +138,10 @@ required-features = ["events"] name = "key-display" required-features = ["events"] +[[example]] +name = "ssh-service" +required-features = ["no-tty"] + [[example]] name = "copy-to-clipboard" required-features = ["osc52"] diff --git a/examples/ssh-service.rs b/examples/ssh-service.rs new file mode 100644 index 00000000..42e2ee32 --- /dev/null +++ b/examples/ssh-service.rs @@ -0,0 +1,306 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use crossterm::event::{ + poll, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, +}; +use std::time::Duration; + +use crossbeam_channel::{unbounded, Receiver, Sender}; +use crossterm::event::{NoTtyEvent, SenderWriter}; +use crossterm::{ + cursor::position, + event::{ + read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, Event, KeyCode, + }, + execute, queue, + terminal::WindowSize, +}; +use russh::keys::ssh_key::PublicKey; +use russh::server::*; +use russh::{Channel, ChannelId, Pty}; +use tokio::sync::Mutex; + +struct App { + pub pty: NoTtyEvent, + pub send: Sender>, + pub recv: Receiver>, +} + +#[derive(Clone)] +struct AppServer { + clients: Arc>>, + id: usize, +} + +impl AppServer { + pub fn new() -> Self { + Self { + clients: Arc::new(Mutex::new(HashMap::new())), + id: 0, + } + } + + pub async fn run(&mut self) -> Result<(), anyhow::Error> { + let config = Config { + inactivity_timeout: Some(std::time::Duration::from_secs(3600)), + auth_rejection_time: std::time::Duration::from_secs(3), + auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)), + keys: vec![russh::keys::PrivateKey::random( + &mut rand_core::OsRng, + russh::keys::Algorithm::Ed25519, + ) + .unwrap()], + nodelay: true, + ..Default::default() + }; + + self.run_on_address(Arc::new(config), ("127.0.0.1", 2222)) + .await?; + Ok(()) + } +} + +impl Server for AppServer { + type Handler = Self; + fn new_client(&mut self, _: Option) -> Self { + let s = self.clone(); + self.id += 1; + s + } +} + +impl Handler for AppServer { + type Error = russh::Error; + + async fn channel_open_session( + &mut self, + _channel: Channel, + _session: &mut Session, + ) -> Result { + let (app_send, term_recv) = unbounded(); + let (psudo_tty, app_recv) = NoTtyEvent::new(term_recv); + let app = App { + pty: psudo_tty, + send: app_send, + recv: app_recv, + }; + + let mut clients = self.clients.lock().await; + clients.insert(self.id, app); + + Ok(true) + } + + async fn auth_publickey(&mut self, _: &str, _: &PublicKey) -> Result { + Ok(Auth::Accept) + } + + async fn data( + &mut self, + _channel: ChannelId, + data: &[u8], + _session: &mut Session, + ) -> Result<(), Self::Error> { + let mut clients = self.clients.lock().await; + let app = clients.get_mut(&self.id).unwrap(); + let _ = app.send.send(data.into()).unwrap(); + if data == [3] { + return Err(russh::Error::Disconnect); + } + + Ok(()) + } + + /// The client's window size has changed. + async fn window_change_request( + &mut self, + _channel: ChannelId, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + _session: &mut Session, + ) -> Result<(), Self::Error> { + let mut clients = self.clients.lock().await; + let app = clients.get_mut(&self.id).unwrap(); + *app.pty.window_size.lock() = WindowSize { + rows: row_height as u16, + columns: col_width as u16, + width: pix_width as u16, + height: pix_height as u16, + }; + + let mut win_raw = Vec::from(b"\x1B[W"); + let col = (col_width as u16).to_string(); + let row = (row_height as u16).to_string(); + win_raw.extend_from_slice(col.as_bytes()); + win_raw.push(b';'); + win_raw.extend_from_slice(row.as_bytes()); + win_raw.push(b'R'); + let _ = app.send.send(win_raw); + + Ok(()) + } + + /// The client requests a pseudo-terminal with the given + /// specifications. + /// + /// NOTE: Success or failure should be communicated to the client by calling + /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. + async fn pty_request( + &mut self, + channel: ChannelId, + _: &str, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + _: &[(Pty, u32)], + session: &mut Session, + ) -> Result<(), Self::Error> { + let mut clients = self.clients.lock().await; + let app = clients.get_mut(&self.id).unwrap(); + + *app.pty.window_size.lock() = WindowSize { + rows: row_height as u16, + columns: col_width as u16, + width: pix_width as u16, + height: pix_height as u16, + }; + + session.channel_success(channel)?; + + Ok(()) + } + async fn shell_request( + &mut self, + channel: ChannelId, + session: &mut Session, + ) -> Result<(), Self::Error> { + let mut clients = self.clients.lock().await; + let app = clients.get_mut(&self.id).unwrap(); + let pty = app.pty.clone(); + let handle = session.handle(); + let (tx, mut rx) = tokio::sync::mpsc::channel::>(5); + let tx2 = tx.clone(); + let tx3 = tx.clone(); + const HELP: &str = "Blocking read()\r\n- Keyboard, mouse, focus and terminal resize events enabled\r\n- Hit \"c\" to print current cursor position\r\n- Use Esc to quit\r\n"; + let _ = handle.data(channel, HELP.into()).await; + tokio::task::spawn_blocking(move || { + let supports_keyboard_enhancement = matches!( + crossterm::terminal::supports_keyboard_enhancement(&pty), + Ok(true) + ); + let mut tx = SenderWriter::new(tx); + + if supports_keyboard_enhancement { + let _ = queue!( + tx, + PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES + | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES + | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS + | KeyboardEnhancementFlags::REPORT_EVENT_TYPES + ) + ); + } + + let _ = execute!( + tx, + EnableBracketedPaste, + EnableFocusChange, + EnableMouseCapture, + ); + + loop { + // Blocking read + let event = match read(&pty) { + Ok(e) => e, + Err(_) => { + continue; + } + }; + + let data = format!("Event: {event:?}\r\n"); + let _ = tx3.blocking_send(data.into()); + + if event == Event::Key(KeyCode::Char('c').into()) { + let data = format!("Cursor position: {:?}\r\n", position(&pty)); + let _ = tx3.blocking_send(data.into()); + } + + if let Event::Resize(x, y) = event { + let (original_size, new_size) = flush_resize_events(&pty, (x, y)); + let data = format!("Resize from: {original_size:?}, to: {new_size:?}\r\n"); + let _ = tx3.blocking_send(data.into()); + } + + if event == Event::Key(KeyCode::Esc.into()) { + break; + } + } + if supports_keyboard_enhancement { + let _ = queue!(tx, PopKeyboardEnhancementFlags); + } + + let _ = execute!( + tx, + DisableBracketedPaste, + DisableFocusChange, + DisableMouseCapture + ); + }); + let r = app.recv.clone(); + tokio::task::spawn_blocking(move || loop { + if let Ok(d) = r.recv() { + let _ = tx2.blocking_send(d); + } else { + break; + } + }); + tokio::spawn(async move { + loop { + if let Some(data) = rx.recv().await { + let _ = handle.data(channel, data.into()).await; + } else { + let _ = handle.close(channel).await; + } + } + }); + session.channel_success(channel)?; + Ok(()) + } +} + +// Resize events can occur in batches. +// With a simple loop they can be flushed. +// This function will keep the first and last resize event. +fn flush_resize_events(event: &NoTtyEvent, first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) { + let mut last_resize = first_resize; + while let Ok(true) = poll(event, Duration::from_millis(50)) { + if let Ok(Event::Resize(x, y)) = read(event) { + last_resize = (x, y); + } + } + + (first_resize, last_resize) +} + +impl Drop for AppServer { + fn drop(&mut self) { + let id = self.id; + let clients = self.clients.clone(); + tokio::spawn(async move { + let mut clients = clients.lock().await; + clients.remove(&id); + }); + } +} + +#[tokio::main] +async fn main() { + let mut server = AppServer::new(); + server.run().await.expect("Failed running server"); +} diff --git a/src/clipboard.rs b/src/clipboard.rs index 75913112..4b9e4275 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -160,6 +160,7 @@ impl> Command for CopyToClipboard { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { use std::io; diff --git a/src/command.rs b/src/command.rs index 0a04fc5f..654f11da 100644 --- a/src/command.rs +++ b/src/command.rs @@ -24,6 +24,7 @@ pub trait Command { /// /// This method does not need to be accessed manually, as it is used by the crossterm's [Command API](./index.html#command-api) #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> io::Result<()>; /// Returns whether the ANSI code representation of this command is supported by windows. @@ -31,6 +32,7 @@ pub trait Command { /// A list of supported ANSI escape codes /// can be found [here](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences). #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn is_ansi_code_supported(&self) -> bool { super::ansi_support::supports_ansi() } @@ -43,11 +45,13 @@ impl Command for &T { #[inline] #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> io::Result<()> { T::execute_winapi(self) } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] #[inline] fn is_ansi_code_supported(&self) -> bool { T::is_ansi_code_supported(self) @@ -120,6 +124,7 @@ impl QueueableCommand for T { /// and [queue](./trait.QueueableCommand.html) for those old Windows versions. fn queue(&mut self, command: impl Command) -> io::Result<&mut Self> { #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] if !command.is_ansi_code_supported() { // There may be queued commands in this writer, but `execute_winapi` will execute the // command immediately. To prevent commands being executed out of order we flush the @@ -287,6 +292,7 @@ fn write_command_ansi( /// Executes the ANSI representation of a command, using the given `fmt::Write`. pub(crate) fn execute_fmt(f: &mut impl fmt::Write, command: impl Command) -> fmt::Result { #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] if !command.is_ansi_code_supported() { return command.execute_winapi().map_err(|_| fmt::Error); } diff --git a/src/cursor.rs b/src/cursor.rs index 24796dc4..b7fb37d1 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -65,6 +65,7 @@ impl Command for MoveTo { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::move_to(self.0, self.1) } @@ -87,6 +88,7 @@ impl Command for MoveToNextLine { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { if self.0 != 0 { sys::move_to_next_line(self.0)?; @@ -112,6 +114,7 @@ impl Command for MoveToPreviousLine { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { if self.0 != 0 { sys::move_to_previous_line(self.0)?; @@ -135,6 +138,7 @@ impl Command for MoveToColumn { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::move_to_column(self.0) } @@ -155,6 +159,7 @@ impl Command for MoveToRow { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::move_to_row(self.0) } @@ -176,6 +181,7 @@ impl Command for MoveUp { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::move_up(self.0) } @@ -197,6 +203,7 @@ impl Command for MoveRight { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::move_right(self.0) } @@ -218,6 +225,7 @@ impl Command for MoveDown { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::move_down(self.0) } @@ -239,6 +247,7 @@ impl Command for MoveLeft { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::move_left(self.0) } @@ -261,6 +270,7 @@ impl Command for SavePosition { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::save_position() } @@ -283,6 +293,7 @@ impl Command for RestorePosition { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::restore_position() } @@ -302,6 +313,7 @@ impl Command for Hide { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::show_cursor(false) } @@ -321,6 +333,7 @@ impl Command for Show { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::show_cursor(true) } @@ -340,6 +353,7 @@ impl Command for EnableBlinking { f.write_str(csi!("?12h")) } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) } @@ -359,6 +373,7 @@ impl Command for DisableBlinking { f.write_str(csi!("?12l")) } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) } @@ -402,6 +417,7 @@ impl Command for SetCursorStyle { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) } @@ -427,10 +443,13 @@ impl_display!(for SetCursorStyle); #[cfg(test)] #[cfg(feature = "events")] mod tests { + #[cfg(not(feature = "no-tty"))] use std::io::{self, stdout}; + #[cfg(not(feature = "no-tty"))] use crate::execute; + #[cfg(not(feature = "no-tty"))] use super::{ sys::position, MoveDown, MoveLeft, MoveRight, MoveTo, MoveUp, RestorePosition, SavePosition, }; @@ -438,6 +457,7 @@ mod tests { // Test is disabled, because it's failing on Travis #[test] #[ignore] + #[cfg(not(feature = "no-tty"))] fn test_move_to() { let (saved_x, saved_y) = position().unwrap(); @@ -451,6 +471,7 @@ mod tests { // Test is disabled, because it's failing on Travis #[test] #[ignore] + #[cfg(not(feature = "no-tty"))] fn test_move_right() { let (saved_x, saved_y) = position().unwrap(); execute!(io::stdout(), MoveRight(1)).unwrap(); @@ -460,6 +481,7 @@ mod tests { // Test is disabled, because it's failing on Travis #[test] #[ignore] + #[cfg(not(feature = "no-tty"))] fn test_move_left() { execute!(stdout(), MoveTo(2, 0), MoveLeft(2)).unwrap(); assert_eq!(position().unwrap(), (0, 0)); @@ -468,6 +490,7 @@ mod tests { // Test is disabled, because it's failing on Travis #[test] #[ignore] + #[cfg(not(feature = "no-tty"))] fn test_move_up() { execute!(stdout(), MoveTo(0, 2), MoveUp(2)).unwrap(); assert_eq!(position().unwrap(), (0, 0)); @@ -476,6 +499,7 @@ mod tests { // Test is disabled, because it's failing on Travis #[test] #[ignore] + #[cfg(not(feature = "no-tty"))] fn test_move_down() { execute!(stdout(), MoveTo(0, 0), MoveDown(2)).unwrap(); @@ -485,6 +509,7 @@ mod tests { // Test is disabled, because it's failing on Travis #[test] #[ignore] + #[cfg(not(feature = "no-tty"))] fn test_save_restore_position() { let (saved_x, saved_y) = position().unwrap(); diff --git a/src/cursor/sys.rs b/src/cursor/sys.rs index 1623740a..8db1399b 100644 --- a/src/cursor/sys.rs +++ b/src/cursor/sys.rs @@ -1,20 +1,34 @@ //! This module provides platform related functions. #[cfg(unix)] +#[cfg(feature = "no-tty")] +#[cfg(feature = "events")] +pub use self::no_tty::position; +#[cfg(unix)] +#[cfg(not(feature = "no-tty"))] #[cfg(feature = "events")] pub use self::unix::position; #[cfg(windows)] +#[cfg(not(feature = "no-tty"))] #[cfg(feature = "events")] pub use self::windows::position; #[cfg(windows)] +#[cfg(not(feature = "no-tty"))] pub(crate) use self::windows::{ move_down, move_left, move_right, move_to, move_to_column, move_to_next_line, move_to_previous_line, move_to_row, move_up, restore_position, save_position, show_cursor, }; #[cfg(windows)] +#[cfg(not(feature = "no-tty"))] pub(crate) mod windows; #[cfg(unix)] +#[cfg(not(feature = "no-tty"))] #[cfg(feature = "events")] pub(crate) mod unix; + +#[cfg(unix)] +#[cfg(feature = "no-tty")] +#[cfg(feature = "events")] +pub(crate) mod no_tty; diff --git a/src/cursor/sys/no_tty.rs b/src/cursor/sys/no_tty.rs new file mode 100644 index 00000000..95458d98 --- /dev/null +++ b/src/cursor/sys/no_tty.rs @@ -0,0 +1,49 @@ +use std::{ + io::{self, Error, ErrorKind}, + time::Duration, +}; + +use crate::event::{ + filter::CursorPositionFilter, internal::InternalEvent, internal_no_tty::NoTtyEvent, +}; + +use crossbeam_channel::RecvTimeoutError; +/// Returns the cursor position (column, row). +/// +/// The top left cell is represented as `(0, 0)`. +/// +/// On unix systems, this function will block and possibly time out while +/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called. +pub fn position(event: &NoTtyEvent) -> io::Result<(u16, u16)> { + // Use `ESC [ 6 n` to and retrieve the cursor position. + event + .send + .send_timeout(b"\x1B[6n".into(), Duration::from_secs(1)) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + loop { + match event.poll(Some(Duration::from_millis(2000)), &CursorPositionFilter) { + Ok(true) => { + if let Ok(InternalEvent::CursorPosition(x, y)) = event.read(&CursorPositionFilter) { + return Ok((x, y)); + } + } + Ok(false) => { + return Err(Error::new( + ErrorKind::Other, + "The cursor position could not be read within a normal duration", + )); + } + Err(e) if e.kind() == io::ErrorKind::Other => { + if Some(RecvTimeoutError::Disconnected) + == e.get_ref() + .and_then(|src| src.downcast_ref::()) + .copied() + { + return Err(e); + } + } + Err(_) => {} + } + } +} diff --git a/src/event.rs b/src/event.rs index 7d5e9ebe..342010ac 100644 --- a/src/event.rs +++ b/src/event.rs @@ -123,13 +123,22 @@ pub(crate) mod internal; pub(crate) mod read; pub(crate) mod source; #[cfg(feature = "event-stream")] +#[cfg(not(feature = "no-tty"))] pub(crate) mod stream; pub(crate) mod sys; pub(crate) mod timeout; +#[cfg(unix)] +#[cfg(feature = "no-tty")] +pub mod internal_no_tty; +#[cfg(unix)] +#[cfg(feature = "no-tty")] +pub use internal_no_tty::{NoTtyEvent, SenderWriter}; + #[cfg(feature = "derive-more")] use derive_more::derive::IsVariant; #[cfg(feature = "event-stream")] +#[cfg(not(feature = "no-tty"))] pub use stream::EventStream; use crate::{ @@ -182,10 +191,17 @@ use std::hash::{Hash, Hasher}; /// poll(Duration::from_millis(100)) /// } /// ``` +#[cfg(not(feature = "no-tty"))] pub fn poll(timeout: Duration) -> std::io::Result { internal::poll(Some(timeout), &EventFilter) } +#[cfg(unix)] +#[cfg(feature = "no-tty")] +pub fn poll(event: &NoTtyEvent, timeout: Duration) -> std::io::Result { + event.poll(Some(timeout), &EventFilter) +} + /// Reads a single [`Event`](enum.Event.html). /// /// This function blocks until an [`Event`](enum.Event.html) is available. Combine it with the @@ -227,6 +243,7 @@ pub fn poll(timeout: Duration) -> std::io::Result { /// } /// } /// ``` +#[cfg(not(feature = "no-tty"))] pub fn read() -> std::io::Result { match internal::read(&EventFilter)? { InternalEvent::Event(event) => Ok(event), @@ -235,6 +252,15 @@ pub fn read() -> std::io::Result { } } +#[cfg(unix)] +#[cfg(feature = "no-tty")] +pub fn read(event: &NoTtyEvent) -> std::io::Result { + match event.read(&EventFilter)? { + InternalEvent::Event(event) => Ok(event), + _ => unreachable!(), + } +} + /// Attempts to read a single [`Event`](enum.Event.html) without blocking the thread. /// /// If no event is found, `None` is returned. @@ -256,6 +282,7 @@ pub fn read() -> std::io::Result { /// } /// } /// ``` +#[cfg(not(feature = "no-tty"))] pub fn try_read() -> Option { match internal::try_read(&EventFilter) { Some(InternalEvent::Event(event)) => Some(event), @@ -265,6 +292,16 @@ pub fn try_read() -> Option { } } +#[cfg(unix)] +#[cfg(feature = "no-tty")] +pub fn try_read(event: &NoTtyEvent) -> Option { + match event.try_read(&EventFilter) { + Some(InternalEvent::Event(event)) => Some(event), + None => None, + _ => unreachable!(), + } +} + bitflags! { /// Represents special flags that tell compatible terminals to add extra information to keyboard events. /// @@ -319,11 +356,13 @@ impl Command for EnableMouseCapture { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::windows::enable_mouse_capture() } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn is_ansi_code_supported(&self) -> bool { false } @@ -348,11 +387,13 @@ impl Command for DisableMouseCapture { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::windows::disable_mouse_capture() } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn is_ansi_code_supported(&self) -> bool { false } @@ -372,6 +413,7 @@ impl Command for EnableFocusChange { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { // Focus events are always enabled on Windows Ok(()) @@ -388,6 +430,7 @@ impl Command for DisableFocusChange { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { // Focus events can't be disabled on Windows Ok(()) @@ -411,6 +454,7 @@ impl Command for EnableBracketedPaste { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { Err(std::io::Error::new( std::io::ErrorKind::Unsupported, @@ -431,6 +475,7 @@ impl Command for DisableBracketedPaste { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) } @@ -482,6 +527,7 @@ impl Command for PushKeyboardEnhancementFlags { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { use std::io; @@ -492,6 +538,7 @@ impl Command for PushKeyboardEnhancementFlags { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn is_ansi_code_supported(&self) -> bool { false } @@ -511,6 +558,7 @@ impl Command for PopKeyboardEnhancementFlags { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { use std::io; @@ -521,6 +569,7 @@ impl Command for PopKeyboardEnhancementFlags { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn is_ansi_code_supported(&self) -> bool { false } diff --git a/src/event/filter.rs b/src/event/filter.rs index b939a297..38d01f45 100644 --- a/src/event/filter.rs +++ b/src/event/filter.rs @@ -56,6 +56,7 @@ impl Filter for EventFilter { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn eval(&self, _: &InternalEvent) -> bool { true } diff --git a/src/event/internal.rs b/src/event/internal.rs index 1d6f9504..8142b78f 100644 --- a/src/event/internal.rs +++ b/src/event/internal.rs @@ -1,21 +1,28 @@ +#[cfg(not(feature = "no-tty"))] use std::time::Duration; +#[cfg(not(feature = "no-tty"))] use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; +use crate::event::Event; #[cfg(unix)] use crate::event::KeyboardEnhancementFlags; -use crate::event::{filter::Filter, read::InternalEventReader, timeout::PollTimeout, Event}; +#[cfg(not(feature = "no-tty"))] +use crate::event::{filter::Filter, read::InternalEventReader, timeout::PollTimeout}; /// Static instance of `InternalEventReader`. /// This needs to be static because there can be one event reader. +#[cfg(not(feature = "no-tty"))] static EVENT_READER: Mutex> = parking_lot::const_mutex(None); +#[cfg(not(feature = "no-tty"))] pub(crate) fn lock_event_reader() -> MappedMutexGuard<'static, InternalEventReader> { MutexGuard::map(EVENT_READER.lock(), |reader| { reader.get_or_insert_with(InternalEventReader::default) }) } +#[cfg(not(feature = "no-tty"))] fn try_lock_event_reader_for( duration: Duration, ) -> Option> { @@ -26,6 +33,7 @@ fn try_lock_event_reader_for( } /// Polls to check if there are any `InternalEvent`s that can be read within the given duration. +#[cfg(not(feature = "no-tty"))] pub(crate) fn poll(timeout: Option, filter: &F) -> std::io::Result where F: Filter, @@ -44,6 +52,7 @@ where } /// Reads a single `InternalEvent`. +#[cfg(not(feature = "no-tty"))] pub(crate) fn read(filter: &F) -> std::io::Result where F: Filter, @@ -53,6 +62,7 @@ where } /// Reads a single `InternalEvent`. Non-blocking. +#[cfg(not(feature = "no-tty"))] pub(crate) fn try_read(filter: &F) -> Option where F: Filter, diff --git a/src/event/internal_no_tty.rs b/src/event/internal_no_tty.rs new file mode 100644 index 00000000..6311899f --- /dev/null +++ b/src/event/internal_no_tty.rs @@ -0,0 +1,97 @@ +use super::internal::InternalEvent; +use crate::event::source::no_tty::NoTtyInternalEventSource; +use crate::event::source::EventSource; +use crate::event::{filter::Filter, read::InternalEventReader, timeout::PollTimeout}; +use crate::terminal::WindowSize; +use crossbeam_channel::{bounded, Receiver, Sender}; +use parking_lot::Mutex; +use std::sync::Arc; +use std::time::Duration; + +#[derive(Clone)] +pub struct NoTtyEvent { + pub(crate) send: Sender>, + pub window_size: Arc>, + inner: Arc>, +} + +impl NoTtyEvent { + pub fn new(recv: Receiver>) -> (Self, Receiver>) { + let (s, r) = bounded(0); + let source = NoTtyInternalEventSource::new(recv); + let source = source.ok().map(|x| Box::new(x) as Box); + let event = InternalEventReader::default().with_source(source); + + ( + Self { + send: s, + window_size: Arc::new(Mutex::new(WindowSize { + rows: 0, + columns: 0, + width: 0, + height: 0, + })), + inner: Arc::new(Mutex::new(event)), + }, + r, + ) + } + /// Polls to check if there are any `InternalEvent`s that can be read within the given duration. + pub(crate) fn poll(&self, timeout: Option, filter: &F) -> std::io::Result + where + F: Filter, + { + let (mut reader, timeout) = if let Some(timeout) = timeout { + let poll_timeout = PollTimeout::new(Some(timeout)); + if let Some(reader) = self.inner.try_lock_for(timeout) { + (reader, poll_timeout.leftover()) + } else { + return Ok(false); + } + } else { + (self.inner.lock(), None) + }; + reader.poll(timeout, filter) + } + + /// Reads a single `InternalEvent`. + pub(crate) fn read(&self, filter: &F) -> std::io::Result + where + F: Filter, + { + let mut reader = self.inner.lock(); + reader.read(filter) + } + + /// Reads a single `InternalEvent`. Non-blocking. + pub(crate) fn try_read(&self, filter: &F) -> Option + where + F: Filter, + { + let mut reader = self.inner.lock(); + reader.try_read(filter) + } +} + +#[derive(Clone)] +pub struct SenderWriter(tokio::sync::mpsc::Sender>); + +impl SenderWriter { + pub fn new(sender: tokio::sync::mpsc::Sender>) -> Self { + Self(sender) + } +} + +impl std::io::Write for SenderWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0 + .blocking_send(buf.to_vec()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + // mpsc is unbuffered; nothing to flush + Ok(()) + } +} diff --git a/src/event/read.rs b/src/event/read.rs index d82fa027..f9c3886b 100644 --- a/src/event/read.rs +++ b/src/event/read.rs @@ -1,10 +1,13 @@ use std::{collections::vec_deque::VecDeque, io, time::Duration}; #[cfg(unix)] +#[cfg(not(feature = "no-tty"))] use crate::event::source::unix::UnixInternalEventSource; #[cfg(windows)] +#[cfg(not(feature = "no-tty"))] use crate::event::source::windows::WindowsEventSource; #[cfg(feature = "event-stream")] +#[cfg(not(feature = "no-tty"))] use crate::event::sys::Waker; use crate::event::{ filter::Filter, internal::InternalEvent, source::EventSource, timeout::PollTimeout, @@ -20,10 +23,16 @@ pub(crate) struct InternalEventReader { impl Default for InternalEventReader { fn default() -> Self { #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] let source = WindowsEventSource::new(); #[cfg(unix)] + #[cfg(not(feature = "no-tty"))] let source = UnixInternalEventSource::new(); + #[cfg(unix)] + #[cfg(feature = "no-tty")] + let source = None; + #[cfg(not(feature = "no-tty"))] let source = source.ok().map(|x| Box::new(x) as Box); InternalEventReader { @@ -37,6 +46,7 @@ impl Default for InternalEventReader { impl InternalEventReader { /// Returns a `Waker` allowing to wake/force the `poll` method to return `Ok(false)`. #[cfg(feature = "event-stream")] + #[cfg(not(feature = "no-tty"))] pub(crate) fn waker(&self) -> Waker { self.source.as_ref().expect("reader source not set").waker() } @@ -143,6 +153,13 @@ impl InternalEventReader { result } + + #[cfg(unix)] + #[cfg(feature = "no-tty")] + pub(crate) fn with_source(mut self, source: Option>) -> Self { + self.source = source; + self + } } #[cfg(test)] @@ -471,6 +488,7 @@ mod tests { } #[cfg(feature = "event-stream")] + #[cfg(not(feature = "no-tty"))] fn waker(&self) -> super::super::sys::Waker { unimplemented!(); } diff --git a/src/event/source.rs b/src/event/source.rs index 12b84d90..e88bb34b 100644 --- a/src/event/source.rs +++ b/src/event/source.rs @@ -2,11 +2,17 @@ use std::{io, time::Duration}; use super::internal::InternalEvent; #[cfg(feature = "event-stream")] +#[cfg(not(feature = "no-tty"))] use super::sys::Waker; #[cfg(unix)] +#[cfg(feature = "no-tty")] +pub(crate) mod no_tty; +#[cfg(unix)] +#[cfg(not(feature = "no-tty"))] pub(crate) mod unix; #[cfg(windows)] +#[cfg(not(feature = "no-tty"))] pub(crate) mod windows; /// An interface for trying to read an `InternalEvent` within an optional `Duration`. @@ -23,5 +29,6 @@ pub(crate) trait EventSource: Sync + Send { /// Returns a `Waker` allowing to wake/force the `try_read` method to return `Ok(None)`. #[cfg(feature = "event-stream")] + #[cfg(not(feature = "no-tty"))] fn waker(&self) -> Waker; } diff --git a/src/event/source/no_tty.rs b/src/event/source/no_tty.rs new file mode 100644 index 00000000..0fd4f568 --- /dev/null +++ b/src/event/source/no_tty.rs @@ -0,0 +1,128 @@ +use std::{collections::VecDeque, io, time::Duration}; + +use crossbeam_channel::{Receiver, RecvTimeoutError}; + +use crate::event::{ + internal::InternalEvent, source::EventSource, sys::unix::parse::parse_event, + timeout::PollTimeout, +}; + +pub struct NoTtyInternalEventSource { + parser: Parser, + recv: Receiver>, +} + +impl NoTtyInternalEventSource { + pub fn new(recv: Receiver>) -> io::Result { + Ok(NoTtyInternalEventSource { + parser: Parser::default(), + recv, + }) + } +} + +impl EventSource for NoTtyInternalEventSource { + fn try_read(&mut self, timeout: Option) -> io::Result> { + if let Some(event) = self.parser.next() { + return Ok(Some(event)); + } + + let timeout = PollTimeout::new(timeout); + + loop { + let t = timeout + .leftover() + .unwrap_or(std::time::Duration::from_secs(u64::MAX)); + let data = match self.recv.recv_timeout(t) { + Ok(d) => d, + Err(RecvTimeoutError::Timeout) => return Ok(None), + // NOTE: fake io error + Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), + }; + + if data.is_empty() { + return Ok(None); + } + self.parser.advance(&data, false); + + if let Some(event) = self.parser.next() { + return Ok(Some(event)); + } + + // Processing above can take some time, check if timeout expired + if timeout.elapsed() { + return Ok(None); + } + } + } +} + +// +// Following `Parser` structure exists for two reasons: +// +// * mimic anes Parser interface +// * move the advancing, parsing, ... stuff out of the `try_read` method +// +#[derive(Debug)] +struct Parser { + buffer: Vec, + internal_events: VecDeque, +} + +impl Default for Parser { + fn default() -> Self { + Parser { + // This buffer is used for -> 1 <- ANSI escape sequence. Are we + // aware of any ANSI escape sequence that is bigger? Can we make + // it smaller? + // + // Probably not worth spending more time on this as "there's a plan" + // to use the anes crate parser. + buffer: Vec::with_capacity(256), + // TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can + // fit? What is an average sequence length? Let's guess here + // and say that the average ANSI escape sequence length is 8 bytes. Thus + // the buffer size should be 1024/8=128 to avoid additional allocations + // when processing large amounts of data. + // + // There's no need to make it bigger, because when you look at the `try_read` + // method implementation, all events are consumed before the next TTY_BUFFER + // is processed -> events pushed. + internal_events: VecDeque::with_capacity(128), + } + } +} + +impl Parser { + fn advance(&mut self, buffer: &[u8], more: bool) { + for (idx, byte) in buffer.iter().enumerate() { + let more = idx + 1 < buffer.len() || more; + + self.buffer.push(*byte); + + match parse_event(&self.buffer, more) { + Ok(Some(ie)) => { + self.internal_events.push_back(ie); + self.buffer.clear(); + } + Ok(None) => { + // Event can't be parsed, because we don't have enough bytes for + // the current sequence. Keep the buffer and process next bytes. + } + Err(_) => { + // Event can't be parsed (not enough parameters, parameter is not a number, ...). + // Clear the buffer and continue with another sequence. + self.buffer.clear(); + } + } + } + } +} + +impl Iterator for Parser { + type Item = InternalEvent; + + fn next(&mut self) -> Option { + self.internal_events.pop_front() + } +} diff --git a/src/event/sys.rs b/src/event/sys.rs index bd793073..c66bb2cc 100644 --- a/src/event/sys.rs +++ b/src/event/sys.rs @@ -1,9 +1,12 @@ #[cfg(all(unix, feature = "event-stream"))] +#[cfg(not(feature = "no-tty"))] pub(crate) use unix::waker::Waker; #[cfg(all(windows, feature = "event-stream"))] +#[cfg(not(feature = "no-tty"))] pub(crate) use windows::waker::Waker; #[cfg(unix)] pub(crate) mod unix; #[cfg(windows)] +#[cfg(not(feature = "no-tty"))] pub(crate) mod windows; diff --git a/src/event/sys/unix.rs b/src/event/sys/unix.rs index 2106ca0c..4451a8a8 100644 --- a/src/event/sys/unix.rs +++ b/src/event/sys/unix.rs @@ -1,4 +1,5 @@ #[cfg(feature = "event-stream")] +#[cfg(not(feature = "no-tty"))] pub(crate) mod waker; #[cfg(feature = "events")] diff --git a/src/event/sys/unix/parse.rs b/src/event/sys/unix/parse.rs index 8897096c..40af23a2 100644 --- a/src/event/sys/unix/parse.rs +++ b/src/event/sys/unix/parse.rs @@ -207,6 +207,15 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result> { } } } + #[cfg(unix)] + #[cfg(feature = "no-tty")] + b'W' => { + if b'R' == buffer[buffer.len() - 1] { + return parse_csi_win_size(buffer); + } else { + None + } + } _ => return Err(could_not_parse_event_error()), }; @@ -345,6 +354,26 @@ fn parse_key_event_kind(kind: u8) -> KeyEventKind { } } +#[cfg(unix)] +#[cfg(feature = "no-tty")] +pub(crate) fn parse_csi_win_size(buffer: &[u8]) -> io::Result> { + // ESC [ Wx ; Wy R + // Wx - window row number (starting from 1) + // Wy - window column number (starting from 1) + assert!(buffer.starts_with(b"\x1B[W")); // ESC [ + assert!(buffer.ends_with(b"R")); + + let s = std::str::from_utf8(&buffer[3..buffer.len() - 1]) + .map_err(|_| could_not_parse_event_error())?; + + let mut split = s.split(';'); + + let x = next_parsed::(&mut split)?; + let y = next_parsed::(&mut split)?; + + Ok(Some(InternalEvent::Event(Event::Resize(x, y)))) +} + pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result> { assert!(buffer.starts_with(b"\x1B[")); // ESC [ // diff --git a/src/lib.rs b/src/lib.rs index 0f6a990a..5ab87917 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -255,10 +255,12 @@ pub mod tty; pub mod clipboard; #[cfg(windows)] +#[cfg(not(feature = "no-tty"))] /// A module that exposes one function to check if the current terminal supports ANSI sequences. pub mod ansi_support; mod command; pub(crate) mod macros; #[cfg(all(windows, not(feature = "windows")))] +#[cfg(not(feature = "no-tty"))] compile_error!("Compiling on Windows with \"windows\" feature disabled. Feature \"windows\" should only be disabled when project will never be compiled on Windows."); diff --git a/src/macros.rs b/src/macros.rs index 47ded985..1975b371 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -178,6 +178,7 @@ mod tests { } #[cfg(not(windows))] + #[cfg(not(feature = "no-tty"))] mod unix { use std::fmt; @@ -242,6 +243,7 @@ mod tests { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] mod windows { use std::fmt; diff --git a/src/style.rs b/src/style.rs index 034b54ad..76091577 100644 --- a/src/style.rs +++ b/src/style.rs @@ -162,6 +162,7 @@ pub fn style(val: D) -> StyledContent { /// This does not always provide a good result. pub fn available_color_count() -> u16 { #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] { // Check if we're running in a pseudo TTY, which supports true color. // Fall back to env vars otherwise for other terminals on Windows. @@ -211,6 +212,7 @@ impl Command for SetForegroundColor { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::windows::set_foreground_color(self.0) } @@ -235,6 +237,7 @@ impl Command for SetBackgroundColor { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::windows::set_background_color(self.0) } @@ -259,6 +262,7 @@ impl Command for SetUnderlineColor { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { Err(std::io::Error::new( std::io::ErrorKind::Other, @@ -314,6 +318,7 @@ impl Command for SetColors { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { if let Some(color) = self.0.foreground { sys::windows::set_foreground_color(color)?; @@ -341,6 +346,7 @@ impl Command for SetAttribute { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { // attributes are not supported by WinAPI. Ok(()) @@ -368,6 +374,7 @@ impl Command for SetAttributes { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { // attributes are not supported by WinAPI. Ok(()) @@ -401,11 +408,13 @@ impl Command for SetStyle { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { panic!("tried to execute SetStyle command using WinAPI, use ANSI instead"); } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn is_ansi_code_supported(&self) -> bool { true } @@ -468,6 +477,7 @@ impl Command for PrintStyledContent { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) } @@ -487,6 +497,7 @@ impl Command for ResetColor { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { sys::windows::reset() } @@ -504,11 +515,13 @@ impl Command for Print { } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn execute_winapi(&self) -> std::io::Result<()> { panic!("tried to execute Print command using WinAPI, use ANSI instead"); } #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn is_ansi_code_supported(&self) -> bool { true } @@ -543,6 +556,7 @@ mod tests { macro_rules! skip_windows_ansi_supported { () => { #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] { if crate::ansi_support::supports_ansi() { return; @@ -553,6 +567,7 @@ mod tests { #[cfg_attr(windows, test)] #[cfg(windows)] + #[cfg(not(feature = "no-tty"))] fn windows_always_truecolor() { // This should always be true on supported Windows 10+, // but downlevel Windows clients and other terminals may fail `cargo test` otherwise. diff --git a/src/style/sys.rs b/src/style/sys.rs index 5a542762..c5d9edaf 100644 --- a/src/style/sys.rs +++ b/src/style/sys.rs @@ -1,2 +1,3 @@ #[cfg(windows)] +#[cfg(not(feature = "no-tty"))] pub(crate) mod windows; diff --git a/src/terminal.rs b/src/terminal.rs index 3aebc1f8..d7fabfbd 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -85,11 +85,11 @@ use std::{fmt, io}; -#[cfg(windows)] +#[cfg(all(windows, not(feature = "no-tty")))] use crossterm_winapi::{ConsoleMode, Handle, ScreenBuffer}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -#[cfg(windows)] +#[cfg(all(windows, not(feature = "no-tty")))] use winapi::um::wincon::ENABLE_WRAP_AT_EOL_OUTPUT; #[doc(no_inline)] @@ -105,12 +105,16 @@ pub use sys::supports_keyboard_enhancement; /// /// Please have a look at the [raw mode](./index.html#raw-mode) section. pub fn is_raw_mode_enabled() -> io::Result { - #[cfg(unix)] + #[cfg(feature = "no-tty")] + { + Ok(sys::is_raw_mode_enabled()) + } + #[cfg(all(unix, not(feature = "no-tty")))] { Ok(sys::is_raw_mode_enabled()) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] { sys::is_raw_mode_enabled() } @@ -133,9 +137,15 @@ pub fn disable_raw_mode() -> io::Result<()> { /// Returns the terminal size `(columns, rows)`. /// /// The top left cell is represented `(1, 1)`. +#[cfg(not(feature = "no-tty"))] pub fn size() -> io::Result<(u16, u16)> { sys::size() } +#[cfg(unix)] +#[cfg(feature = "no-tty")] +pub fn size(event: &crate::event::internal_no_tty::NoTtyEvent) -> io::Result<(u16, u16)> { + sys::size(event) +} #[derive(Debug)] pub struct WindowSize { @@ -150,10 +160,17 @@ pub struct WindowSize { /// The width and height in pixels may not be reliably implemented or default to 0. /// For unix, documents them as "unused". /// For windows it is not implemented. +#[cfg(not(feature = "no-tty"))] pub fn window_size() -> io::Result { sys::window_size() } +#[cfg(unix)] +#[cfg(feature = "no-tty")] +pub fn window_size(event: &crate::event::internal_no_tty::NoTtyEvent) -> io::Result { + sys::window_size(event) +} + /// Disables line wrapping. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DisableLineWrap; @@ -163,7 +180,7 @@ impl Command for DisableLineWrap { f.write_str(csi!("?7l")) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { let screen_buffer = ScreenBuffer::current()?; let console_mode = ConsoleMode::from(screen_buffer.handle().clone()); @@ -182,7 +199,7 @@ impl Command for EnableLineWrap { f.write_str(csi!("?7h")) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { let screen_buffer = ScreenBuffer::current()?; let console_mode = ConsoleMode::from(screen_buffer.handle().clone()); @@ -222,7 +239,7 @@ impl Command for EnterAlternateScreen { f.write_str(csi!("?1049h")) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { let alternate_screen = ScreenBuffer::create()?; alternate_screen.show()?; @@ -260,7 +277,7 @@ impl Command for LeaveAlternateScreen { f.write_str(csi!("?1049l")) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { let screen_buffer = ScreenBuffer::from(Handle::current_out_handle()?); screen_buffer.show()?; @@ -302,7 +319,7 @@ impl Command for ScrollUp { Ok(()) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { sys::scroll_up(self.0) } @@ -324,7 +341,7 @@ impl Command for ScrollDown { Ok(()) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { sys::scroll_down(self.0) } @@ -352,7 +369,7 @@ impl Command for Clear { }) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { sys::clear(self.0) } @@ -371,7 +388,7 @@ impl Command for SetSize { write!(f, csi!("8;{};{}t"), self.1, self.0) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { sys::set_size(self.0, self.1) } @@ -390,7 +407,7 @@ impl Command for SetTitle { write!(f, "\x1B]0;{}\x07", &self.0) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { sys::set_window_title(&self.0) } @@ -437,12 +454,12 @@ impl Command for BeginSynchronizedUpdate { f.write_str(csi!("?2026h")) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { Ok(()) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] #[inline] fn is_ansi_code_supported(&self) -> bool { true @@ -490,12 +507,12 @@ impl Command for EndSynchronizedUpdate { f.write_str(csi!("?2026l")) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] fn execute_winapi(&self) -> io::Result<()> { Ok(()) } - #[cfg(windows)] + #[cfg(all(windows, not(feature = "no-tty")))] #[inline] fn is_ansi_code_supported(&self) -> bool { true @@ -509,8 +526,10 @@ impl_display!(for Clear); #[cfg(test)] mod tests { + #[cfg(not(feature = "no-tty"))] use std::{io::stdout, thread, time}; + #[cfg(not(feature = "no-tty"))] use crate::execute; use super::*; @@ -518,6 +537,7 @@ mod tests { // Test is disabled, because it's failing on Travis CI #[test] #[ignore] + #[cfg(not(feature = "no-tty"))] fn test_resize_ansi() { let (width, height) = size().unwrap(); diff --git a/src/terminal/sys.rs b/src/terminal/sys.rs index 9dde47d0..b68800ae 100644 --- a/src/terminal/sys.rs +++ b/src/terminal/sys.rs @@ -2,26 +2,45 @@ #[cfg(unix)] #[cfg(feature = "events")] +#[cfg(not(feature = "no-tty"))] pub use self::unix::supports_keyboard_enhancement; #[cfg(unix)] +#[cfg(not(feature = "no-tty"))] pub(crate) use self::unix::{ disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size, window_size, }; -#[cfg(windows)] +#[cfg(all(windows, not(feature = "no-tty")))] #[cfg(feature = "events")] pub use self::windows::supports_keyboard_enhancement; -#[cfg(all(windows, test))] +#[cfg(all(windows, test, not(feature = "no-tty")))] pub(crate) use self::windows::temp_screen_buffer; -#[cfg(windows)] +#[cfg(all(windows, not(feature = "no-tty")))] pub(crate) use self::windows::{ clear, disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, scroll_down, scroll_up, set_size, set_window_title, size, window_size, }; -#[cfg(windows)] +#[cfg(all(windows, not(feature = "no-tty")))] mod windows; #[cfg(unix)] +#[cfg(not(feature = "no-tty"))] pub mod file_descriptor; #[cfg(unix)] +#[cfg(not(feature = "no-tty"))] mod unix; + +#[cfg(unix)] +#[cfg(feature = "no-tty")] +pub(crate) use self::no_tty::{ + disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size, window_size, +}; + +#[cfg(unix)] +#[cfg(feature = "no-tty")] +mod no_tty; + +#[cfg(unix)] +#[cfg(feature = "events")] +#[cfg(feature = "no-tty")] +pub use self::no_tty::supports_keyboard_enhancement; diff --git a/src/terminal/sys/no_tty.rs b/src/terminal/sys/no_tty.rs new file mode 100644 index 00000000..1381ffa3 --- /dev/null +++ b/src/terminal/sys/no_tty.rs @@ -0,0 +1,129 @@ +//! Non-terminal related logic for terminal manipulation. + +use crate::event::internal_no_tty::NoTtyEvent; +#[cfg(feature = "events")] +use crate::event::KeyboardEnhancementFlags; +use crate::terminal::WindowSize; +use std::io; + +pub(crate) fn is_raw_mode_enabled() -> bool { + true +} + +pub(crate) fn window_size(event: &NoTtyEvent) -> io::Result { + let win = event.window_size.lock(); + let size = WindowSize { + rows: win.rows, + columns: win.columns, + width: win.width, + height: win.height, + }; + Ok(size) +} + +#[allow(clippy::useless_conversion)] +pub(crate) fn size(event: &NoTtyEvent) -> io::Result<(u16, u16)> { + match window_size(event) { + Ok(window_size) => Ok((window_size.columns, window_size.rows)), + Err(e) => Err(e), + } +} + +pub(crate) fn enable_raw_mode() -> io::Result<()> { + Ok(()) +} + +/// Reset the raw mode. +/// +/// More precisely, reset the whole termios mode to what it was before the first call +/// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's +/// effectively disabling the raw mode and doing nothing else. +pub(crate) fn disable_raw_mode() -> io::Result<()> { + Ok(()) +} + +/// Queries the terminal's support for progressive keyboard enhancement. +/// +/// On unix systems, this function will block and possibly time out while +/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called. +#[cfg(feature = "events")] +pub fn supports_keyboard_enhancement(event: &NoTtyEvent) -> io::Result { + query_keyboard_enhancement_flags(event).map(|flags| flags.is_some()) +} + +/// Queries the terminal's currently active keyboard enhancement flags. +/// +/// On unix systems, this function will block and possibly time out while +/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called. +#[cfg(feature = "events")] +pub fn query_keyboard_enhancement_flags( + event: &NoTtyEvent, +) -> io::Result> { + if is_raw_mode_enabled() { + query_keyboard_enhancement_flags_raw(event) + } else { + query_keyboard_enhancement_flags_nonraw(event) + } +} + +#[cfg(feature = "events")] +fn query_keyboard_enhancement_flags_nonraw( + event: &NoTtyEvent, +) -> io::Result> { + enable_raw_mode()?; + let flags = query_keyboard_enhancement_flags_raw(event); + disable_raw_mode()?; + flags +} + +#[cfg(feature = "events")] +fn query_keyboard_enhancement_flags_raw( + event: &NoTtyEvent, +) -> io::Result> { + use crate::event::{ + filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter}, + internal::InternalEvent, + }; + use std::time::Duration; + + // This is the recommended method for testing support for the keyboard enhancement protocol. + // We send a query for the flags supported by the terminal and then the primary device attributes + // query. If we receive the primary device attributes response but not the keyboard enhancement + // flags, none of the flags are supported. + // + // See + + // ESC [ ? u Query progressive keyboard enhancement flags (kitty protocol). + // ESC [ c Query primary device attributes. + const QUERY: &[u8] = b"\x1B[?u\x1B[c"; + + event + .send + .send_timeout(QUERY.into(), Duration::from_secs(1)) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + loop { + match event.poll( + Some(Duration::from_millis(2000)), + &KeyboardEnhancementFlagsFilter, + ) { + Ok(true) => { + match event.read(&KeyboardEnhancementFlagsFilter) { + Ok(InternalEvent::KeyboardEnhancementFlags(current_flags)) => { + // Flush the PrimaryDeviceAttributes out of the event queue. + event.read(&PrimaryDeviceAttributesFilter).ok(); + return Ok(Some(current_flags)); + } + _ => return Ok(None), + } + } + Ok(false) => { + return Err(io::Error::new( + io::ErrorKind::Other, + "The keyboard enhancement status could not be read within a normal duration", + )); + } + Err(_) => {} + } + } +} diff --git a/src/tty.rs b/src/tty.rs index 5a710b4a..9626f159 100644 --- a/src/tty.rs +++ b/src/tty.rs @@ -3,12 +3,12 @@ //! This module defines the IsTty trait and the is_tty method to //! return true if the item represents a terminal. -#[cfg(unix)] +#[cfg(all(unix, not(feature = "no-tty")))] use std::os::unix::io::AsRawFd; -#[cfg(windows)] +#[cfg(all(windows, not(feature = "no-tty")))] use std::os::windows::io::AsRawHandle; -#[cfg(windows)] +#[cfg(all(windows, not(feature = "no-tty")))] use winapi::um::consoleapi::GetConsoleMode; /// Adds the `is_tty` method to types that might represent a terminal @@ -26,7 +26,7 @@ pub trait IsTty { /// On UNIX, the `isatty()` function returns true if a file /// descriptor is a terminal. -#[cfg(all(unix, feature = "libc"))] +#[cfg(all(unix, feature = "libc", not(feature = "no-tty")))] impl IsTty for S { fn is_tty(&self) -> bool { let fd = self.as_raw_fd(); @@ -34,7 +34,7 @@ impl IsTty for S { } } -#[cfg(all(unix, not(feature = "libc")))] +#[cfg(all(unix, not(feature = "libc"), not(feature = "no-tty")))] impl IsTty for S { fn is_tty(&self) -> bool { let fd = self.as_raw_fd(); @@ -44,7 +44,7 @@ impl IsTty for S { /// On windows, `GetConsoleMode` will return true if we are in a terminal. /// Otherwise false. -#[cfg(windows)] +#[cfg(all(windows, not(feature = "no-tty")))] impl IsTty for S { fn is_tty(&self) -> bool { let mut mode = 0;