use std::io::{Read, Write};
use std::path::PathBuf;
use anyhow::{Context, Result};
use portable_pty::{Child, CommandBuilder, ExitStatus, MasterPty, PtySize, native_pty_system};
use crate::shell_integration::{ShellKind, write_integration_script};
pub struct ShellPty {
master: Box<dyn MasterPty + Send>,
child: Box<dyn Child + Send + Sync>,
}
pub struct PtyIo {
pub reader: Box<dyn Read + Send>,
pub writer: Box<dyn Write + Send>,
}
impl ShellPty {
pub fn spawn(shell: Option<&str>, cols: u16, rows: u16) -> Result<(Self, PtyIo)> {
let pty_system = native_pty_system();
let pair = pty_system
.openpty(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.context("openpty failed")?;
let shell_path = shell
.map(|s| s.to_string())
.or_else(|| std::env::var("SHELL").ok())
.unwrap_or_else(|| "/bin/sh".to_string());
let kind = ShellKind::detect_from_path(&shell_path);
let integration_script = write_integration_script(kind).ok();
let mut cmd = CommandBuilder::new(&shell_path);
cmd.env("CMDQ_ACTIVE", "1");
match kind {
ShellKind::Zsh => {
cmd.arg("-i");
if let Some(script) = &integration_script
&& let Some(zdotdir) = prepare_zdotdir(script)
{
cmd.env("ZDOTDIR", zdotdir);
}
}
ShellKind::Bash => {
cmd.arg("-i");
if let Some(script) = &integration_script {
cmd.env("BASH_ENV", script);
}
}
ShellKind::Fish | ShellKind::Sh => {
cmd.arg("-i");
}
}
if let Ok(cwd) = std::env::current_dir() {
cmd.cwd(cwd);
}
let child = pair
.slave
.spawn_command(cmd)
.context("failed to spawn shell")?;
drop(pair.slave);
let reader = pair
.master
.try_clone_reader()
.context("failed to clone pty reader")?;
let writer = pair
.master
.take_writer()
.context("failed to take pty writer")?;
Ok((
Self {
master: pair.master,
child,
},
PtyIo { reader, writer },
))
}
pub fn resize(&self, cols: u16, rows: u16) -> Result<()> {
self.master
.resize(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.context("pty resize failed")?;
Ok(())
}
pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
Ok(self.child.try_wait()?)
}
pub fn kill(&mut self) -> Result<()> {
self.child.kill()?;
Ok(())
}
}
fn prepare_zdotdir(script: &std::path::Path) -> Option<PathBuf> {
let data = dirs::data_dir().or_else(dirs::home_dir)?.join("cmdq");
std::fs::create_dir_all(&data).ok()?;
let zdotdir = data.join("zdotdir");
std::fs::create_dir_all(&zdotdir).ok()?;
let real_home = dirs::home_dir()?;
let real_zshrc = real_home.join(".zshrc");
let real_zshenv = real_home.join(".zshenv");
let real_zprofile = real_home.join(".zprofile");
let real_zlogin = real_home.join(".zlogin");
let mk = |dest: &std::path::Path, real: &std::path::Path, also_source_integration: bool| {
let mut content = format!(
"[ -f \"{}\" ] && source \"{}\"\n",
real.display(),
real.display()
);
if also_source_integration {
content.push_str(&format!(
"[ -f \"{}\" ] && source \"{}\"\n",
script.display(),
script.display()
));
}
std::fs::write(dest, content).ok();
};
mk(&zdotdir.join(".zshenv"), &real_zshenv, false);
mk(&zdotdir.join(".zprofile"), &real_zprofile, false);
mk(&zdotdir.join(".zshrc"), &real_zshrc, true);
mk(&zdotdir.join(".zlogin"), &real_zlogin, false);
Some(zdotdir)
}