Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion crates/pty/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,15 @@ version.workspace = true
edition.workspace = true
license.workspace = true

[dependencies]
[target.'cfg(unix)'.dependencies]
rustix = { version = "0.38", features = ["pty", "process", "termios", "fs", "event", "stdio"] }

[target.'cfg(windows)'.dependencies]
windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Storage_FileSystem",
"Win32_System_Console",
"Win32_System_Pipes",
"Win32_System_Threading",
] }
28 changes: 20 additions & 8 deletions crates/pty/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
//! Unix pseudo-terminal management for the Prompt terminal emulator.
//! Pseudo-terminal management for the Prompt terminal emulator.
//!
//! Open a pty pair, spawn a shell (or any argv) on the slave, and drive the
//! master: read child output, write input, resize, kill/wait.
//! Open a pty, spawn a shell (or any argv) attached to it, and drive it: read
//! child output, write input, resize, kill/wait. Two backends implement one
//! [`Pty`] API — a Unix pty pair (`rustix`) and a Windows pseudoconsole
//! (ConPTY, via the `windows` crate).
//!
//! ```no_run
//! let opts = pty::SpawnOptions::default(); // user's login shell
//! let session = pty::Pty::spawn(&opts).unwrap();
//! session.resize(pty::Winsize::new(120, 40)).unwrap();
//! ```

#![cfg(unix)]
mod spawn;
mod winsize;

#[cfg(unix)]
mod session;
mod spawn;
#[cfg(unix)]
mod unix;
mod winsize;

pub use session::Pty;
#[cfg(windows)]
mod windows;

pub use spawn::{default_env, default_shell, SpawnOptions};
pub use unix::{open_pair, spawn_child, PtyPair};
pub use winsize::Winsize;

#[cfg(unix)]
pub use session::Pty;
#[cfg(unix)]
pub use unix::{open_pair, spawn_child, PtyPair};

#[cfg(windows)]
pub use windows::Pty;
10 changes: 10 additions & 0 deletions crates/pty/src/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,23 @@ pub struct SpawnOptions {
}

/// The user's shell from `$SHELL`, falling back to `/bin/zsh`.
#[cfg(unix)]
pub fn default_shell() -> String {
std::env::var("SHELL")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "/bin/zsh".to_string())
}

/// The command interpreter from `%COMSPEC%`, falling back to `cmd.exe`.
#[cfg(windows)]
pub fn default_shell() -> String {
std::env::var("COMSPEC")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "cmd.exe".to_string())
}

