From 119cc9c2a1392e49a5684de934330d981555bccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Sun, 9 Mar 2025 14:53:55 +0800 Subject: [PATCH] feat: support using Yazi in Helix directly without Zellij or tmux (#2461) --- Cargo.lock | 6 +- Cargo.toml | 2 +- cspell.json | 2 +- yazi-adapter/Cargo.toml | 6 - yazi-adapter/src/brand.rs | 3 + yazi-adapter/src/drivers/kgp_old.rs | 9 +- yazi-adapter/src/emulator.rs | 32 ++-- yazi-adapter/src/lib.rs | 2 +- yazi-adapter/src/mux.rs | 3 +- yazi-adapter/src/stdin.rs | 174 ---------------------- yazi-boot/src/actions/debug.rs | 3 +- yazi-config/src/lib.rs | 14 +- yazi-core/src/mgr/commands/bulk_rename.rs | 40 +++-- yazi-core/src/mgr/commands/refresh.rs | 4 +- yazi-core/src/tasks/commands/inspect.rs | 20 ++- yazi-fm/src/app/commands/render.rs | 12 +- yazi-fm/src/term.rs | 36 ++--- yazi-plugin/src/clipboard.rs | 5 +- yazi-shared/Cargo.toml | 2 +- yazi-shared/src/lib.rs | 4 +- yazi-shared/src/os.rs | 4 +- yazi-shared/src/terminal.rs | 12 +- yazi-shared/src/tty/handle.rs | 164 ++++++++++++++++++++ yazi-shared/src/tty/mod.rs | 5 + yazi-shared/src/tty/tty.rs | 74 +++++++++ 25 files changed, 354 insertions(+), 284 deletions(-) delete mode 100644 yazi-adapter/src/stdin.rs create mode 100644 yazi-shared/src/tty/handle.rs create mode 100644 yazi-shared/src/tty/mod.rs create mode 100644 yazi-shared/src/tty/tty.rs diff --git a/Cargo.lock b/Cargo.lock index 817a7391..ea6e69ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2638,9 +2638,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" dependencies = [ "backtrace", "bytes", @@ -3329,12 +3329,10 @@ dependencies = [ "color_quant", "crossterm", "image", - "libc", "ratatui", "scopeguard", "tokio", "tracing", - "windows-sys 0.59.0", "yazi-config", "yazi-macro", "yazi-shared", diff --git a/Cargo.toml b/Cargo.toml index 78ee656a..2ea36e1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ regex = "1.11.1" scopeguard = "1.2.0" serde = { version = "1.0.218", features = [ "derive" ] } serde_json = "1.0.140" -tokio = { version = "1.43.0", features = [ "full" ] } +tokio = { version = "1.44.0", features = [ "full" ] } tokio-stream = "0.1.17" tokio-util = "0.7.13" toml = { version = "0.8.20" } diff --git a/cspell.json b/cspell.json index a3b63c4f..41b40792 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit"],"flagWords":[],"version":"0.2","language":"en"} \ No newline at end of file +{"version":"0.2","language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin"]} \ No newline at end of file diff --git a/yazi-adapter/Cargo.toml b/yazi-adapter/Cargo.toml index 46f5aad6..3188a5ff 100644 --- a/yazi-adapter/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -25,11 +25,5 @@ scopeguard = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -[target."cfg(unix)".dependencies] -libc = { workspace = true } - -[target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59.0", features = [ "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Threading" ] } - [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-adapter/src/brand.rs b/yazi-adapter/src/brand.rs index f9f6bef6..100a11a1 100644 --- a/yazi-adapter/src/brand.rs +++ b/yazi-adapter/src/brand.rs @@ -12,6 +12,7 @@ pub enum Brand { Foot, Ghostty, Microsoft, + Warp, Rio, BlackBox, VSCode, @@ -69,6 +70,7 @@ impl Brand { "iTerm.app" => return Some(B::Iterm2), "WezTerm" => return Some(B::WezTerm), "ghostty" => return Some(B::Ghostty), + "WarpTerminal" => return Some(B::Warp), "rio" => return Some(B::Rio), "BlackBox" => return Some(B::BlackBox), "vscode" => return Some(B::VSCode), @@ -99,6 +101,7 @@ impl Brand { B::Foot => &[A::Sixel], B::Ghostty => &[A::Kgp], B::Microsoft => &[A::Sixel], + B::Warp => &[], B::Rio => &[A::Iip, A::Sixel], B::BlackBox => &[A::Sixel], B::VSCode => &[A::Iip, A::Sixel], diff --git a/yazi-adapter/src/drivers/kgp_old.rs b/yazi-adapter/src/drivers/kgp_old.rs index 1d5259f7..f149bf8a 100644 --- a/yazi-adapter/src/drivers/kgp_old.rs +++ b/yazi-adapter/src/drivers/kgp_old.rs @@ -1,10 +1,11 @@ use core::str; -use std::{io::{LineWriter, Write, stderr}, path::Path}; +use std::{io::Write, path::Path}; use anyhow::Result; use base64::{Engine, engine::general_purpose}; use image::DynamicImage; use ratatui::layout::Rect; +use yazi_shared::tty::TTY; use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter}; @@ -26,9 +27,9 @@ impl KgpOld { #[inline] pub(crate) fn image_erase(_: Rect) -> Result<()> { - let mut stderr = LineWriter::new(stderr()); - write!(stderr, "{START}_Gq=2,a=d,d=A{ESCAPE}\\{CLOSE}")?; - stderr.flush()?; + let mut w = TTY.lockout(); + write!(w, "{START}_Gq=2,a=d,d=A{ESCAPE}\\{CLOSE}")?; + w.flush()?; Ok(()) } diff --git a/yazi-adapter/src/emulator.rs b/yazi-adapter/src/emulator.rs index 6b258387..e8119808 100644 --- a/yazi-adapter/src/emulator.rs +++ b/yazi-adapter/src/emulator.rs @@ -1,13 +1,13 @@ -use std::{io::{LineWriter, stderr}, time::Duration}; +use std::{io::BufWriter, time::Duration}; use anyhow::Result; use crossterm::{cursor::{RestorePosition, SavePosition}, execute, style::Print, terminal::{disable_raw_mode, enable_raw_mode}}; use scopeguard::defer; use tokio::time::sleep; use tracing::{debug, error, warn}; -use yazi_shared::Either; +use yazi_shared::{Either, tty::{Handle, TTY}}; -use crate::{Adapter, AsyncStdin, Brand, Mux, TMUX, Unknown}; +use crate::{Adapter, Brand, Mux, TMUX, Unknown}; #[derive(Clone, Copy, Debug)] pub struct Emulator { @@ -33,7 +33,7 @@ impl Emulator { }; execute!( - LineWriter::new(stderr()), + TTY.writer(), SavePosition, Print(kgp_seq), // Detect KGP Print(Mux::csi("\x1b[>q")), // Request terminal version @@ -75,33 +75,33 @@ impl Emulator { pub fn move_lock((x, y): (u16, u16), cb: F) -> Result where - F: FnOnce(&mut std::io::BufWriter) -> Result, + F: FnOnce(&mut BufWriter) -> Result, { use std::{io::Write, thread, time::Duration}; use crossterm::{cursor::{Hide, MoveTo, RestorePosition, SavePosition, Show}, queue}; - let mut buf = std::io::BufWriter::new(stderr().lock()); + let mut w = TTY.lockout(); // I really don't want to add this, // But tmux and ConPTY sometimes cause the cursor position to get out of sync. if TMUX.get() || cfg!(windows) { - execute!(buf, SavePosition, MoveTo(x, y), Show)?; - execute!(buf, MoveTo(x, y), Show)?; - execute!(buf, MoveTo(x, y), Show)?; + execute!(w, SavePosition, MoveTo(x, y), Show)?; + execute!(w, MoveTo(x, y), Show)?; + execute!(w, MoveTo(x, y), Show)?; thread::sleep(Duration::from_millis(1)); } else { - queue!(buf, SavePosition, MoveTo(x, y))?; + queue!(w, SavePosition, MoveTo(x, y))?; } - let result = cb(&mut buf); + let result = cb(&mut w); if TMUX.get() || cfg!(windows) { - queue!(buf, Hide, RestorePosition)?; + queue!(w, Hide, RestorePosition)?; } else { - queue!(buf, RestorePosition)?; + queue!(w, RestorePosition)?; } - buf.flush()?; + w.flush()?; result } @@ -109,7 +109,7 @@ impl Emulator { let now = std::time::Instant::now(); let h = tokio::spawn(Self::error_to_user()); - let (buf, result) = AsyncStdin::default().read_until(Duration::from_millis(500), |b, buf| { + let (buf, result) = TTY.read_until(Duration::from_millis(500), |b, buf| { b == b'c' && buf.contains(&0x1b) && buf.rsplitn(2, |&b| b == 0x1b).next().is_some_and(|s| s.starts_with(b"[?")) @@ -128,7 +128,7 @@ impl Emulator { pub fn read_until_dsr() -> String { let now = std::time::Instant::now(); - let (buf, result) = AsyncStdin::default().read_until(Duration::from_millis(100), |b, buf| { + let (buf, result) = TTY.read_until(Duration::from_millis(100), |b, buf| { b == b'n' && (buf.ends_with(b"\x1b[0n") || buf.ends_with(b"\x1b[3n")) }); diff --git a/yazi-adapter/src/lib.rs b/yazi-adapter/src/lib.rs index d931bc84..42da957c 100644 --- a/yazi-adapter/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -2,7 +2,7 @@ yazi_macro::mod_pub!(drivers); -yazi_macro::mod_flat!(adapter brand dimension emulator image info mux stdin unknown); +yazi_macro::mod_flat!(adapter brand dimension emulator image info mux unknown); use yazi_shared::{SyncCell, in_wsl}; diff --git a/yazi-adapter/src/mux.rs b/yazi-adapter/src/mux.rs index 2eb2c1ae..a08acc79 100644 --- a/yazi-adapter/src/mux.rs +++ b/yazi-adapter/src/mux.rs @@ -1,6 +1,7 @@ use anyhow::Result; use tracing::error; use yazi_macro::time; +use yazi_shared::tty::TTY; use crate::{CLOSE, ESCAPE, Emulator, START, TMUX}; @@ -47,7 +48,7 @@ impl Mux { pub fn tmux_drain() -> Result<()> { if TMUX.get() { - crossterm::execute!(std::io::stderr(), crossterm::style::Print(Mux::csi("\x1b[5n")))?; + crossterm::execute!(TTY.writer(), crossterm::style::Print(Mux::csi("\x1b[5n")))?; _ = Emulator::read_until_dsr(); } Ok(()) diff --git a/yazi-adapter/src/stdin.rs b/yazi-adapter/src/stdin.rs deleted file mode 100644 index 8cf9ca95..00000000 --- a/yazi-adapter/src/stdin.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::{io::{Error, ErrorKind}, ops::Deref, time::{Duration, Instant}}; - -pub struct AsyncStdin { - fd: Fd, -} - -impl Default for AsyncStdin { - fn default() -> Self { Self { fd: Fd::new().expect("failed to open stdin") } } -} - -impl AsyncStdin { - pub fn read_until

(&mut self, timeout: Duration, predicate: P) -> (Vec, std::io::Result<()>) - where - P: Fn(u8, &[u8]) -> bool, - { - let mut buf: Vec = Vec::with_capacity(200); - let now = Instant::now(); - - let mut read = || { - loop { - if now.elapsed() > timeout { - return Err(Error::new(ErrorKind::TimedOut, "timed out")); - } else if !self.poll(Duration::from_millis(30))? { - continue; - } - - let b = self.read_u8()?; - buf.push(b); - - if predicate(b, &buf) { - break; - } - } - Ok(()) - }; - - let result = read(); - (buf, result) - } -} - -// --- Unix -#[cfg(unix)] -impl AsyncStdin { - pub fn poll(&mut self, timeout: Duration) -> std::io::Result { - let mut tv = libc::timeval { - tv_sec: timeout.as_secs() as libc::time_t, - tv_usec: timeout.subsec_micros() as libc::suseconds_t, - }; - - let result = unsafe { - let mut set: libc::fd_set = std::mem::zeroed(); - libc::FD_ZERO(&mut set); - libc::FD_SET(*self.fd, &mut set); - libc::select(*self.fd + 1, &mut set, std::ptr::null_mut(), std::ptr::null_mut(), &mut tv) - }; - - match result { - -1 => Err(Error::last_os_error()), - 0 => Ok(false), - _ => Ok(true), - } - } - - pub fn read_u8(&mut self) -> std::io::Result { - let mut b = 0; - match unsafe { libc::read(*self.fd, &mut b as *mut _ as *mut _, 1) } { - -1 => Err(Error::last_os_error()), - 0 => Err(Error::new(ErrorKind::UnexpectedEof, "unexpected EOF")), - _ => Ok(b), - } - } -} - -// --- Windows -#[cfg(windows)] -impl AsyncStdin { - pub fn poll(&mut self, timeout: Duration) -> std::io::Result { - use windows_sys::Win32::{Foundation::{WAIT_FAILED, WAIT_OBJECT_0}, System::Threading::WaitForSingleObject}; - - let millis = timeout.as_millis(); - match unsafe { WaitForSingleObject(*self.fd, millis as u32) } { - WAIT_FAILED => Err(Error::last_os_error()), - WAIT_OBJECT_0 => Ok(true), - _ => Ok(false), - } - } - - pub fn read_u8(&mut self) -> std::io::Result { - use windows_sys::Win32::Storage::FileSystem::ReadFile; - - let mut buf = 0; - let mut bytes = 0; - let success = unsafe { ReadFile(*self.fd, &mut buf, 1, &mut bytes, std::ptr::null_mut()) }; - - if success == 0 { - return Err(Error::last_os_error()); - } else if bytes == 0 { - return Err(Error::new(ErrorKind::UnexpectedEof, "unexpected EOF")); - } - Ok(buf) - } -} - -// --- Fd -struct Fd { - #[cfg(unix)] - inner: std::os::fd::RawFd, - #[cfg(windows)] - inner: std::os::windows::io::RawHandle, - close: bool, -} - -impl Deref for Fd { - #[cfg(unix)] - type Target = std::os::fd::RawFd; - #[cfg(windows)] - type Target = std::os::windows::io::RawHandle; - - fn deref(&self) -> &Self::Target { &self.inner } -} - -impl Drop for Fd { - fn drop(&mut self) { - #[cfg(unix)] - if self.close { - unsafe { libc::close(self.inner) }; - } - #[cfg(windows)] - if self.close { - unsafe { windows_sys::Win32::Foundation::CloseHandle(self.inner) }; - } - } -} - -impl Fd { - #[cfg(unix)] - fn new() -> std::io::Result { - use std::{fs::OpenOptions, os::fd::IntoRawFd}; - - Ok(if unsafe { libc::isatty(libc::STDIN_FILENO) } == 1 { - Self { inner: libc::STDIN_FILENO, close: false } - } else { - Self { - inner: OpenOptions::new().read(true).write(true).open("/dev/tty")?.into_raw_fd(), - close: true, - } - }) - } - - #[cfg(windows)] - fn new() -> std::io::Result { - use windows_sys::Win32::{Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, Storage::FileSystem::{CreateFileW, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING}}; - - let name: Vec = "CONIN$\0".encode_utf16().collect(); - let result = unsafe { - CreateFileW( - name.as_ptr(), - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - std::ptr::null_mut(), - OPEN_EXISTING, - 0, - std::ptr::null_mut(), - ) - }; - - if result == INVALID_HANDLE_VALUE { - Err(std::io::Error::last_os_error()) - } else { - Ok(Self { inner: result, close: true }) - } - } -} diff --git a/yazi-boot/src/actions/debug.rs b/yazi-boot/src/actions/debug.rs index df451734..1659964a 100644 --- a/yazi-boot/src/actions/debug.rs +++ b/yazi-boot/src/actions/debug.rs @@ -25,7 +25,8 @@ impl Actions { writeln!(s, " Emulator.detect : {:?}", yazi_adapter::EMULATOR)?; writeln!(s, "\nAdapter")?; - writeln!(s, " Adapter.matches: {:?}", yazi_adapter::ADAPTOR)?; + writeln!(s, " Adapter.matches : {:?}", yazi_adapter::ADAPTOR)?; + writeln!(s, " Dimension.available: {:?}", yazi_adapter::Dimension::available())?; writeln!(s, "\nDesktop")?; writeln!(s, " XDG_SESSION_TYPE : {:?}", env::var_os("XDG_SESSION_TYPE"))?; diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index ff25f37e..7cb515aa 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -4,9 +4,9 @@ yazi_macro::mod_pub!(keymap mgr open plugin popup preview tasks theme which); yazi_macro::mod_flat!(layout pattern preset priority); -use std::str::FromStr; +use std::{io::{Read, Write}, str::FromStr}; -use yazi_shared::{RoCell, SyncCell}; +use yazi_shared::{RoCell, SyncCell, tty::TTY}; pub static KEYMAP: RoCell = RoCell::new(); pub static MGR: RoCell = RoCell::new(); @@ -93,14 +93,16 @@ fn try_init(merge: bool) -> anyhow::Result<()> { } fn wait_for_key(e: anyhow::Error) -> anyhow::Result<()> { - eprintln!("{e}"); + let stdout = &mut *TTY.lockout(); + + writeln!(stdout, "{e}")?; if let Some(src) = e.source() { - eprintln!("\nCaused by:\n{src}"); + writeln!(stdout, "\nCaused by:\n{src}")?; } use crossterm::style::{Attribute, Print, SetAttributes}; crossterm::execute!( - std::io::stderr(), + stdout, SetAttributes(Attribute::Reverse.into()), SetAttributes(Attribute::Bold.into()), Print("Press to continue with preset settings..."), @@ -108,6 +110,6 @@ fn wait_for_key(e: anyhow::Error) -> anyhow::Result<()> { Print("\n"), )?; - std::io::stdin().read_line(&mut String::new())?; + TTY.reader().read_exact(&mut [0])?; Ok(()) } diff --git a/yazi-core/src/mgr/commands/bulk_rename.rs b/yazi-core/src/mgr/commands/bulk_rename.rs index fe53ba50..1376a361 100644 --- a/yazi-core/src/mgr/commands/bulk_rename.rs +++ b/yazi-core/src/mgr/commands/bulk_rename.rs @@ -1,13 +1,13 @@ -use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, io::{BufWriter, Write, stderr}, path::PathBuf}; +use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, io::{Read, Write}, path::PathBuf}; use anyhow::{Result, anyhow}; use scopeguard::defer; -use tokio::{fs::{self, OpenOptions}, io::{AsyncReadExt, AsyncWriteExt, stdin}}; +use tokio::{fs::{self, OpenOptions}, io::AsyncWriteExt}; use yazi_config::{OPEN, PREVIEW}; use yazi_dds::Pubsub; use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file}; use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER}; -use yazi_shared::{terminal_clear, url::Url}; +use yazi_shared::{terminal_clear, tty::TTY, url::Url}; use crate::mgr::Mgr; @@ -51,10 +51,10 @@ impl Mgr { } async fn bulk_rename_do(root: PathBuf, old: Vec, new: Vec) -> Result<()> { - terminal_clear(&mut stderr())?; + terminal_clear(TTY.writer())?; if old.len() != new.len() { - eprintln!("Number of old and new differ, press ENTER to exit"); - stdin().read_exact(&mut [0]).await?; + TTY.writer().write_all(b"Number of old and new differ, press ENTER to exit")?; + TTY.reader().read_exact(&mut [0])?; return Ok(()); } @@ -65,16 +65,16 @@ impl Mgr { } { - let mut stderr = BufWriter::new(stderr().lock()); + let mut w = TTY.lockout(); for (o, n) in &todo { - writeln!(stderr, "{} -> {}", o.display(), n.display())?; + writeln!(w, "{} -> {}", o.display(), n.display())?; } - write!(stderr, "Continue to rename? (y/N): ")?; - stderr.flush()?; + write!(w, "Continue to rename? (y/N): ")?; + w.flush()?; } let mut buf = [0; 10]; - _ = stdin().read(&mut buf).await?; + _ = TTY.reader().read(&mut buf)?; if buf[0] != b'y' && buf[0] != b'Y' { return Ok(()); } @@ -108,19 +108,17 @@ impl Mgr { } async fn output_failed(failed: Vec<(PathBuf, PathBuf, anyhow::Error)>) -> Result<()> { - terminal_clear(&mut stderr())?; + let mut stdout = TTY.lockout(); + terminal_clear(&mut *stdout)?; - { - let mut stderr = BufWriter::new(stderr().lock()); - writeln!(stderr, "Failed to rename:")?; - for (o, n, e) in failed { - writeln!(stderr, "{} -> {}: {e}", o.display(), n.display())?; - } - writeln!(stderr, "\nPress ENTER to exit")?; - stderr.flush()?; + writeln!(stdout, "Failed to rename:")?; + for (old, new, err) in failed { + writeln!(stdout, "{} -> {}: {err}", old.display(), new.display())?; } + writeln!(stdout, "\nPress ENTER to exit")?; - stdin().read_exact(&mut [0]).await?; + stdout.flush()?; + TTY.reader().read_exact(&mut [0])?; Ok(()) } diff --git a/yazi-core/src/mgr/commands/refresh.rs b/yazi-core/src/mgr/commands/refresh.rs index 45ceeae0..b2d721ca 100644 --- a/yazi-core/src/mgr/commands/refresh.rs +++ b/yazi-core/src/mgr/commands/refresh.rs @@ -3,14 +3,14 @@ use std::path::MAIN_SEPARATOR; use crossterm::{execute, terminal::SetTitle}; use yazi_config::MGR; use yazi_fs::CWD; -use yazi_shared::event::CmdCow; +use yazi_shared::{event::CmdCow, tty::TTY}; use crate::{mgr::Mgr, tasks::Tasks}; impl Mgr { pub fn refresh(&mut self, _: CmdCow, tasks: &Tasks) { if CWD.set(self.cwd()) && !MGR.title_format.is_empty() { - execute!(std::io::stderr(), SetTitle(self.title())).ok(); + execute!(TTY.writer(), SetTitle(self.title())).ok(); } self.active_mut().apply_files_attrs(); diff --git a/yazi-core/src/tasks/commands/inspect.rs b/yazi-core/src/tasks/commands/inspect.rs index 147c4381..f3003e13 100644 --- a/yazi-core/src/tasks/commands/inspect.rs +++ b/yazi-core/src/tasks/commands/inspect.rs @@ -1,10 +1,10 @@ -use std::{io::{BufWriter, LineWriter, Write, stderr}, mem}; +use std::io::Write; -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode}}; use scopeguard::defer; use tokio::{io::{AsyncReadExt, stdin}, select, sync::mpsc, time}; use yazi_proxy::{AppProxy, HIDER}; -use yazi_shared::{event::CmdCow, terminal_clear}; +use yazi_shared::{event::CmdCow, terminal_clear, tty::TTY}; use crate::tasks::Tasks; @@ -19,7 +19,7 @@ impl Tasks { let _permit = HIDER.acquire().await.unwrap(); let (tx, mut rx) = mpsc::unbounded_channel(); - let mut buffered = { + let buffered = { let mut ongoing = ongoing.lock(); let Some(task) = ongoing.get_mut(id) else { return }; @@ -30,24 +30,22 @@ impl Tasks { defer!(AppProxy::resume()); AppProxy::stop().await; - terminal_clear(&mut stderr()).ok(); - BufWriter::new(stderr().lock()).write_all(mem::take(&mut buffered).as_bytes()).ok(); + terminal_clear(TTY.writer()).ok(); + TTY.writer().write_all(buffered.as_bytes()).ok(); defer! { disable_raw_mode().ok(); } enable_raw_mode().ok(); - let mut stdin = stdin(); + let mut stdin = stdin(); // TODO: stdin let mut answer = 0; loop { select! { Some(line) = rx.recv() => { - let mut stderr = LineWriter::new(stderr().lock()); - stderr.write_all(line.as_bytes()).ok(); - stderr.write_all(b"\r\n").ok(); + execute!(TTY.writer(), crossterm::style::Print(line), crossterm::style::Print("\r\n")).ok(); } _ = time::sleep(time::Duration::from_millis(500)) => { if ongoing.lock().get(id).is_none() { - stderr().write_all(b"Task finished, press `q` to quit\r\n").ok(); + execute!(TTY.writer(), crossterm::style::Print("Task finished, press `q` to quit\r\n")).ok(); break; } }, diff --git a/yazi-fm/src/app/commands/render.rs b/yazi-fm/src/app/commands/render.rs index 1361a4ae..97a80c69 100644 --- a/yazi-fm/src/app/commands/render.rs +++ b/yazi-fm/src/app/commands/render.rs @@ -1,10 +1,10 @@ -use std::{io::{BufWriter, stderr}, sync::atomic::Ordering}; +use std::sync::atomic::Ordering; use crossterm::{execute, queue, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; use ratatui::{CompletedFrame, backend::{Backend, CrosstermBackend}, buffer::Buffer}; use scopeguard::defer; use yazi_plugin::elements::COLLISION; -use yazi_shared::event::NEED_RENDER; +use yazi_shared::{event::NEED_RENDER, tty::TTY}; use crate::{app::App, lives::Lives, root::Root}; @@ -13,8 +13,8 @@ impl App { NEED_RENDER.store(false, Ordering::Relaxed); let Some(term) = &mut self.term else { return }; - queue!(stderr(), BeginSynchronizedUpdate).ok(); - defer! { execute!(stderr(), EndSynchronizedUpdate).ok(); } + queue!(TTY.writer(), BeginSynchronizedUpdate).ok(); + defer! { execute!(TTY.writer(), EndSynchronizedUpdate).ok(); } let collision = COLLISION.swap(false, Ordering::Relaxed); let frame = term @@ -79,7 +79,9 @@ impl App { } let patches = frame.buffer.diff(&new); - let mut backend = CrosstermBackend::new(BufWriter::new(stderr().lock())); + let stdout = &mut *TTY.lockout(); + + let mut backend = CrosstermBackend::new(stdout); backend.draw(patches.into_iter()).ok(); if let Some(pos) = cursor { backend.show_cursor().ok(); diff --git a/yazi-fm/src/term.rs b/yazi-fm/src/term.rs index c1539bdc..c73a3441 100644 --- a/yazi-fm/src/term.rs +++ b/yazi-fm/src/term.rs @@ -1,4 +1,4 @@ -use std::{io::{self, BufWriter, Stderr, stderr}, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, AtomicU8, Ordering}}; +use std::{io, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, AtomicU8, Ordering}}; use anyhow::Result; use crossterm::{event::{DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode}}; @@ -6,14 +6,14 @@ use cursor::RestoreCursor; use ratatui::{CompletedFrame, Frame, Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect}; use yazi_adapter::{Emulator, Mux}; use yazi_config::{INPUT, MGR}; -use yazi_shared::SyncCell; +use yazi_shared::{SyncCell, tty::{TTY, TtyWriter}}; static CSI_U: AtomicBool = AtomicBool::new(false); static BLINK: AtomicBool = AtomicBool::new(false); static SHAPE: AtomicU8 = AtomicU8::new(0); pub(super) struct Term { - inner: Terminal>>, + inner: Terminal>>, last_area: Rect, last_buffer: Buffer, } @@ -22,7 +22,7 @@ impl Term { pub(super) fn start() -> Result { static SKIP: SyncCell = SyncCell::new(false); let mut term = Self { - inner: Terminal::new(CrosstermBackend::new(BufWriter::new(stderr())))?, + inner: Terminal::new(CrosstermBackend::new(TTY.writer()))?, last_area: Default::default(), last_buffer: Default::default(), }; @@ -33,7 +33,7 @@ impl Term { } execute!( - BufWriter::new(stderr()), + TTY.writer(), screen::SetScreen(true), Print(Mux::csi("\x1bP$q q\x1b\\")), // Request cursor shape (DECRQSS query for DECSCUSR) Print(Mux::csi("\x1b[?12$p")), // Request cursor blink status (DECSET) @@ -60,7 +60,7 @@ impl Term { if CSI_U.load(Ordering::Relaxed) { queue!( - stderr(), + TTY.writer(), PushKeyboardEnhancementFlags( KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS @@ -76,11 +76,11 @@ impl Term { fn stop(&mut self) -> Result<()> { if CSI_U.swap(false, Ordering::Relaxed) { - execute!(stderr(), PopKeyboardEnhancementFlags)?; + execute!(TTY.writer(), PopKeyboardEnhancementFlags)?; } execute!( - stderr(), + TTY.writer(), mouse::SetMouse(false), RestoreCursor, DisableBracketedPaste, @@ -93,15 +93,15 @@ impl Term { pub(super) fn goodbye(f: impl FnOnce() -> bool) -> ! { if CSI_U.swap(false, Ordering::Relaxed) { - execute!(stderr(), PopKeyboardEnhancementFlags).ok(); + execute!(TTY.writer(), PopKeyboardEnhancementFlags).ok(); } if !MGR.title_format.is_empty() { - execute!(stderr(), SetTitle("")).ok(); + execute!(TTY.writer(), SetTitle("")).ok(); } execute!( - stderr(), + TTY.writer(), mouse::SetMouse(false), RestoreCursor, SetTitle(""), @@ -148,9 +148,9 @@ impl Term { pub(super) fn set_cursor_block() -> Result<()> { use crossterm::cursor::SetCursorStyle; Ok(if INPUT.cursor_blink { - queue!(stderr(), SetCursorStyle::BlinkingBlock)? + queue!(TTY.writer(), SetCursorStyle::BlinkingBlock)? } else { - queue!(stderr(), SetCursorStyle::SteadyBlock)? + queue!(TTY.writer(), SetCursorStyle::SteadyBlock)? }) } @@ -158,9 +158,9 @@ impl Term { pub(super) fn set_cursor_bar() -> Result<()> { use crossterm::cursor::SetCursorStyle; Ok(if INPUT.cursor_blink { - queue!(stderr(), SetCursorStyle::BlinkingBar)? + queue!(TTY.writer(), SetCursorStyle::BlinkingBar)? } else { - queue!(stderr(), SetCursorStyle::SteadyBar)? + queue!(TTY.writer(), SetCursorStyle::SteadyBar)? }) } @@ -168,9 +168,9 @@ impl Term { pub(super) fn set_cursor_underscore() -> Result<()> { use crossterm::cursor::SetCursorStyle; Ok(if INPUT.cursor_blink { - queue!(stderr(), SetCursorStyle::BlinkingUnderScore)? + queue!(TTY.writer(), SetCursorStyle::BlinkingUnderScore)? } else { - queue!(stderr(), SetCursorStyle::SteadyUnderScore)? + queue!(TTY.writer(), SetCursorStyle::SteadyUnderScore)? }) } } @@ -180,7 +180,7 @@ impl Drop for Term { } impl Deref for Term { - type Target = Terminal>>; + type Target = Terminal>>; fn deref(&self) -> &Self::Target { &self.inner } } diff --git a/yazi-plugin/src/clipboard.rs b/yazi-plugin/src/clipboard.rs index 1a51deee..a5d3642c 100644 --- a/yazi-plugin/src/clipboard.rs +++ b/yazi-plugin/src/clipboard.rs @@ -55,13 +55,14 @@ impl Clipboard { #[cfg(unix)] pub async fn set(&self, s: impl AsRef) { - use std::{io::{BufWriter, stderr}, process::Stdio}; + use std::process::Stdio; use crossterm::execute; use tokio::{io::AsyncWriteExt, process::Command}; + use yazi_shared::tty::TTY; s.as_ref().clone_into(&mut self.content.lock()); - execute!(BufWriter::new(stderr()), osc52::SetClipboard::new(s.as_ref())).ok(); + execute!(TTY.writer(), osc52::SetClipboard::new(s.as_ref())).ok(); let all = [ ("pbcopy", &[][..]), diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 67f23ef5..46550e43 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -28,7 +28,7 @@ libc = { workspace = true } uzers = { workspace = true } [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59.0", features = [ "Win32_UI_Shell" ] } +windows-sys = { version = "0.59.0", features = [ "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Threading", "Win32_UI_Shell" ] } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 7588f037..ca4260ab 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -1,10 +1,12 @@ #![allow(clippy::option_map_unit_fn)] -yazi_macro::mod_pub!(errors event shell theme translit url); +yazi_macro::mod_pub!(errors event shell theme translit tty url); yazi_macro::mod_flat!(chars condition debounce either env id layer natsort number os rand ro_cell sync_cell terminal throttle time); pub fn init() { + tty::init(); + LOG_LEVEL.replace(<_>::from(std::env::var("YAZI_LOG").unwrap_or_default())); #[cfg(unix)] diff --git a/yazi-shared/src/os.rs b/yazi-shared/src/os.rs index b6c76c4b..fa9f213a 100644 --- a/yazi-shared/src/os.rs +++ b/yazi-shared/src/os.rs @@ -10,8 +10,6 @@ pub fn hostname() -> Option<&'static str> { #[cfg(unix)] fn hostname_impl() -> Result { - use std::io::{Error, ErrorKind}; - use libc::{gethostname, strlen}; let mut s = [0; 256]; @@ -24,6 +22,6 @@ fn hostname_impl() -> Result { }; std::str::from_utf8(&s[..len]) - .map_err(|_| Error::new(ErrorKind::Other, "invalid hostname")) + .map_err(|_| std::io::Error::other("invalid hostname")) .map(|s| s.to_owned()) } diff --git a/yazi-shared/src/terminal.rs b/yazi-shared/src/terminal.rs index ed66d0cc..c874a7fc 100644 --- a/yazi-shared/src/terminal.rs +++ b/yazi-shared/src/terminal.rs @@ -1,10 +1,12 @@ use std::io::Write; -use crossterm::queue; +use crossterm::execute; #[inline] -pub fn terminal_clear(w: &mut impl Write) -> std::io::Result<()> { - queue!(w, crossterm::terminal::Clear(crossterm::terminal::ClearType::All))?; - writeln!(w)?; - w.flush() +pub fn terminal_clear(mut w: impl Write) -> std::io::Result<()> { + execute!( + w, + crossterm::terminal::Clear(crossterm::terminal::ClearType::All), + crossterm::style::Print("\n") + ) } diff --git a/yazi-shared/src/tty/handle.rs b/yazi-shared/src/tty/handle.rs new file mode 100644 index 00000000..23544fd1 --- /dev/null +++ b/yazi-shared/src/tty/handle.rs @@ -0,0 +1,164 @@ +use std::{io::{Error, ErrorKind, Read, Write}, time::Duration}; + +pub struct Handle { + #[cfg(unix)] + inner: std::os::fd::RawFd, + #[cfg(windows)] + inner: std::os::windows::io::RawHandle, + close: bool, +} + +impl Drop for Handle { + fn drop(&mut self) { + #[cfg(unix)] + if self.close { + unsafe { libc::close(self.inner) }; + } + #[cfg(windows)] + if self.close { + unsafe { windows_sys::Win32::Foundation::CloseHandle(self.inner) }; + } + } +} + +impl Read for Handle { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + #[cfg(unix)] + { + use std::os::{fd::IntoRawFd, unix::io::FromRawFd}; + let mut f = unsafe { std::fs::File::from_raw_fd(self.inner) }; + let result = f.read(buf); + self.inner = f.into_raw_fd(); + result + } + #[cfg(windows)] + { + use std::os::windows::io::{FromRawHandle, IntoRawHandle}; + let mut f = unsafe { std::fs::File::from_raw_handle(self.inner) }; + let result = f.read(buf); + self.inner = f.into_raw_handle(); + result + } + } +} + +impl Write for Handle { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + #[cfg(unix)] + { + use std::os::{fd::IntoRawFd, unix::io::FromRawFd}; + let mut f = unsafe { std::fs::File::from_raw_fd(self.inner) }; + let result = f.write(buf); + self.inner = f.into_raw_fd(); + result + } + #[cfg(windows)] + { + use std::os::windows::io::{FromRawHandle, IntoRawHandle}; + let mut f = unsafe { std::fs::File::from_raw_handle(self.inner) }; + let result = f.write(buf); + self.inner = f.into_raw_handle(); + result + } + } + + fn flush(&mut self) -> std::io::Result<()> { Ok(()) } +} + +#[cfg(unix)] +impl Handle { + pub(super) fn new(out: bool) -> std::io::Result { + use std::{fs::OpenOptions, os::fd::IntoRawFd}; + + let fileno = if out { libc::STDOUT_FILENO } else { libc::STDIN_FILENO }; + Ok(if unsafe { libc::isatty(fileno) } == 1 { + Self { inner: fileno, close: false } + } else { + Self { + inner: OpenOptions::new().read(!out).write(out).open("/dev/tty")?.into_raw_fd(), + close: true, + } + }) + } + + pub(super) fn poll(&mut self, timeout: Duration) -> std::io::Result { + let mut tv = libc::timeval { + tv_sec: timeout.as_secs() as libc::time_t, + tv_usec: timeout.subsec_micros() as libc::suseconds_t, + }; + + let result = unsafe { + let mut set: libc::fd_set = std::mem::zeroed(); + libc::FD_ZERO(&mut set); + libc::FD_SET(self.inner, &mut set); + libc::select(self.inner + 1, &mut set, std::ptr::null_mut(), std::ptr::null_mut(), &mut tv) + }; + + match result { + -1 => Err(Error::last_os_error()), + 0 => Ok(false), + _ => Ok(true), + } + } + + pub(super) fn read_u8(&mut self) -> std::io::Result { + let mut b = 0; + match unsafe { libc::read(self.inner, &mut b as *mut _ as *mut _, 1) } { + -1 => Err(Error::last_os_error()), + 0 => Err(Error::new(ErrorKind::UnexpectedEof, "unexpected EOF")), + _ => Ok(b), + } + } +} + +#[cfg(windows)] +impl Handle { + pub(super) fn new(out: bool) -> std::io::Result { + use windows_sys::Win32::{Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, Storage::FileSystem::{CreateFileW, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING}}; + + let name: Vec = if out { "CONOUT$\0" } else { "CONIN$\0" }.encode_utf16().collect(); + let result = unsafe { + CreateFileW( + name.as_ptr(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + std::ptr::null_mut(), + OPEN_EXISTING, + 0, + std::ptr::null_mut(), + ) + }; + + if result == INVALID_HANDLE_VALUE { + Err(std::io::Error::last_os_error()) + } else { + Ok(Self { inner: result, close: true }) + } + } + + pub(super) fn poll(&mut self, timeout: Duration) -> std::io::Result { + use windows_sys::Win32::{Foundation::{WAIT_FAILED, WAIT_OBJECT_0}, System::Threading::WaitForSingleObject}; + + let millis = timeout.as_millis(); + match unsafe { WaitForSingleObject(self.inner, millis as u32) } { + WAIT_FAILED => Err(Error::last_os_error()), + WAIT_OBJECT_0 => Ok(true), + _ => Ok(false), + } + } + + pub(super) fn read_u8(&mut self) -> std::io::Result { + use windows_sys::Win32::Storage::FileSystem::ReadFile; + + let mut buf = 0; + let mut bytes = 0; + let success = unsafe { ReadFile(self.inner, &mut buf, 1, &mut bytes, std::ptr::null_mut()) }; + + if success == 0 { + return Err(Error::last_os_error()); + } else if bytes == 0 { + return Err(Error::new(ErrorKind::UnexpectedEof, "unexpected EOF")); + } + Ok(buf) + } +} diff --git a/yazi-shared/src/tty/mod.rs b/yazi-shared/src/tty/mod.rs new file mode 100644 index 00000000..9a54aebc --- /dev/null +++ b/yazi-shared/src/tty/mod.rs @@ -0,0 +1,5 @@ +yazi_macro::mod_flat!(tty handle); + +pub static TTY: crate::RoCell = crate::RoCell::new(); + +pub(super) fn init() { TTY.with(<_>::default); } diff --git a/yazi-shared/src/tty/tty.rs b/yazi-shared/src/tty/tty.rs new file mode 100644 index 00000000..74690348 --- /dev/null +++ b/yazi-shared/src/tty/tty.rs @@ -0,0 +1,74 @@ +use std::{io::{BufWriter, Error, ErrorKind, Read, Write}, time::{Duration, Instant}}; + +use parking_lot::{Mutex, MutexGuard}; + +use super::Handle; + +pub struct Tty { + stdin: Mutex, + stdout: Mutex>, +} + +impl Default for Tty { + fn default() -> Self { + let stdin = Handle::new(false).expect("failed to open stdin"); + let stdout = Handle::new(true).expect("failed to open stdout"); + Self { stdin: Mutex::new(stdin), stdout: Mutex::new(BufWriter::new(stdout)) } + } +} + +impl Tty { + pub const fn reader(&self) -> TtyReader { TtyReader(&self.stdin) } + + pub const fn writer(&self) -> TtyWriter { TtyWriter(&self.stdout) } + + pub fn lockin(&self) -> MutexGuard { self.stdin.lock() } + + pub fn lockout(&self) -> MutexGuard> { self.stdout.lock() } + + pub fn read_until

(&self, timeout: Duration, predicate: P) -> (Vec, std::io::Result<()>) + where + P: Fn(u8, &[u8]) -> bool, + { + let mut buf: Vec = Vec::with_capacity(200); + let now = Instant::now(); + + let mut read = || { + let mut stdin = self.stdin.lock(); + loop { + if now.elapsed() > timeout { + return Err(Error::new(ErrorKind::TimedOut, "timed out")); + } else if !stdin.poll(Duration::from_millis(30))? { + continue; + } + + let b = stdin.read_u8()?; + buf.push(b); + + if predicate(b, &buf) { + break; + } + } + Ok(()) + }; + + let result = read(); + (buf, result) + } +} + +// --- Reader +pub struct TtyReader<'a>(&'a Mutex); + +impl Read for TtyReader<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.0.lock().read(buf) } +} + +// --- Writer +pub struct TtyWriter<'a>(&'a Mutex>); + +impl Write for TtyWriter<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { self.0.lock().write(buf) } + + fn flush(&mut self) -> std::io::Result<()> { self.0.lock().flush() } +}