use std::{fs::File, io};
use parking_lot::Mutex;
use crate::input::{InputDecoder, InputEvent, Keymap};
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TerminalEvent {
Input(InputEvent),
Resize,
Debug(DebugQuery),
Closed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DebugQuery {
pub id: u64,
pub op: DebugOp,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum DebugOp {
Info,
Text,
Resize,
Quit,
Frame,
Tree,
Values,
}
enum Ctl {
Event(TerminalEvent),
Bytes(Vec<u8>),
Keymap(Keymap),
}
static SHARED_CTL: Mutex<Option<flume::Sender<Ctl>>> = Mutex::new(None);
pub fn send_event(event: TerminalEvent) -> bool {
send_ctl(Ctl::Event(event))
}
pub fn inject_bytes(bytes: Vec<u8>) -> bool {
send_ctl(Ctl::Bytes(bytes))
}
fn send_ctl(ctl: Ctl) -> bool {
let sender = SHARED_CTL.lock().clone();
sender.is_some_and(|sender| sender.send(ctl).is_ok())
}
#[cfg(test)]
pub fn publish_ingress_for_test() -> flume::Receiver<TerminalEvent> {
let (ctl_tx, ctl_rx) = flume::unbounded();
let (event_tx, event_rx) = flume::unbounded();
*SHARED_CTL.lock() = Some(ctl_tx);
std::thread::spawn(move || {
while let Ok(ctl) = ctl_rx.recv() {
if let Ctl::Event(event) = ctl
&& event_tx.send(event).is_err()
{
return;
}
}
});
event_rx
}
pub struct Pump {
task: tokio::task::JoinHandle<()>,
bridge: Option<Bridge>,
ctl: flume::Sender<Ctl>,
}
struct Bridge {
stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
worker: Option<std::thread::JoinHandle<()>>,
}
impl Pump {
pub(crate) fn publish(&self) {
*SHARED_CTL.lock() = Some(self.ctl.clone());
}
pub(crate) fn set_keymap(&self, keymap: Keymap) {
let _ = self.ctl.send(Ctl::Keymap(keymap));
}
pub(crate) fn stop(&mut self) {
self.task.abort();
if let Some(bridge) = self.bridge.as_mut() {
bridge
.stop
.store(true, std::sync::atomic::Ordering::Release);
if let Some(worker) = bridge.worker.take() {
let _ = worker.join();
}
}
}
}
impl Drop for Pump {
fn drop(&mut self) {
self.stop();
}
}
pub struct PumpChannels {
pub pump: Pump,
pub events: flume::Receiver<TerminalEvent>,
pub resize: tokio::sync::watch::Receiver<u64>,
}
enum ByteSource {
#[cfg(unix)]
Fd(tokio::io::unix::AsyncFd<std::os::fd::OwnedFd>),
Thread(flume::Receiver<Vec<u8>>),
}
impl ByteSource {
async fn next(&mut self) -> io::Result<Option<Vec<u8>>> {
match self {
#[cfg(unix)]
Self::Fd(fd) => loop {
let mut guard = fd.readable().await?;
let mut bytes = [0_u8; 4096];
match guard.try_io(|fd| read_fd(fd.get_ref(), &mut bytes)) {
Ok(Ok(0)) => return Ok(None),
Ok(Ok(read)) => return Ok(Some(bytes[..read].to_vec())),
Ok(Err(error)) => return Err(error),
Err(_) => {},
}
},
Self::Thread(rx) => Ok(rx.recv_async().await.ok()),
}
}
}
#[cfg(unix)]
fn read_fd(fd: &std::os::fd::OwnedFd, bytes: &mut [u8]) -> io::Result<usize> {
use std::os::fd::AsRawFd as _;
loop {
let read = unsafe { nix::libc::read(fd.as_raw_fd(), bytes.as_mut_ptr().cast(), bytes.len()) };
if read >= 0 {
return Ok(read as usize);
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted {
return Err(error);
}
}
}
pub fn spawn(
input: Input,
mut decoder: InputDecoder,
preserved: &[u8],
#[cfg_attr(windows, expect(unused_variables, reason = "windows polls geometry instead"))]
resize: Option<ResizeFd>,
) -> io::Result<PumpChannels> {
let (events_tx, events_rx) = flume::unbounded();
let (resize_tx, resize_rx) = tokio::sync::watch::channel(0_u64);
let (ctl_tx, ctl_rx) = flume::unbounded();
let (source, bridge) = input.into_source()?;
#[cfg(unix)]
let resize = resize.map(tokio::io::unix::AsyncFd::new).transpose()?;
#[cfg(windows)]
let resize = ();
let mut events = Vec::new();
decoder.feed(preserved, std::time::Instant::now(), &mut events);
let task = tokio::spawn(actor(source, decoder, events, events_tx, ctl_rx, resize, resize_tx));
Ok(PumpChannels {
pump: Pump { task, bridge, ctl: ctl_tx },
events: events_rx,
resize: resize_rx,
})
}
#[cfg(unix)]
pub type ResizeFd = std::os::fd::OwnedFd;
#[cfg(windows)]
pub(crate) type ResizeFd = std::convert::Infallible;
pub enum Input {
#[cfg(unix)]
#[cfg_attr(
target_os = "macos",
allow(dead_code, reason = "macOS terminals bridge; tests spawn pollable pipe sources")
)]
Pollable(File),
Bridged(File),
}
impl Input {
fn into_source(self) -> io::Result<(ByteSource, Option<Bridge>)> {
match self {
#[cfg(unix)]
Self::Pollable(file) => {
use std::os::fd::AsRawFd as _;
if unsafe {
nix::libc::fcntl(file.as_raw_fd(), nix::libc::F_SETFL, nix::libc::O_NONBLOCK)
} < 0
{
return Err(io::Error::last_os_error());
}
let fd = tokio::io::unix::AsyncFd::new(std::os::fd::OwnedFd::from(file))?;
Ok((ByteSource::Fd(fd), None))
},
Self::Bridged(file) => {
let (tx, rx) = flume::unbounded();
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let bridge_stop = std::sync::Arc::clone(&stop);
let worker = std::thread::Builder::new()
.name("omp-tui-input".into())
.spawn(move || bridge_loop(file, &tx, &bridge_stop))?;
Ok((ByteSource::Thread(rx), Some(Bridge { stop, worker: Some(worker) })))
},
}
}
}
fn bridge_loop(input: File, tx: &flume::Sender<Vec<u8>>, stop: &std::sync::atomic::AtomicBool) {
use std::sync::atomic::Ordering;
let mut bytes = [0_u8; 4096];
#[cfg(unix)]
{
use std::{io::Read as _, os::fd::AsRawFd as _};
let mut input = input;
let mut descriptor =
nix::libc::pollfd { fd: input.as_raw_fd(), events: nix::libc::POLLIN, revents: 0 };
while !stop.load(Ordering::Acquire) {
descriptor.revents = 0;
let ready = unsafe { nix::libc::poll(&mut descriptor, 1, 50) };
if ready < 0 {
if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
continue;
}
return;
}
if ready == 0 {
continue;
}
if descriptor.revents & (nix::libc::POLLERR | nix::libc::POLLNVAL) != 0 {
return;
}
match input.read(&mut bytes) {
Ok(0) => return,
Ok(read) => {
if tx.send(bytes[..read].to_vec()).is_err() {
return;
}
},
Err(error)
if matches!(
error.kind(),
io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
) => {},
Err(_) => return,
}
}
}
#[cfg(windows)]
{
use std::{io::Read as _, os::windows::io::AsRawHandle as _};
let mut input = input;
let handle = input.as_raw_handle();
while !stop.load(Ordering::Acquire) {
let ready =
unsafe { windows_sys::Win32::System::Threading::WaitForSingleObject(handle, 50) };
if ready == windows_sys::Win32::Foundation::WAIT_TIMEOUT {
continue;
}
if ready != windows_sys::Win32::Foundation::WAIT_OBJECT_0 {
return;
}
match input.read(&mut bytes) {
Ok(0) => return,
Ok(read) => {
if tx.send(bytes[..read].to_vec()).is_err() {
return;
}
},
Err(_) => return,
}
}
}
}
async fn actor(
mut source: ByteSource,
mut decoder: InputDecoder,
mut events: Vec<InputEvent>,
events_tx: flume::Sender<TerminalEvent>,
ctl_rx: flume::Receiver<Ctl>,
#[cfg(unix)] resize: Option<tokio::io::unix::AsyncFd<std::os::fd::OwnedFd>>,
#[cfg(windows)] resize: (),
resize_tx: tokio::sync::watch::Sender<u64>,
) {
let mut resize_wakes = 0_u64;
loop {
for event in std::mem::take(&mut events) {
if events_tx.send(TerminalEvent::Input(event)).is_err() {
return;
}
}
let wake = decoder.deadline().map(tokio::time::Instant::from_std);
tokio::select! {
biased;
() = resize_readable(#[cfg(unix)] resize.as_ref()) => {
resize_wakes += 1;
if resize_tx.send(resize_wakes).is_err() {
return;
}
},
chunk = source.next() => if let Ok(Some(bytes)) = chunk {
decoder.feed(&bytes, std::time::Instant::now(), &mut events);
} else {
let _ = events_tx.send(TerminalEvent::Closed);
return;
},
ctl = ctl_rx.recv_async() => {
let Ok(ctl) = ctl else {
return;
};
if !apply_ctl(ctl, &mut decoder, &mut events, &events_tx) {
return;
}
},
() = deadline(wake) => {
decoder.tick(std::time::Instant::now(), &mut events);
},
}
}
}
fn apply_ctl(
ctl: Ctl,
decoder: &mut InputDecoder,
events: &mut Vec<InputEvent>,
events_tx: &flume::Sender<TerminalEvent>,
) -> bool {
match ctl {
Ctl::Bytes(bytes) => {
decoder.feed(&bytes, std::time::Instant::now(), events);
true
},
Ctl::Event(event) => {
for decoded in events.drain(..) {
if events_tx.send(TerminalEvent::Input(decoded)).is_err() {
return false;
}
}
events_tx.send(event).is_ok()
},
Ctl::Keymap(keymap) => {
*decoder.keymap_mut() = keymap;
true
},
}
}
#[cfg(unix)]
async fn resize_readable(resize: Option<&tokio::io::unix::AsyncFd<std::os::fd::OwnedFd>>) -> () {
let Some(fd) = resize else {
return std::future::pending().await;
};
loop {
let Ok(mut guard) = fd.readable().await else {
return std::future::pending().await;
};
let mut bytes = [0_u8; 128];
match guard.try_io(|fd| read_fd(fd.get_ref(), &mut bytes)) {
Ok(Ok(0)) => return std::future::pending().await,
Ok(Ok(_)) => return,
Ok(Err(_)) => return std::future::pending().await,
Err(_) => {},
}
}
}
#[cfg(windows)]
async fn resize_readable(_resize: ()) {
std::future::pending().await
}
async fn deadline(at: Option<tokio::time::Instant>) {
match at {
Some(at) => tokio::time::sleep_until(at).await,
None => std::future::pending().await,
}
}