use crate::error::{Error, Result};
use crate::input::Key;
use std::io::{self, Read, Write};
use std::sync::{Mutex, OnceLock};
static BACKEND: OnceLock<Mutex<Backend>> = OnceLock::new();
static UPDATE_BUFFER: OnceLock<Mutex<String>> = OnceLock::new();
#[cfg(target_os = "linux")]
const INPUT_BUF_CAPACITY: usize = 8192;
#[cfg(not(target_os = "linux"))]
const INPUT_BUF_CAPACITY: usize = 1024;
static INPUT_BUF: OnceLock<Mutex<InputBuf>> = OnceLock::new();
struct InputBuf {
bytes: Vec<u8>,
last_read_filled: bool,
}
fn input_buf() -> &'static Mutex<InputBuf> {
INPUT_BUF.get_or_init(|| {
Mutex::new(InputBuf {
bytes: Vec::with_capacity(INPUT_BUF_CAPACITY),
last_read_filled: false,
})
})
}
impl InputBuf {
#[cfg(unix)]
fn fill_nonblocking(&mut self) -> io::Result<()> {
use std::io::ErrorKind;
let stdin = io::stdin();
let fd = stdin.as_raw_fd();
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
let result = self.fill_from(stdin.lock());
unsafe { libc::fcntl(fd, libc::F_SETFL, flags) };
match result {
Ok(()) => Ok(()),
Err(e) if e.kind() == ErrorKind::WouldBlock => Ok(()),
Err(e) => Err(e),
}
}
#[cfg(not(unix))]
fn fill_nonblocking(&mut self) -> io::Result<()> {
Ok(())
}
fn fill_blocking(&mut self) -> io::Result<()> {
let stdin = io::stdin();
self.fill_from(stdin.lock())
}
fn fill_from(&mut self, mut reader: impl Read) -> io::Result<()> {
let target = self.bytes.len() + INPUT_BUF_CAPACITY;
if self.bytes.capacity() < target {
self.bytes.reserve(target - self.bytes.capacity());
}
let start = self.bytes.len();
let n = {
let cap = self.bytes.capacity();
let spare = unsafe {
std::slice::from_raw_parts_mut(
self.bytes.as_mut_ptr().add(start),
cap - start,
)
};
reader.read(spare)?
};
unsafe { self.bytes.set_len(start + n) };
self.last_read_filled = n > 0 && (n == INPUT_BUF_CAPACITY);
Ok(())
}
fn pop_byte(&mut self) -> Option<u8> {
if self.bytes.is_empty() {
None
} else {
Some(self.bytes.remove(0))
}
}
fn drain_until(&mut self, end: &[u8]) -> io::Result<Vec<u8>> {
let mut scanned = 0usize;
loop {
let scan_from = scanned.saturating_sub(end.len() - 1);
if let Some(rel) = find_subseq(&self.bytes[scan_from..], end) {
let abs = scan_from + rel;
let content = self.bytes[..abs].to_vec();
self.bytes.drain(..abs + end.len());
return Ok(content);
}
scanned = self.bytes.len();
let before = self.bytes.len();
self.fill_blocking()?;
if self.bytes.len() == before {
let content = std::mem::take(&mut self.bytes);
return Ok(content);
}
}
}
}
fn find_subseq(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
pub(crate) struct Backend {
original_termios: Option<Termios>,
initialized: bool,
}
#[cfg(unix)]
use std::os::unix::io::AsRawFd;
#[cfg(unix)]
#[derive(Clone)]
struct Termios {
termios: libc::termios,
}
#[cfg(not(unix))]
#[derive(Clone)]
struct Termios;
impl Backend {
fn new() -> Self {
Self {
original_termios: None,
initialized: false,
}
}
pub(crate) fn init() -> Result<()> {
let backend = BACKEND.get_or_init(|| Mutex::new(Backend::new()));
let mut guard = backend.lock().unwrap();
if guard.initialized {
return Err(Error::AlreadyInitialized);
}
guard.enable_raw_mode()?;
guard.initialized = true;
print!("\x1b[?1049h");
print!("\x1b[?25l");
print!("\x1b[2J");
io::stdout().flush()?;
Ok(())
}
pub(crate) fn cleanup() -> Result<()> {
let backend = BACKEND.get().ok_or(Error::NotInitialized)?;
let mut guard = backend.lock().unwrap();
if !guard.initialized {
return Ok(());
}
print!("\x1b[?25h");
print!("\x1b[?1049l");
io::stdout().flush()?;
guard.disable_raw_mode()?;
guard.initialized = false;
Ok(())
}
#[cfg(unix)]
fn enable_raw_mode(&mut self) -> Result<()> {
let fd = io::stdin().as_raw_fd();
if unsafe { libc::isatty(fd) } == 0 {
return Ok(());
}
let mut termios = unsafe {
let mut termios: libc::termios = std::mem::zeroed();
if libc::tcgetattr(fd, &mut termios) != 0 {
return Err(Error::Io(io::Error::last_os_error()));
}
termios
};
self.original_termios = Some(Termios { termios });
unsafe {
libc::cfmakeraw(&mut termios);
if libc::tcsetattr(fd, libc::TCSANOW, &termios) != 0 {
return Err(Error::Io(io::Error::last_os_error()));
}
}
Ok(())
}
#[cfg(unix)]
fn disable_raw_mode(&mut self) -> Result<()> {
if let Some(original) = &self.original_termios {
let fd = io::stdin().as_raw_fd();
unsafe {
if libc::tcsetattr(fd, libc::TCSANOW, &original.termios) != 0 {
return Err(Error::Io(io::Error::last_os_error()));
}
}
}
Ok(())
}
#[cfg(not(unix))]
fn enable_raw_mode(&mut self) -> Result<()> {
Err(Error::NotSupported)
}
#[cfg(not(unix))]
fn disable_raw_mode(&mut self) -> Result<()> {
Ok(())
}
pub(crate) fn read_key_timeout(timeout_ms: Option<u64>) -> Result<Option<Key>> {
let mut guard = input_buf().lock().unwrap();
let buf = &mut *guard;
if buf.bytes.is_empty() {
match timeout_ms {
Some(0) => {
buf.fill_nonblocking()?;
if buf.bytes.is_empty() {
return Ok(None);
}
}
Some(ms) => {
if !wait_for_stdin(Some(ms))? {
return Ok(None);
}
buf.fill_blocking()?;
}
None => {
buf.fill_blocking()?;
}
}
}
let Some(byte) = buf.pop_byte() else {
return Ok(None);
};
Self::parse_key_from_byte(byte, buf).map(Some)
}
pub(crate) fn read_key() -> Result<Key> {
let mut guard = input_buf().lock().unwrap();
let buf = &mut *guard;
if buf.bytes.is_empty() {
buf.fill_blocking()?;
}
let Some(byte) = buf.pop_byte() else {
return Ok(Key::Unknown);
};
Self::parse_key_from_byte(byte, buf)
}
fn parse_key_from_byte(byte: u8, ibuf: &mut InputBuf) -> Result<Key> {
match byte {
b'\r' | b'\n' => Ok(Key::Enter),
b'\t' => Ok(Key::Tab),
127 => Ok(Key::Backspace),
27 => {
let mut seq = vec![27];
while !is_sequence_complete(&seq) {
if let Some(b) = ibuf.pop_byte() {
seq.push(b);
continue;
}
ibuf.fill_nonblocking()?;
if ibuf.bytes.is_empty() {
break;
}
}
if seq == b"\x1b[200~" {
let content = ibuf.drain_until(b"\x1b[201~")?;
return Ok(Key::Paste(
String::from_utf8_lossy(&content).into_owned(),
));
}
Ok(Key::from_escape_sequence(&seq).unwrap_or(Key::Escape))
}
1..=26 => Ok(Key::Ctrl((byte - 1 + b'a') as char)),
32..=126 => Ok(Key::Char(byte as char)),
_ => Ok(Key::Unknown),
}
}
pub(crate) fn get_terminal_size() -> Result<(u16, u16)> {
#[cfg(unix)]
{
let fd = io::stdout().as_raw_fd();
if unsafe { libc::isatty(fd) } == 0 {
return Ok((24, 80));
}
let mut winsize: libc::winsize = unsafe { std::mem::zeroed() };
unsafe {
if libc::ioctl(fd, libc::TIOCGWINSZ, &mut winsize) != 0 {
return Err(Error::Io(io::Error::last_os_error()));
}
}
Ok((winsize.ws_row, winsize.ws_col))
}
#[cfg(not(unix))]
{
Err(Error::NotSupported)
}
}
pub(crate) fn add_to_update_buffer(content: &str) -> Result<()> {
let buffer = UPDATE_BUFFER.get_or_init(|| Mutex::new(String::new()));
let mut guard = buffer.lock().unwrap();
guard.push_str(content);
Ok(())
}
pub(crate) fn doupdate() -> Result<()> {
let buffer = UPDATE_BUFFER.get_or_init(|| Mutex::new(String::new()));
let mut guard = buffer.lock().unwrap();
if !guard.is_empty() {
io::stdout().write_all(guard.as_bytes())?;
io::stdout().flush()?;
guard.clear();
}
Ok(())
}
}
#[cfg(unix)]
fn wait_for_stdin(timeout_ms: Option<u64>) -> Result<bool> {
let fd = io::stdin().as_raw_fd();
unsafe {
let mut readfds: libc::fd_set = std::mem::zeroed();
libc::FD_ZERO(&mut readfds);
libc::FD_SET(fd, &mut readfds);
let result = if let Some(ms) = timeout_ms {
let mut tv = libc::timeval {
tv_sec: (ms / 1000) as libc::time_t,
tv_usec: ((ms % 1000) * 1000) as libc::suseconds_t,
};
libc::select(
fd + 1,
&mut readfds,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut tv,
)
} else {
libc::select(
fd + 1,
&mut readfds,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if result < 0 {
Err(Error::Io(io::Error::last_os_error()))
} else {
Ok(result > 0)
}
}
}
#[cfg(not(unix))]
fn wait_for_stdin(_: Option<u64>) -> Result<bool> {
Err(Error::NotSupported)
}
fn is_sequence_complete(seq: &[u8]) -> bool {
if seq.len() < 2 {
return false;
}
match seq[1] {
0x1b => is_sequence_complete(&seq[1..]),
b'O' => seq.len() >= 3,
b'[' => seq[2..].iter().any(|&b| (0x40..=0x7e).contains(&b)),
_ => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_creation() {
let backend = Backend::new();
assert!(!backend.initialized);
assert!(backend.original_termios.is_none());
}
#[test]
#[cfg(unix)]
fn test_terminal_size() {
if let Ok((rows, cols)) = Backend::get_terminal_size() {
assert!(rows > 0);
assert!(cols > 0);
}
}
#[test]
fn input_buf_capacity_matches_libc_bufsiz_per_platform() {
#[cfg(target_os = "linux")]
assert_eq!(INPUT_BUF_CAPACITY, 8192);
#[cfg(not(target_os = "linux"))]
assert_eq!(INPUT_BUF_CAPACITY, 1024);
}
#[test]
fn find_subseq_basic() {
assert_eq!(find_subseq(b"hello world", b"world"), Some(6));
assert_eq!(find_subseq(b"hello world", b"hello"), Some(0));
assert_eq!(find_subseq(b"hello world", b"xyz"), None);
assert_eq!(find_subseq(b"abc", b"abcdef"), None);
assert_eq!(find_subseq(b"abc", b""), None);
}
#[test]
fn drain_until_splits_at_marker_no_io() {
let mut b = InputBuf {
bytes: b"pasted text\x1b[201~next key".to_vec(),
last_read_filled: false,
};
let content = b.drain_until(b"\x1b[201~").unwrap();
assert_eq!(&content, b"pasted text");
assert_eq!(&b.bytes, b"next key");
}
#[test]
fn drain_until_marker_at_start_returns_empty() {
let mut b = InputBuf {
bytes: b"\x1b[201~tail".to_vec(),
last_read_filled: false,
};
let content = b.drain_until(b"\x1b[201~").unwrap();
assert!(content.is_empty());
assert_eq!(&b.bytes, b"tail");
}
#[test]
fn pop_byte_drains_in_order() {
let mut b = InputBuf {
bytes: vec![1, 2, 3],
last_read_filled: false,
};
assert_eq!(b.pop_byte(), Some(1));
assert_eq!(b.pop_byte(), Some(2));
assert_eq!(b.pop_byte(), Some(3));
assert_eq!(b.pop_byte(), None);
}
#[test]
fn bare_escape_is_never_complete() {
assert!(!is_sequence_complete(b"\x1b"));
}
#[test]
fn meta_two_byte_is_complete() {
assert!(is_sequence_complete(b"\x1bb"));
assert!(is_sequence_complete(b"\x1bf"));
}
#[test]
fn ss3_completes_at_three_bytes() {
assert!(!is_sequence_complete(b"\x1bO"));
assert!(is_sequence_complete(b"\x1bOP"));
}
#[test]
fn csi_completes_at_final_byte() {
assert!(!is_sequence_complete(b"\x1b["));
assert!(!is_sequence_complete(b"\x1b[1"));
assert!(!is_sequence_complete(b"\x1b[1;3"));
assert!(is_sequence_complete(b"\x1b[D"));
assert!(is_sequence_complete(b"\x1b[1;3D"));
assert!(is_sequence_complete(b"\x1b[97;5:2u"));
}
#[test]
fn meta_prefix_recurses_into_inner() {
assert!(!is_sequence_complete(b"\x1b\x1b"));
assert!(!is_sequence_complete(b"\x1b\x1b["));
assert!(!is_sequence_complete(b"\x1b\x1b[1;3"));
assert!(is_sequence_complete(b"\x1b\x1b[D"));
assert!(is_sequence_complete(b"\x1b\x1b[1;3D"));
assert!(is_sequence_complete(b"\x1b\x1bb"));
assert!(is_sequence_complete(b"\x1b\x1bOP"));
}
}