/// Default environment overrides for terminal children.
pub fn default_env() -> Vec<(String, String)> {
vec![
Expand Down
82 changes: 82 additions & 0 deletions crates/pty/src/windows/conpty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! The pseudoconsole guard and pipe wiring.
//!
//! [`create`] makes two anonymous pipes and an `HPCON`. ConPTY keeps the
//! child's stdin-read and stdout-write ends; the caller receives the opposite
//! ends — `input` to write to the child, `output` to read its bytes — plus a
//! [`Pcon`] guard that closes the pseudoconsole on drop.

use std::io;
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};

use windows::Win32::Foundation::HANDLE;
use windows::Win32::System::Console::{
ClosePseudoConsole, CreatePseudoConsole, ResizePseudoConsole, COORD, HPCON,
};
use windows::Win32::System::Pipes::CreatePipe;

use crate::winsize::Winsize;

/// Owns an `HPCON`, closing it on drop.
pub struct Pcon(HPCON);

impl Pcon {
/// The raw handle, for the process-creation attribute list.
pub fn handle(&self) -> HPCON {
self.0
}

/// Resize the pseudoconsole; the child sees a console-resize event.
pub fn resize(&self, size: Winsize) -> io::Result<()> {
unsafe { ResizePseudoConsole(self.0, coord(size)) }.map_err(io::Error::other)
}
}

impl Drop for Pcon {
fn drop(&mut self) {
unsafe { ClosePseudoConsole(self.0) };
}
}

/// Create a pseudoconsole sized to `size`. Returns the guard plus the input
/// (write) and output (read) pipe ends we keep.
pub fn create(size: Winsize) -> io::Result<(Pcon, OwnedHandle, OwnedHandle)> {
let (in_read, in_write) = pipe()?;
let (out_read, out_write) = pipe()?;

let handle = unsafe {
CreatePseudoConsole(coord(size), to_handle(&in_read), to_handle(&out_write), 0)
}
.map_err(io::Error::other)?;

// ConPTY duplicates the ends it needs; drop our copies of them.
drop(in_read);
drop(out_write);

Ok((Pcon(handle), in_write, out_read))
}

/// A pseudoconsole grid size as a console `COORD` (columns = X, rows = Y).
fn coord(size: Winsize) -> COORD {
COORD {
X: size.cols as i16,
Y: size.rows as i16,
}
}

/// Create an anonymous pipe, returning `(read, write)` owned ends.
fn pipe() -> io::Result<(OwnedHandle, OwnedHandle)> {
let mut read = HANDLE::default();
let mut write = HANDLE::default();
unsafe { CreatePipe(&mut read, &mut write, None, 0) }.map_err(io::Error::other)?;
Ok(unsafe { (own(read), own(write)) })
}

/// Borrow an owned handle as a Win32 `HANDLE`.
fn to_handle(h: &OwnedHandle) -> HANDLE {
HANDLE(h.as_raw_handle())
}

/// Take ownership of a raw Win32 `HANDLE` as an `OwnedHandle`.
unsafe fn own(h: HANDLE) -> OwnedHandle {
OwnedHandle::from_raw_handle(h.0)
}
95 changes: 95 additions & 0 deletions crates/pty/src/windows/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Windows pseudoconsole (ConPTY) backend.
//!
//! Mirrors the Unix [`crate::session::Pty`] API: open a pseudoconsole, spawn a
//! child attached to it, and drive it — read output, write input, resize,
//! kill/wait.

mod conpty;
mod spawn;

use std::fs::File;
use std::io::{self, Read, Write};
use std::process::ExitStatus;

use crate::spawn::SpawnOptions;
use crate::winsize::Winsize;

use conpty::Pcon;
use spawn::Child;

/// A child attached to a pseudoconsole. Owns the console guard, the two pipe
/// ends (as `File`s), and the child handle.
pub struct Pty {
// Field order is drop order: close the pipes, then the pseudoconsole, then
// the child handle.
input: File,
output: File,
pcon: Pcon,
child: Child,
}

impl Pty {
/// Open a pseudoconsole sized from `opts`, then spawn the child on it.
pub fn spawn(opts: &SpawnOptions) -> io::Result<Self> {
let (pcon, input, output) = conpty::create(opts.winsize)?;
let child = spawn::spawn_child(opts, pcon.handle())?;
Ok(Self {
input: File::from(input),
output: File::from(output),
pcon,
child,
})
}

/// Write bytes to the child's input. Blocking.
pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
(&self.input).write(buf)
}

/// Read bytes of child output. Blocking; returns `Ok(0)` at EOF.
pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
(&self.output).read(buf)
}

/// A `File` over a duplicate of the output pipe, for a reader thread.
pub fn try_clone_reader(&self) -> io::Result<File> {
self.output.try_clone()
}

/// A `File` over a duplicate of the input pipe, for a writer.
pub fn try_clone_writer(&self) -> io::Result<File> {
self.input.try_clone()
}

/// Resize the pseudoconsole. The child sees a console-resize event.
pub fn resize(&self, size: Winsize) -> io::Result<()> {
self.pcon.resize(size)
}

/// OS pid of the child.
pub fn child_pid(&self) -> u32 {
self.child.pid()
}

/// Whether a foreground job other than the shell is running. ConPTY exposes
/// no controlling-terminal foreground-group query, so this is always
/// `false` on Windows (callers treat it as "not busy").
pub fn foreground_running(&self) -> bool {
false
}

/// Force-terminate the child.
pub fn kill(&mut self) -> io::Result<()> {
self.child.kill()
}

/// Wait for the child to exit.
pub fn wait(&mut self) -> io::Result<ExitStatus> {
self.child.wait()
}

/// Non-blocking check for child exit.
pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
self.child.try_wait()
}
}
Loading
Loading