/// Append a the first few characters of an ANSI escape code to the given string. #[macro_export] #[doc(hidden)] macro_rules! csi { ($( $l:expr ),*) => { concat!("\x1B[", $( $l ),*) }; } /// Queues one or more command(s) for further execution. /// /// Queued commands must be flushed to the underlying device to be executed. /// This generally happens in the following cases: /// /// * When `flush` is called manually on the given type implementing `io::Write`. /// * The terminal will `flush` automatically if the buffer is full. /// * Each line is flushed in case of `stdout`, because it is line buffered. /// /// # Arguments /// /// - [std::io::Writer](std::io::Write) /// /// ANSI escape codes are written on the given 'writer', after which they are flushed. /// /// - [Command](./trait.Command.html) /// /// One or more commands /// /// # Examples /// /// ```rust /// use std::io::{Write, stdout}; /// use keyfork_crossterm::{queue, style::Print}; /// /// let mut stdout = stdout(); /// /// // `Print` will executed executed when `flush` is called. /// queue!(stdout, Print("foo".to_string())); /// /// // some other code (no execution happening here) ... /// /// // when calling `flush` on `stdout`, all commands will be written to the stdout and therefore executed. /// stdout.flush(); /// /// // ==== Output ==== /// // foo /// ``` /// /// Have a look over at the [Command API](./index.html#command-api) for more details. /// /// # Notes /// /// In case of Windows versions lower than 10, a direct WinAPI call will be made. /// The reason for this is that Windows versions lower than 10 do not support ANSI codes, /// and can therefore not be written to the given `writer`. /// Therefore, there is no difference between [execute](macro.execute.html) /// and [queue](macro.queue.html) for those old Windows versions. /// #[macro_export] macro_rules! queue { ($writer:expr $(, $command:expr)* $(,)?) => {{ use ::std::io::Write; // This allows the macro to take both mut impl Write and &mut impl Write. Ok($writer.by_ref()) $(.and_then(|writer| $crate::QueueableCommand::queue(writer, $command)))* .map(|_| ()) }} } /// Executes one or more command(s). /// /// # Arguments /// /// - [std::io::Writer](std::io::Write) /// /// ANSI escape codes are written on the given 'writer', after which they are flushed. /// /// - [Command](./trait.Command.html) /// /// One or more commands /// /// # Examples /// /// ```rust /// use std::io::{Write, stdout}; /// use keyfork_crossterm::{execute, style::Print}; /// /// // will be executed directly /// execute!(stdout(), Print("sum:\n".to_string())); /// /// // will be executed directly /// execute!(stdout(), Print("1 + 1 = ".to_string()), Print((1+1).to_string())); /// /// // ==== Output ==== /// // sum: /// // 1 + 1 = 2 /// ``` /// /// Have a look over at the [Command API](./index.html#command-api) for more details. /// /// # Notes /// /// * In the case of UNIX and Windows 10, ANSI codes are written to the given 'writer'. /// * In case of Windows versions lower than 10, a direct WinAPI call will be made. /// The reason for this is that Windows versions lower than 10 do not support ANSI codes, /// and can therefore not be written to the given `writer`. /// Therefore, there is no difference between [execute](macro.execute.html) /// and [queue](macro.queue.html) for those old Windows versions. #[macro_export] macro_rules! execute { ($writer:expr $(, $command:expr)* $(,)? ) => {{ use ::std::io::Write; // Queue each command, then flush $crate::queue!($writer $(, $command)*) .and_then(|()| { ::std::io::Write::flush($writer.by_ref()) }) }} } #[doc(hidden)] #[macro_export] macro_rules! impl_display { (for $($t:ty),+) => { $(impl ::std::fmt::Display for $t { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { $crate::command::execute_fmt(f, self) } })* } } #[doc(hidden)] #[macro_export] macro_rules! impl_from { ($from:path, $to:expr) => { impl From<$from> for ErrorKind { fn from(e: $from) -> Self { $to(e) } } }; } #[cfg(test)] mod tests { use std::io; use std::str; // Helper for execute tests to confirm flush #[derive(Default, Debug, Clone)] pub(self) struct FakeWrite { buffer: String, flushed: bool, } impl io::Write for FakeWrite { fn write(&mut self, content: &[u8]) -> io::Result { let content = str::from_utf8(content) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; self.buffer.push_str(content); self.flushed = false; Ok(content.len()) } fn flush(&mut self) -> io::Result<()> { self.flushed = true; Ok(()) } } #[cfg(not(windows))] mod unix { use std::fmt; use super::FakeWrite; use crate::command::Command; pub struct FakeCommand; impl Command for FakeCommand { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { f.write_str("cmd") } } #[test] fn test_queue_one() { let mut result = FakeWrite::default(); queue!(&mut result, FakeCommand).unwrap(); assert_eq!(&result.buffer, "cmd"); assert!(!result.flushed); } #[test] fn test_queue_many() { let mut result = FakeWrite::default(); queue!(&mut result, FakeCommand, FakeCommand).unwrap(); assert_eq!(&result.buffer, "cmdcmd"); assert!(!result.flushed); } #[test] fn test_queue_trailing_comma() { let mut result = FakeWrite::default(); queue!(&mut result, FakeCommand, FakeCommand,).unwrap(); assert_eq!(&result.buffer, "cmdcmd"); assert!(!result.flushed); } #[test] fn test_execute_one() { let mut result = FakeWrite::default(); execute!(&mut result, FakeCommand).unwrap(); assert_eq!(&result.buffer, "cmd"); assert!(result.flushed); } #[test] fn test_execute_many() { let mut result = FakeWrite::default(); execute!(&mut result, FakeCommand, FakeCommand).unwrap(); assert_eq!(&result.buffer, "cmdcmd"); assert!(result.flushed); } #[test] fn test_execute_trailing_comma() { let mut result = FakeWrite::default(); execute!(&mut result, FakeCommand, FakeCommand,).unwrap(); assert_eq!(&result.buffer, "cmdcmd"); assert!(result.flushed); } } #[cfg(windows)] mod windows { use std::fmt; use std::cell::RefCell; use super::FakeWrite; use crate::command::Command; // We need to test two different APIs: WinAPI and the write api. We // don't know until runtime which we're supporting (via // Command::is_ansi_code_supported), so we have to test them both. The // CI environment hopefully includes both versions of windows. // WindowsEventStream is a place for execute_winapi to push strings, // when called. type WindowsEventStream = Vec<&'static str>; struct FakeCommand<'a> { // Need to use a refcell because we want execute_winapi to be able // push to the vector, but execute_winapi take &self. stream: RefCell<&'a mut WindowsEventStream>, value: &'static str, } impl<'a> FakeCommand<'a> { fn new(stream: &'a mut WindowsEventStream, value: &'static str) -> Self { Self { value, stream: RefCell::new(stream), } } } impl<'a> Command for FakeCommand<'a> { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { f.write_str(self.value) } fn execute_winapi(&self) -> std::io::Result<()> { self.stream.borrow_mut().push(self.value); Ok(()) } } // Helper function for running tests against either WinAPI or an // io::Write. // // This function will execute the `test` function, which should // queue some commands against the given FakeWrite and // WindowsEventStream. It will then test that the correct data sink // was populated. It does not currently check is_ansi_code_supported; // for now it simply checks that one of the two streams was correctly // populated. // // If the stream was populated, it tests that the two arrays are equal. // If the writer was populated, it tests that the contents of the // write buffer are equal to the concatenation of `stream_result`. fn test_harness( stream_result: &[&'static str], test: impl FnOnce(&mut FakeWrite, &mut WindowsEventStream) -> std::io::Result<()>, ) { let mut stream = WindowsEventStream::default(); let mut writer = FakeWrite::default(); if let Err(err) = test(&mut writer, &mut stream) { panic!("Error returned from test function: {:?}", err); } // We need this for type inference, for whatever reason. const EMPTY_RESULT: [&str; 0] = []; // TODO: confirm that the correct sink was used, based on // is_ansi_code_supported match (writer.buffer.is_empty(), stream.is_empty()) { (true, true) if stream_result == EMPTY_RESULT => {} (true, true) => panic!( "Neither the event stream nor the writer were populated. Expected {:?}", stream_result ), // writer is populated (false, true) => { // Concat the stream result to find the string result let result: String = stream_result.iter().copied().collect(); assert_eq!(result, writer.buffer); assert_eq!(&stream, &EMPTY_RESULT); } // stream is populated (true, false) => { assert_eq!(stream, stream_result); assert_eq!(writer.buffer, ""); } // Both are populated (false, false) => panic!( "Both the writer and the event stream were written to.\n\ Only one should be used, based on is_ansi_code_supported.\n\ stream: {stream:?}\n\ writer: {writer:?}", stream = stream, writer = writer, ), } } #[test] fn test_queue_one() { test_harness(&["cmd1"], |writer, stream| { queue!(writer, FakeCommand::new(stream, "cmd1")) }) } #[test] fn test_queue_some() { test_harness(&["cmd1", "cmd2"], |writer, stream| { queue!( writer, FakeCommand::new(stream, "cmd1"), FakeCommand::new(stream, "cmd2"), ) }) } #[test] fn test_many_queues() { test_harness(&["cmd1", "cmd2", "cmd3"], |writer, stream| { queue!(writer, FakeCommand::new(stream, "cmd1"))?; queue!(writer, FakeCommand::new(stream, "cmd2"))?; queue!(writer, FakeCommand::new(stream, "cmd3")) }) } } }