diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ceaaad4..2821c238 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,17 +6,17 @@ on: branches: [master] jobs: build-msrv: - name: Build on MSRV (1.48) + name: Build on MSRV (1.56) strategy: fail-fast: false matrix: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu - rust: 1.48.0 + rust: 1.56.0 - os: windows-latest target: i686-pc-windows-msvc - rust: 1.48.0 + rust: 1.56.0 runs-on: ${{ matrix.os }} steps: - name: Install rust @@ -46,16 +46,16 @@ jobs: rust: stable - os: ubuntu-latest target: x86_64-unknown-linux-gnu - rust: 1.51.0 + rust: 1.56.0 - os: ubuntu-latest target: i686-unknown-linux-gnu - rust: 1.51.0 + rust: 1.56.0 - os: windows-latest target: i686-pc-windows-msvc - rust: 1.51.0 + rust: 1.56.0 - os: windows-latest target: x86_64-pc-windows-msvc - rust: 1.51.0 + rust: 1.56.0 - os: ubuntu-latest target: x86_64-unknown-linux-gnu rust: stable @@ -72,7 +72,7 @@ jobs: target: ${{ matrix.target }} override: true - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install linker if: matrix.target == 'i686-unknown-linux-gnu' run: | @@ -89,7 +89,15 @@ jobs: strategy: fail-fast: false matrix: - target: [wasm32-unknown-unknown] + include: + - target: wasm32-unknown-unknown + os: ubuntu-latest + - target: wasm32-unknown-emscripten + os: ubuntu-latest + - target: wasm32-unknown-emscripten + os: windows-latest + - target: wasm32-unknown-emscripten + os: macos-latest runs-on: ubuntu-latest steps: - name: Install rust @@ -100,7 +108,7 @@ jobs: target: ${{ matrix.target }} override: true - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check env: CARGO_BUILD_TARGET: ${{ matrix.target }} @@ -121,7 +129,7 @@ jobs: target: ${{ matrix.target }} override: true - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install WasmTime run: | curl https://wasmtime.dev/install.sh -sSf | bash @@ -144,6 +152,6 @@ jobs: toolchain: nightly override: true - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Test run: make test diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index d86db79e..deb4a1d7 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions-rs/toolchain@v1 with: toolchain: stable diff --git a/.github/workflows/minver.yml b/.github/workflows/minver.yml index ac590604..68e5e73d 100644 --- a/.github/workflows/minver.yml +++ b/.github/workflows/minver.yml @@ -9,7 +9,7 @@ jobs: continue-on-error: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions-rs/toolchain@v1 with: toolchain: nightly diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml index e803f5b3..aaa843cf 100644 --- a/.github/workflows/rustfmt.yml +++ b/.github/workflows/rustfmt.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions-rs/toolchain@v1 with: toolchain: stable diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f13206f..05f735f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.15.8 + +### Enhancements + +* Added `wasm32-unknown-emscripten` target. (#179) +* `read_line_initial_text` now retains the initial prefix. (#190) +* Reading raw input now traps Ctrl+C. (#189) + +### Bugfixes + +* Properly use configured output of `Term` to get terminal size (#186) +* Aligned `read_line` and `read_line_initial_text`'s behavior. (#181) +* Fixed soundness issue in `msys_tty_on`. (#183) + ## 0.15.7 ### Enhancements diff --git a/Cargo.toml b/Cargo.toml index a5daa917..d850f3f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "console" description = "A terminal and console abstraction for Rust" -version = "0.15.7" +version = "0.15.8" keywords = ["cli", "terminal", "colors", "console", "ansi"] authors = ["Armin Ronacher "] license = "MIT" @@ -10,7 +10,7 @@ homepage = "https://github.com/console-rs/console" repository = "https://github.com/console-rs/console" documentation = "https://docs.rs/console" readme = "README.md" -rust-version = "1.48.0" +rust-version = "1.56.0" [features] default = ["unicode-width", "ansi-parsing"] @@ -26,7 +26,7 @@ lazy_static = "1.4.0" encode_unicode = "0.3" [target.'cfg(windows)'.dependencies.windows-sys] -version = "0.45.0" +version = "0.52.0" features = [ "Win32_Foundation", "Win32_System_Console", @@ -36,7 +36,11 @@ features = [ [dev-dependencies] # Pick a setup for proptest that works with wasi -proptest = { version = "1.0.0", default-features = false, features = ["std", "bit-set", "break-dead-code"] } +proptest = { version = "1.0.0", default-features = false, features = [ + "std", + "bit-set", + "break-dead-code", +] } regex = "1.4.2" ## These are currently disabled. If you want to play around with the benchmarks diff --git a/Makefile b/Makefile index ccab99dd..b779cf41 100644 --- a/Makefile +++ b/Makefile @@ -35,5 +35,6 @@ lint: msrv-lock: @cargo update -p proptest --precise=1.0.0 + @cargo update -p byteorder --precise=1.4.0 .PHONY: all doc build check test format format-check lint check-minver msrv-lock diff --git a/README.md b/README.md index 9a046d85..0a04eb07 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://github.com/console-rs/console/workflows/CI/badge.svg?branch=master)](https://github.com/console-rs/console/actions?query=workflow%3ACI) [![Crates.io](https://img.shields.io/crates/d/console.svg)](https://crates.io/crates/console) [![License](https://img.shields.io/github/license/console-rs/console)](https://github.com/console-rs/console/blob/master/LICENSE) -[![rustc 1.48.0](https://img.shields.io/badge/rust-1.48%2B-orange.svg)](https://img.shields.io/badge/rust-1.48%2B-orange.svg) +[![rustc 1.56.0](https://img.shields.io/badge/rust-1.56%2B-orange.svg)](https://img.shields.io/badge/rust-1.56%2B-orange.svg) [![Documentation](https://docs.rs/console/badge.svg)](https://docs.rs/console) **console** is a library for Rust that provides access to various terminal diff --git a/examples/keyboard.rs b/examples/keyboard.rs new file mode 100644 index 00000000..2dae1acd --- /dev/null +++ b/examples/keyboard.rs @@ -0,0 +1,16 @@ +use std::io; + +use console::{Key, Term}; + +fn main() -> io::Result<()> { + let term = Term::stdout(); + term.write_line("Press any key. Esc to exit")?; + loop { + let key = term.read_key()?; + term.write_line(&format!("You pressed {:?}", key))?; + if key == Key::Escape { + break; + } + } + Ok(()) +} diff --git a/src/kb.rs b/src/kb.rs index 5258c135..2a0f61ed 100644 --- a/src/kb.rs +++ b/src/kb.rs @@ -26,4 +26,5 @@ pub enum Key { PageUp, PageDown, Char(char), + CtrlC, } diff --git a/src/lib.rs b/src/lib.rs index 1b18afc0..a1ac2275 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,7 +92,7 @@ pub use crate::ansi::{strip_ansi_codes, AnsiCodeIterator}; mod common_term; mod kb; mod term; -#[cfg(unix)] +#[cfg(all(unix, not(target_arch = "wasm32")))] mod unix_term; mod utils; #[cfg(target_arch = "wasm32")] diff --git a/src/term.rs b/src/term.rs index 0a402585..44e94055 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,6 +1,6 @@ use std::fmt::{Debug, Display}; use std::io::{self, Read, Write}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; #[cfg(unix)] use std::os::unix::io::{AsRawFd, RawFd}; @@ -41,6 +41,8 @@ pub enum TermTarget { pub struct TermInner { target: TermTarget, buffer: Option>>, + prompt: RwLock, + prompt_guard: Mutex<()>, } /// The family of the terminal. @@ -108,7 +110,7 @@ impl<'a> TermFeatures<'a> { { TermFamily::WindowsConsole } - #[cfg(unix)] + #[cfg(all(unix, not(target_arch = "wasm32")))] { TermFamily::UnixTerm } @@ -149,6 +151,8 @@ impl Term { Term::with_inner(TermInner { target: TermTarget::Stdout, buffer: None, + prompt: RwLock::new(String::new()), + prompt_guard: Mutex::new(()), }) } @@ -158,6 +162,8 @@ impl Term { Term::with_inner(TermInner { target: TermTarget::Stderr, buffer: None, + prompt: RwLock::new(String::new()), + prompt_guard: Mutex::new(()), }) } @@ -166,6 +172,8 @@ impl Term { Term::with_inner(TermInner { target: TermTarget::Stdout, buffer: Some(Mutex::new(vec![])), + prompt: RwLock::new(String::new()), + prompt_guard: Mutex::new(()), }) } @@ -174,6 +182,8 @@ impl Term { Term::with_inner(TermInner { target: TermTarget::Stderr, buffer: Some(Mutex::new(vec![])), + prompt: RwLock::new(String::new()), + prompt_guard: Mutex::new(()), }) } @@ -201,6 +211,8 @@ impl Term { style, }), buffer: None, + prompt: RwLock::new(String::new()), + prompt_guard: Mutex::new(()), }) } @@ -231,14 +243,19 @@ impl Term { /// Write a string to the terminal and add a newline. pub fn write_line(&self, s: &str) -> io::Result<()> { + let prompt = self.inner.prompt.read().unwrap(); + if !prompt.is_empty() { + self.clear_line()?; + } match self.inner.buffer { Some(ref mutex) => { let mut buffer = mutex.lock().unwrap(); buffer.extend_from_slice(s.as_bytes()); buffer.push(b'\n'); + buffer.extend_from_slice(prompt.as_bytes()); Ok(()) } - None => self.write_through(format!("{}\n", s).as_bytes()), + None => self.write_through(format!("{}\n{}", s, prompt.as_str()).as_bytes()), } } @@ -275,7 +292,15 @@ impl Term { if !self.is_tty { Ok(Key::Unknown) } else { - read_single_key() + read_single_key(false) + } + } + + pub fn read_key_raw(&self) -> io::Result { + if !self.is_tty { + Ok(Key::Unknown) + } else { + read_single_key(true) } } @@ -284,51 +309,58 @@ impl Term { /// This does not include the trailing newline. If the terminal is not /// user attended the return value will always be an empty string. pub fn read_line(&self) -> io::Result { - if !self.is_tty { - return Ok("".into()); - } - let mut rv = String::new(); - io::stdin().read_line(&mut rv)?; - let len = rv.trim_end_matches(&['\r', '\n'][..]).len(); - rv.truncate(len); - Ok(rv) + self.read_line_initial_text("") } /// Read one line of input with initial text. /// + /// This method blocks until no other thread is waiting for this read_line + /// before reading a line from the terminal. /// This does not include the trailing newline. If the terminal is not /// user attended the return value will always be an empty string. pub fn read_line_initial_text(&self, initial: &str) -> io::Result { if !self.is_tty { return Ok("".into()); } + *self.inner.prompt.write().unwrap() = initial.to_string(); + // use a guard in order to prevent races with other calls to read_line_initial_text + let _guard = self.inner.prompt_guard.lock().unwrap(); + self.write_str(initial)?; - let mut chars: Vec = initial.chars().collect(); + fn read_line_internal(slf: &Term, initial: &str) -> io::Result { + let prefix_len = initial.len(); - loop { - match self.read_key()? { - Key::Backspace => { - if chars.pop().is_some() { - self.clear_chars(1)?; + let mut chars: Vec = initial.chars().collect(); + + loop { + match slf.read_key()? { + Key::Backspace => { + if prefix_len < chars.len() && chars.pop().is_some() { + slf.clear_chars(1)?; + } + slf.flush()?; } - self.flush()?; - } - Key::Char(chr) => { - chars.push(chr); - let mut bytes_char = [0; 4]; - chr.encode_utf8(&mut bytes_char); - self.write_str(chr.encode_utf8(&mut bytes_char))?; - self.flush()?; - } - Key::Enter => { - self.write_line("")?; - break; + Key::Char(chr) => { + chars.push(chr); + let mut bytes_char = [0; 4]; + chr.encode_utf8(&mut bytes_char); + slf.write_str(chr.encode_utf8(&mut bytes_char))?; + slf.flush()?; + } + Key::Enter => { + slf.write_through(format!("\n{}", initial).as_bytes())?; + break; + } + _ => (), } - _ => (), } + Ok(chars.iter().skip(prefix_len).collect::()) } - Ok(chars.iter().collect::()) + let ret = read_line_internal(self, initial); + + *self.inner.prompt.write().unwrap() = String::new(); + ret } /// Read a line of input securely. @@ -624,7 +656,7 @@ impl<'a> Read for &'a Term { } } -#[cfg(unix)] +#[cfg(all(unix, not(target_arch = "wasm32")))] pub use crate::unix_term::*; #[cfg(target_arch = "wasm32")] pub use crate::wasm_term::*; diff --git a/src/unix_term.rs b/src/unix_term.rs index 8e1e5925..271709f2 100644 --- a/src/unix_term.rs +++ b/src/unix_term.rs @@ -5,7 +5,6 @@ use std::io; use std::io::{BufRead, BufReader}; use std::mem; use std::os::unix::io::AsRawFd; -use std::ptr; use std::str; use crate::kb::Key; @@ -46,11 +45,11 @@ pub fn c_result libc::c_int>(f: F) -> io::Result<()> { pub fn terminal_size(out: &Term) -> Option<(u16, u16)> { unsafe { - if libc::isatty(libc::STDOUT_FILENO) != 1 { + if libc::isatty(out.as_raw_fd()) != 1 { return None; } - let mut winsize: libc::winsize = std::mem::zeroed(); + let mut winsize: libc::winsize = mem::zeroed(); // FIXME: ".into()" used as a temporary fix for a libc bug // https://github.com/rust-lang/libc/pull/704 @@ -81,7 +80,7 @@ pub fn read_secure() -> io::Result { } }; - let mut termios = core::mem::MaybeUninit::uninit(); + let mut termios = mem::MaybeUninit::uninit(); c_result(|| unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) })?; let mut termios = unsafe { termios.assume_init() }; let original = termios; @@ -125,7 +124,7 @@ fn select_fd(fd: i32, timeout: i32) -> io::Result { let mut timeout_val; let timeout = if timeout < 0 { - ptr::null_mut() + std::ptr::null_mut() } else { timeout_val = libc::timeval { tv_sec: (timeout / 1000) as _, @@ -139,8 +138,8 @@ fn select_fd(fd: i32, timeout: i32) -> io::Result { let ret = libc::select( fd + 1, &mut read_fd_set, - ptr::null_mut(), - ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), timeout, ); if ret < 0 { @@ -295,7 +294,7 @@ fn read_single_key_impl(fd: i32) -> Result { } } -pub fn read_single_key() -> io::Result { +pub fn read_single_key(ctrlc_key: bool) -> io::Result { let tty_f; let fd = unsafe { if libc::isatty(libc::STDIN_FILENO) == 1 { @@ -321,8 +320,12 @@ pub fn read_single_key() -> io::Result { // if the user hit ^C we want to signal SIGINT to outselves. if let Err(ref err) = rv { if err.kind() == io::ErrorKind::Interrupted { - unsafe { - libc::raise(libc::SIGINT); + if !ctrlc_key { + unsafe { + libc::raise(libc::SIGINT); + } + } else { + return Ok(Key::CtrlC); } } } diff --git a/src/utils.rs b/src/utils.rs index 9e6b942f..cfecc78f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -778,8 +778,8 @@ pub fn truncate_str<'a>(s: &'a str, width: usize, tail: &str) -> Cow<'a, str> { } } (s, true) => { - if rv.is_some() { - rv.as_mut().unwrap().push_str(s); + if let Some(ref mut rv) = rv { + rv.push_str(s); } } } diff --git a/src/wasm_term.rs b/src/wasm_term.rs index 764bc341..13b844b2 100644 --- a/src/wasm_term.rs +++ b/src/wasm_term.rs @@ -39,7 +39,7 @@ pub fn read_secure() -> io::Result { )) } -pub fn read_single_key() -> io::Result { +pub fn read_single_key(_ctrlc_key: bool) -> io::Result { Err(io::Error::new( io::ErrorKind::Other, "unsupported operation", diff --git a/src/windows_term/mod.rs b/src/windows_term/mod.rs index c4ec193c..173f3ef5 100644 --- a/src/windows_term/mod.rs +++ b/src/windows_term/mod.rs @@ -8,15 +8,12 @@ use std::mem; use std::os::raw::c_void; use std::os::windows::ffi::OsStrExt; use std::os::windows::io::AsRawHandle; -use std::slice; use std::{char, mem::MaybeUninit}; use encode_unicode::error::InvalidUtf16Tuple; use encode_unicode::CharExt; -use windows_sys::Win32::Foundation::{CHAR, HANDLE, INVALID_HANDLE_VALUE, MAX_PATH}; -use windows_sys::Win32::Storage::FileSystem::{ - FileNameInfo, GetFileInformationByHandleEx, FILE_NAME_INFO, -}; +use windows_sys::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE, MAX_PATH}; +use windows_sys::Win32::Storage::FileSystem::{FileNameInfo, GetFileInformationByHandleEx}; use windows_sys::Win32::System::Console::{ FillConsoleOutputAttribute, FillConsoleOutputCharacterA, GetConsoleCursorInfo, GetConsoleMode, GetConsoleScreenBufferInfo, GetNumberOfConsoleInputEvents, GetStdHandle, ReadConsoleInputW, @@ -225,7 +222,7 @@ pub fn clear_line(out: &Term) -> io::Result<()> { Y: csbi.dwCursorPosition.Y, }; let mut written = 0; - FillConsoleOutputCharacterA(hand, b' ' as CHAR, width as u32, pos, &mut written); + FillConsoleOutputCharacterA(hand, b' ', width as u32, pos, &mut written); FillConsoleOutputAttribute(hand, csbi.wAttributes, width as u32, pos, &mut written); SetConsoleCursorPosition(hand, pos); } @@ -245,7 +242,7 @@ pub fn clear_chars(out: &Term, n: usize) -> io::Result<()> { Y: csbi.dwCursorPosition.Y, }; let mut written = 0; - FillConsoleOutputCharacterA(hand, b' ' as CHAR, width as u32, pos, &mut written); + FillConsoleOutputCharacterA(hand, b' ', width as u32, pos, &mut written); FillConsoleOutputAttribute(hand, csbi.wAttributes, width as u32, pos, &mut written); SetConsoleCursorPosition(hand, pos); } @@ -262,7 +259,7 @@ pub fn clear_screen(out: &Term) -> io::Result<()> { let cells = csbi.dwSize.X as u32 * csbi.dwSize.Y as u32; // as u32, or else this causes stack overflows. let pos = COORD { X: 0, Y: 0 }; let mut written = 0; - FillConsoleOutputCharacterA(hand, b' ' as CHAR, cells, pos, &mut written); // cells as u32 no longer needed. + FillConsoleOutputCharacterA(hand, b' ', cells, pos, &mut written); // cells as u32 no longer needed. FillConsoleOutputAttribute(hand, csbi.wAttributes, cells, pos, &mut written); SetConsoleCursorPosition(hand, pos); } @@ -283,7 +280,7 @@ pub fn clear_to_end_of_screen(out: &Term) -> io::Result<()> { Y: csbi.dwCursorPosition.Y, }; let mut written = 0; - FillConsoleOutputCharacterA(hand, b' ' as CHAR, cells, pos, &mut written); // cells as u32 no longer needed. + FillConsoleOutputCharacterA(hand, b' ', cells, pos, &mut written); // cells as u32 no longer needed. FillConsoleOutputAttribute(hand, csbi.wAttributes, cells, pos, &mut written); SetConsoleCursorPosition(hand, pos); } @@ -357,7 +354,7 @@ pub fn key_from_key_code(code: VIRTUAL_KEY) -> Key { pub fn read_secure() -> io::Result { let mut rv = String::new(); loop { - match read_single_key()? { + match read_single_key(false)? { Key::Enter => { break; } @@ -376,7 +373,7 @@ pub fn read_secure() -> io::Result { Ok(rv) } -pub fn read_single_key() -> io::Result { +pub fn read_single_key(_ctrlc_key: bool) -> io::Result { let key_event = read_key_event()?; let unicode_char = unsafe { key_event.uChar.UnicodeChar }; @@ -390,6 +387,8 @@ pub fn read_single_key() -> io::Result { // a special keycode for `Enter`, while ReadConsoleInputW() prefers to use '\r'. if c == '\r' { Ok(Key::Enter) + } else if c == '\t' { + Ok(Key::Tab) } else if c == '\x08' { Ok(Key::Backspace) } else if c == '\x1B' { @@ -525,22 +524,37 @@ pub fn msys_tty_on(term: &Term) -> bool { } } - let size = mem::size_of::(); - let mut name_info_bytes = vec![0u8; size + MAX_PATH as usize * mem::size_of::()]; + /// Mirrors windows_sys::Win32::Storage::FileSystem::FILE_NAME_INFO, giving + /// it a fixed length that we can stack allocate + #[repr(C)] + #[allow(non_snake_case)] + struct FILE_NAME_INFO { + FileNameLength: u32, + FileName: [u16; MAX_PATH as usize], + } + + let mut name_info = FILE_NAME_INFO { + FileNameLength: 0, + FileName: [0; MAX_PATH as usize], + }; let res = GetFileInformationByHandleEx( handle as HANDLE, FileNameInfo, - &mut *name_info_bytes as *mut _ as *mut c_void, - name_info_bytes.len() as u32, + &mut name_info as *mut _ as *mut c_void, + std::mem::size_of::() as u32, ); if res == 0 { return false; } - let name_info: &FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const FILE_NAME_INFO); - let s = slice::from_raw_parts( - name_info.FileName.as_ptr(), - name_info.FileNameLength as usize / 2, - ); + + // Use `get` because `FileNameLength` can be out of range. + let s = match name_info + .FileName + .get(..name_info.FileNameLength as usize / 2) + { + Some(s) => s, + None => return false, + }; let name = String::from_utf16_lossy(s); // This checks whether 'pty' exists in the file name, which indicates that // a pseudo-terminal is attached. To mitigate against false positives