Skip to main content

kasl/libs/
stdin_drain.rs

1//! Detect and drain leftover stdin after a multi-line paste.
2//!
3//! Terminals treat newlines in a paste as Enter, so the first prompt only sees
4//! the first line. Remaining lines must be consumed before the next prompt.
5
6use std::io::{self, Write};
7use std::thread;
8use std::time::Duration;
9
10/// Returns true when stdin has data that can be read without waiting for the user.
11pub fn stdin_has_pending_input() -> bool {
12    #[cfg(windows)]
13    {
14        stdin_has_pending_input_windows()
15    }
16    #[cfg(unix)]
17    {
18        stdin_has_pending_input_unix()
19    }
20    #[cfg(not(any(windows, unix)))]
21    {
22        false
23    }
24}
25
26/// Reads all currently available stdin bytes without blocking when empty.
27pub fn read_available_stdin_bytes() -> Vec<u8> {
28    #[cfg(windows)]
29    {
30        read_available_stdin_bytes_windows()
31    }
32    #[cfg(unix)]
33    {
34        read_available_stdin_bytes_unix()
35    }
36    #[cfg(not(any(windows, unix)))]
37    {
38        Vec::new()
39    }
40}
41
42/// Splits available stdin data into non-empty logical lines.
43pub fn drain_available_stdin_lines() -> Vec<String> {
44    let bytes = read_available_stdin_bytes();
45    if bytes.is_empty() {
46        return Vec::new();
47    }
48
49    let text = String::from_utf8_lossy(&bytes);
50    let mut lines: Vec<String> = text.split('\n').map(|line| line.trim_end_matches('\r').to_string()).collect();
51
52    if lines.last().map(|line| line.is_empty()).unwrap_or(false) {
53        lines.pop();
54    }
55
56    lines.into_iter().filter(|line| !line.is_empty()).collect()
57}
58
59/// Reads a task name from stdin, absorbing multi-line paste into one string.
60///
61/// Unlike `dialoguer::Input`, this keeps reading while stdin still has pending
62/// paste data (with short retries), then collapses whitespace/newlines.
63pub fn read_pastable_line(prompt: &str) -> io::Result<String> {
64    print!("{} › ", prompt);
65    io::stdout().flush()?;
66
67    let mut lines = Vec::new();
68    let stdin = io::stdin();
69
70    let mut first = String::new();
71    stdin.read_line(&mut first)?;
72    let first = first.trim_end_matches(['\r', '\n']).to_string();
73    if !first.is_empty() {
74        lines.push(first);
75    }
76
77    // Pull remaining paste lines that are already (or soon) buffered.
78    for _ in 0..8 {
79        thread::sleep(Duration::from_millis(25));
80        if !stdin_has_pending_input() {
81            // One more short wait — mintty/ConPTY sometimes delivers late.
82            thread::sleep(Duration::from_millis(40));
83            if !stdin_has_pending_input() {
84                break;
85            }
86        }
87
88        let mut line = String::new();
89        if stdin.read_line(&mut line)? == 0 {
90            break;
91        }
92        let trimmed = line.trim_end_matches(['\r', '\n']).to_string();
93        if trimmed.is_empty() {
94            break;
95        }
96        lines.push(trimmed);
97    }
98
99    // Byte-level drain as a last resort (pipes where read_line already returned).
100    for extra in drain_available_stdin_lines() {
101        lines.push(extra);
102    }
103
104    Ok(lines.join("\n"))
105}
106
107#[cfg(windows)]
108fn stdin_has_pending_input_windows() -> bool {
109    use winapi::shared::minwindef::{DWORD, FALSE, TRUE};
110    use winapi::um::consoleapi::{GetNumberOfConsoleInputEvents, PeekConsoleInputA};
111    use winapi::um::handleapi::INVALID_HANDLE_VALUE;
112    use winapi::um::namedpipeapi::PeekNamedPipe;
113    use winapi::um::processenv::GetStdHandle;
114    use winapi::um::winbase::STD_INPUT_HANDLE;
115    use winapi::um::wincontypes::{INPUT_RECORD, KEY_EVENT};
116    use winapi::um::winnt::HANDLE;
117
118    unsafe {
119        let handle: HANDLE = GetStdHandle(STD_INPUT_HANDLE);
120        if handle.is_null() || handle == INVALID_HANDLE_VALUE {
121            return false;
122        }
123
124        // Pipe / pty (Git Bash, many terminals): byte backlog is authoritative.
125        let mut available: DWORD = 0;
126        if PeekNamedPipe(handle, std::ptr::null_mut(), 0, std::ptr::null_mut(), &mut available, std::ptr::null_mut()) != FALSE && available > 0 {
127            return true;
128        }
129
130        // Native console: only treat pending KEY_EVENT characters as paste remainder.
131        let mut events: DWORD = 0;
132        if GetNumberOfConsoleInputEvents(handle, &mut events) == FALSE || events == 0 {
133            return false;
134        }
135
136        let mut records = vec![std::mem::zeroed::<INPUT_RECORD>(); events as usize];
137        let mut read: DWORD = 0;
138        if PeekConsoleInputA(handle, records.as_mut_ptr(), events, &mut read) == FALSE {
139            return false;
140        }
141
142        for record in records.iter().take(read as usize) {
143            if record.EventType == KEY_EVENT {
144                let key = record.Event.KeyEvent();
145                if key.bKeyDown == TRUE {
146                    let unicode = *key.uChar.UnicodeChar();
147                    let ascii = *key.uChar.AsciiChar() as u8;
148                    if unicode != 0 || ascii != 0 {
149                        return true;
150                    }
151                }
152            }
153        }
154
155        false
156    }
157}
158
159#[cfg(windows)]
160fn read_available_stdin_bytes_windows() -> Vec<u8> {
161    use std::ptr;
162    use winapi::shared::minwindef::{DWORD, FALSE};
163    use winapi::um::fileapi::ReadFile;
164    use winapi::um::handleapi::INVALID_HANDLE_VALUE;
165    use winapi::um::namedpipeapi::PeekNamedPipe;
166    use winapi::um::processenv::GetStdHandle;
167    use winapi::um::winbase::STD_INPUT_HANDLE;
168
169    unsafe {
170        let handle = GetStdHandle(STD_INPUT_HANDLE);
171        if handle.is_null() || handle == INVALID_HANDLE_VALUE {
172            return Vec::new();
173        }
174
175        let mut available: DWORD = 0;
176        let peeked = PeekNamedPipe(handle, ptr::null_mut(), 0, ptr::null_mut(), &mut available, ptr::null_mut());
177        if peeked == FALSE || available == 0 {
178            return Vec::new();
179        }
180
181        let mut buf = vec![0u8; available as usize];
182        let mut read: DWORD = 0;
183        let ok = ReadFile(handle, buf.as_mut_ptr() as *mut _, available, &mut read, ptr::null_mut());
184        if ok == FALSE {
185            return Vec::new();
186        }
187        buf.truncate(read as usize);
188        buf
189    }
190}
191
192#[cfg(unix)]
193fn stdin_has_pending_input_unix() -> bool {
194    use nix::poll::{PollFd, PollFlags, poll};
195    use std::os::fd::AsFd;
196
197    let stdin = std::io::stdin();
198    let mut fds = [PollFd::new(stdin.as_fd(), PollFlags::POLLIN)];
199    matches!(poll(&mut fds, 0u16), Ok(n) if n > 0)
200}
201
202#[cfg(unix)]
203fn read_available_stdin_bytes_unix() -> Vec<u8> {
204    use nix::poll::{PollFd, PollFlags, poll};
205    use std::io::Read;
206    use std::os::fd::{AsRawFd, BorrowedFd};
207
208    let stdin = std::io::stdin();
209    if !stdin_has_pending_input_unix() {
210        return Vec::new();
211    }
212
213    let mut out = Vec::new();
214    let mut chunk = [0u8; 4096];
215    let mut stdin_lock = stdin.lock();
216    loop {
217        let mut fds = [PollFd::new(unsafe { BorrowedFd::borrow_raw(stdin_lock.as_raw_fd()) }, PollFlags::POLLIN)];
218        match poll(&mut fds, 0u16) {
219            Ok(n) if n > 0 => match stdin_lock.read(&mut chunk) {
220                Ok(0) => break,
221                Ok(n) => out.extend_from_slice(&chunk[..n]),
222                Err(_) => break,
223            },
224            _ => break,
225        }
226        if out.len() > 64 * 1024 {
227            break;
228        }
229    }
230    out
231}