ref:main
//! Plain shell backend — wraps `alacritty_terminal::tty::Pty`.
//!
//! This is the extraction of PTY spawning logic that previously lived inline
//! in `TerminalState::new()`. The session is a thin wrapper around the Pty,
//! adding non-blocking reads and alive tracking.
use super::{BackendConfig, TerminalBackend, TerminalSession};
use alacritty_terminal::event::{OnResize, WindowSize};
use alacritty_terminal::tty::{self, EventedReadWrite, Options as TtyOptions};
use std::io::{Read, Write};
use std::path::PathBuf;
/// Backend that spawns a local shell via a PTY.
pub struct PlainShellBackend;
impl TerminalBackend for PlainShellBackend {
fn spawn(&self, config: &BackendConfig) -> Result<Box<dyn TerminalSession>, String> {
// Determine shell
let shell_path = if config.shell.is_empty() {
#[cfg(not(windows))]
{
std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string())
}
#[cfg(windows)]
{
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe".to_string()
}
} else {
config.shell.clone()
};
let working_directory = if config.working_dir.is_empty() {
std::env::current_dir().ok()
} else {
Some(PathBuf::from(&config.working_dir))
};
let mut env = std::collections::HashMap::new();
env.insert("TERM".to_string(), "xterm-256color".to_string());
env.insert("COLORTERM".to_string(), "truecolor".to_string());
#[cfg(not(windows))]
let pty_config = TtyOptions {
shell: Some(tty::Shell::new(shell_path, vec![])),
working_directory,
env,
drain_on_exit: true,
};
#[cfg(windows)]
let pty_config = TtyOptions {
shell: Some(tty::Shell::new(shell_path, vec![])),
working_directory,
env,
drain_on_exit: false,
escape_args: false,
};
// Use font_size to approximate cell dimensions for the PTY.
// These are only hints for the PTY driver; rendering uses real metrics.
let cell_width = (config.font_size * 0.6) as u16;
let cell_height = (config.font_size * 1.4) as u16;
let window_size = WindowSize {
num_lines: config.rows,
num_cols: config.cols,
cell_width,
cell_height,
};
let pty = tty::new(&pty_config, window_size, 0)
.map_err(|e| format!("Failed to create PTY: {}", e))?;
Ok(Box::new(PlainShellSession { pty, alive: true }))
}
fn name(&self) -> &'static str {
"plain"
}
fn is_available(&self) -> Result<bool, String> {
#[cfg(not(windows))]
{
Ok(PathBuf::from("/bin/bash").exists() || PathBuf::from("/bin/zsh").exists())
}
#[cfg(windows)]
{
Ok(true) // PowerShell is always available on Windows
}
}
}
/// A live PTY session wrapping `alacritty_terminal::tty::Pty`.
struct PlainShellSession {
pty: tty::Pty,
alive: bool,
}
impl TerminalSession for PlainShellSession {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
// Set non-blocking before each read to guarantee we never block
// the game tick thread. This is defensive — alacritty_terminal
// should set non-blocking via mio, but we bypass mio with direct read().
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = self.pty.reader().as_raw_fd();
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags >= 0 && (flags & libc::O_NONBLOCK) == 0 {
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
}
}
let result: Result<usize, std::io::Error> = self.pty.reader().read(buf);
match &result {
Ok(0) => {
self.alive = false;
}
Err(e) if e.kind() != std::io::ErrorKind::WouldBlock => {
self.alive = false;
}
_ => {}
}
result
}
fn write(&mut self, data: &[u8]) -> Result<usize, std::io::Error> {
self.pty.writer().write_all(data)?;
self.pty.writer().flush()?;
Ok(data.len())
}
fn resize(&mut self, cols: u16, rows: u16) -> Result<(), String> {
let window_size = WindowSize {
num_lines: rows,
num_cols: cols,
cell_width: 8, // Approximate; actual cell size comes from renderer
cell_height: 16,
};
self.pty.on_resize(window_size);
Ok(())
}
fn kill(&mut self) -> Result<(), String> {
self.alive = false;
// PTY Drop will kill the child process
Ok(())
}
fn is_alive(&self) -> bool {
self.alive
}
}