use std::io;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
use std::path::PathBuf;
use anyhow::{Context, Result};
use nix::libc;
use nix::pty::{OpenptyResult, Winsize, openpty};
use nix::unistd;
use tokio::io::unix::AsyncFd;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
pub const DEFAULT_TERM: &str = "xterm-256color";
pub const DEFAULT_COLS: u32 = 80;
pub const DEFAULT_ROWS: u32 = 24;
const MAX_DIMENSION: u32 = u16::MAX as u32;
#[derive(Debug, Clone)]
pub struct PtyConfig {
pub term: String,
pub col_width: u32,
pub row_height: u32,
pub pix_width: u32,
pub pix_height: u32,
}
impl PtyConfig {
pub fn new(
term: String,
col_width: u32,
row_height: u32,
pix_width: u32,
pix_height: u32,
) -> Self {
Self {
term,
col_width,
row_height,
pix_width,
pix_height,
}
}
pub fn winsize(&self) -> Winsize {
Winsize {
ws_row: self.row_height.min(MAX_DIMENSION) as u16,
ws_col: self.col_width.min(MAX_DIMENSION) as u16,
ws_xpixel: self.pix_width.min(MAX_DIMENSION) as u16,
ws_ypixel: self.pix_height.min(MAX_DIMENSION) as u16,
}
}
}
impl Default for PtyConfig {
fn default() -> Self {
Self {
term: DEFAULT_TERM.to_string(),
col_width: DEFAULT_COLS,
row_height: DEFAULT_ROWS,
pix_width: 0,
pix_height: 0,
}
}
}
pub struct PtyMaster {
config: PtyConfig,
async_fd: AsyncFd<OwnedFd>,
slave_path: PathBuf,
}
impl PtyMaster {
pub fn open(config: PtyConfig) -> Result<Self> {
let OpenptyResult {
master: master_fd,
slave: slave_fd,
} = openpty(None, None).context("Failed to open PTY pair")?;
let slave_path =
unistd::ttyname(slave_fd.as_fd()).context("Failed to get slave TTY path")?;
Self::set_window_size_fd(slave_fd.as_fd(), &config.winsize())
.context("Failed to set initial window size")?;
drop(slave_fd);
Self::set_nonblocking(master_fd.as_fd())?;
let async_fd = AsyncFd::new(master_fd).context("Failed to create AsyncFd")?;
Ok(Self {
config,
async_fd,
slave_path,
})
}
pub fn slave_path(&self) -> &PathBuf {
&self.slave_path
}
pub fn config(&self) -> &PtyConfig {
&self.config
}
pub fn as_raw_fd(&self) -> RawFd {
self.async_fd.get_ref().as_raw_fd()
}
pub fn resize(&mut self, cols: u32, rows: u32) -> Result<()> {
self.config.col_width = cols;
self.config.row_height = rows;
let winsize = self.config.winsize();
Self::set_window_size_fd(self.async_fd.get_ref().as_fd(), &winsize)
}
fn set_window_size_fd(fd: BorrowedFd<'_>, winsize: &Winsize) -> Result<()> {
let result = unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCSWINSZ, winsize) };
if result < 0 {
Err(io::Error::last_os_error()).context("Failed to set window size (TIOCSWINSZ ioctl)")
} else {
Ok(())
}
}
fn set_nonblocking(fd: BorrowedFd<'_>) -> Result<()> {
let flags = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_GETFL).context("F_GETFL")?;
let new_flags =
nix::fcntl::OFlag::from_bits_truncate(flags) | nix::fcntl::OFlag::O_NONBLOCK;
nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_SETFL(new_flags)).context("F_SETFL")?;
Ok(())
}
pub async fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
loop {
let mut guard = self.async_fd.readable().await?;
match guard.try_io(|inner| {
let fd = inner.get_ref().as_raw_fd();
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
if n < 0 {
Err(io::Error::last_os_error())
} else {
Ok(n as usize)
}
}) {
Ok(result) => return result,
Err(_would_block) => continue,
}
}
}
pub async fn write(&self, buf: &[u8]) -> io::Result<usize> {
loop {
let mut guard = self.async_fd.writable().await?;
match guard.try_io(|inner| {
let fd = inner.get_ref().as_raw_fd();
let n = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) };
if n < 0 {
Err(io::Error::last_os_error())
} else {
Ok(n as usize)
}
}) {
Ok(result) => return result,
Err(_would_block) => continue,
}
}
}
pub async fn write_all(&self, mut buf: &[u8]) -> io::Result<()> {
while !buf.is_empty() {
let n = self.write(buf).await?;
buf = &buf[n..];
}
Ok(())
}
}
impl std::fmt::Debug for PtyMaster {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PtyMaster")
.field("config", &self.config)
.field("slave_path", &self.slave_path)
.field("fd", &self.as_raw_fd())
.finish()
}
}
pub struct PtyReader<'a> {
pty: &'a PtyMaster,
}
impl<'a> PtyReader<'a> {
pub fn new(pty: &'a PtyMaster) -> Self {
Self { pty }
}
}
impl AsyncRead for PtyReader<'_> {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> std::task::Poll<io::Result<()>> {
loop {
let mut guard = match self.pty.async_fd.poll_read_ready(cx) {
std::task::Poll::Ready(Ok(guard)) => guard,
std::task::Poll::Ready(Err(e)) => return std::task::Poll::Ready(Err(e)),
std::task::Poll::Pending => return std::task::Poll::Pending,
};
let unfilled = buf.initialize_unfilled();
let fd = self.pty.async_fd.get_ref().as_raw_fd();
let result = unsafe { libc::read(fd, unfilled.as_mut_ptr() as *mut _, unfilled.len()) };
if result < 0 {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::WouldBlock {
guard.clear_ready();
continue;
}
return std::task::Poll::Ready(Err(err));
}
buf.advance(result as usize);
return std::task::Poll::Ready(Ok(()));
}
}
}
pub struct PtyWriter<'a> {
pty: &'a PtyMaster,
}
impl<'a> PtyWriter<'a> {
pub fn new(pty: &'a PtyMaster) -> Self {
Self { pty }
}
}
impl AsyncWrite for PtyWriter<'_> {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<io::Result<usize>> {
loop {
let mut guard = match self.pty.async_fd.poll_write_ready(cx) {
std::task::Poll::Ready(Ok(guard)) => guard,
std::task::Poll::Ready(Err(e)) => return std::task::Poll::Ready(Err(e)),
std::task::Poll::Pending => return std::task::Poll::Pending,
};
let fd = self.pty.async_fd.get_ref().as_raw_fd();
let result = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) };
if result < 0 {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::WouldBlock {
guard.clear_ready();
continue;
}
return std::task::Poll::Ready(Err(err));
}
return std::task::Poll::Ready(Ok(result as usize));
}
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pty_config_default() {
let config = PtyConfig::default();
assert_eq!(config.term, DEFAULT_TERM);
assert_eq!(config.col_width, DEFAULT_COLS);
assert_eq!(config.row_height, DEFAULT_ROWS);
assert_eq!(config.pix_width, 0);
assert_eq!(config.pix_height, 0);
}
#[test]
fn test_pty_config_new() {
let config = PtyConfig::new("vt100".to_string(), 132, 50, 1024, 768);
assert_eq!(config.term, "vt100");
assert_eq!(config.col_width, 132);
assert_eq!(config.row_height, 50);
assert_eq!(config.pix_width, 1024);
assert_eq!(config.pix_height, 768);
}
#[test]
fn test_pty_config_winsize() {
let config = PtyConfig::new("xterm".to_string(), 80, 24, 640, 480);
let winsize = config.winsize();
assert_eq!(winsize.ws_col, 80);
assert_eq!(winsize.ws_row, 24);
assert_eq!(winsize.ws_xpixel, 640);
assert_eq!(winsize.ws_ypixel, 480);
}
#[test]
fn test_pty_config_winsize_overflow_clamping() {
let config = PtyConfig::new("xterm".to_string(), 100_000, 100_000, 100_000, 100_000);
let winsize = config.winsize();
assert_eq!(winsize.ws_col, u16::MAX);
assert_eq!(winsize.ws_row, u16::MAX);
assert_eq!(winsize.ws_xpixel, u16::MAX);
assert_eq!(winsize.ws_ypixel, u16::MAX);
}
#[tokio::test]
async fn test_pty_master_open() {
let config = PtyConfig::default();
let result = PtyMaster::open(config);
assert!(result.is_ok(), "Failed to open PTY: {:?}", result.err());
let pty = result.unwrap();
assert!(pty.slave_path().exists());
assert!(pty.as_raw_fd() >= 0);
}
#[tokio::test]
async fn test_pty_master_resize() {
let config = PtyConfig::default();
let mut pty = PtyMaster::open(config).expect("Failed to open PTY");
assert!(pty.resize(120, 40).is_ok());
assert_eq!(pty.config().col_width, 120);
assert_eq!(pty.config().row_height, 40);
}
#[tokio::test]
async fn test_pty_master_read_write() {
use std::fs::OpenOptions;
let config = PtyConfig::default();
let pty = PtyMaster::open(config).expect("Failed to open PTY");
let slave_path = pty.slave_path();
let _slave = OpenOptions::new()
.read(true)
.write(true)
.open(slave_path)
.expect("Failed to open PTY slave");
let test_data = b"hello\n";
let write_result = pty.write(test_data).await;
assert!(
write_result.is_ok(),
"Write failed: {:?}",
write_result.err()
);
}
#[tokio::test]
async fn test_pty_master_debug() {
let config = PtyConfig::default();
let pty = PtyMaster::open(config).expect("Failed to open PTY");
let debug = format!("{:?}", pty);
assert!(debug.contains("PtyMaster"));
assert!(debug.contains("config"));
assert!(debug.contains("slave_path"));
}
#[test]
fn test_default_constants() {
assert_eq!(DEFAULT_TERM, "xterm-256color");
assert_eq!(DEFAULT_COLS, 80);
assert_eq!(DEFAULT_ROWS, 24);
}
#[test]
fn test_pty_config_clone() {
let config = PtyConfig::new("vt220".to_string(), 100, 30, 500, 400);
let cloned = config.clone();
assert_eq!(config.term, cloned.term);
assert_eq!(config.col_width, cloned.col_width);
assert_eq!(config.row_height, cloned.row_height);
assert_eq!(config.pix_width, cloned.pix_width);
assert_eq!(config.pix_height, cloned.pix_height);
}
#[test]
fn test_pty_config_debug() {
let config = PtyConfig::new("screen".to_string(), 200, 60, 1920, 1080);
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("PtyConfig"));
assert!(debug_str.contains("screen"));
assert!(debug_str.contains("200"));
assert!(debug_str.contains("60"));
}
#[test]
fn test_winsize_boundary_values() {
let config = PtyConfig::new("xterm".to_string(), 0, 0, 0, 0);
let winsize = config.winsize();
assert_eq!(winsize.ws_col, 0);
assert_eq!(winsize.ws_row, 0);
assert_eq!(winsize.ws_xpixel, 0);
assert_eq!(winsize.ws_ypixel, 0);
let config = PtyConfig::new(
"xterm".to_string(),
u16::MAX as u32,
u16::MAX as u32,
u16::MAX as u32,
u16::MAX as u32,
);
let winsize = config.winsize();
assert_eq!(winsize.ws_col, u16::MAX);
assert_eq!(winsize.ws_row, u16::MAX);
assert_eq!(winsize.ws_xpixel, u16::MAX);
assert_eq!(winsize.ws_ypixel, u16::MAX);
}
#[tokio::test]
async fn test_pty_master_fd_validity() {
let config = PtyConfig::default();
let pty = PtyMaster::open(config).expect("Failed to open PTY");
let fd = pty.as_raw_fd();
assert!(
fd >= 0,
"PTY file descriptor should be valid (non-negative)"
);
}
#[tokio::test]
async fn test_pty_master_slave_path_format() {
let config = PtyConfig::default();
let pty = PtyMaster::open(config).expect("Failed to open PTY");
let slave_path = pty.slave_path();
let path_str = slave_path.to_string_lossy();
assert!(
path_str.starts_with("/dev/pts/") || path_str.starts_with("/dev/tty"),
"Slave path should be a PTY device: {}",
path_str
);
}
#[tokio::test]
async fn test_pty_master_config_accessor() {
let config = PtyConfig::new("linux".to_string(), 132, 43, 1024, 768);
let pty = PtyMaster::open(config).expect("Failed to open PTY");
let retrieved_config = pty.config();
assert_eq!(retrieved_config.term, "linux");
assert_eq!(retrieved_config.col_width, 132);
assert_eq!(retrieved_config.row_height, 43);
}
#[tokio::test]
async fn test_pty_master_multiple_resizes() {
let config = PtyConfig::default();
let mut pty = PtyMaster::open(config).expect("Failed to open PTY");
assert!(pty.resize(100, 30).is_ok());
assert_eq!(pty.config().col_width, 100);
assert_eq!(pty.config().row_height, 30);
assert!(pty.resize(200, 50).is_ok());
assert_eq!(pty.config().col_width, 200);
assert_eq!(pty.config().row_height, 50);
assert!(pty.resize(80, 24).is_ok());
assert_eq!(pty.config().col_width, 80);
assert_eq!(pty.config().row_height, 24);
}
#[test]
fn test_pty_reader_new() {
}
#[test]
fn test_pty_writer_new() {
}
}