use std::path::Path;
use std::time::Duration;
use expectrl::Regex;
use expectrl::session::Session;
pub const SENTINEL_PROMPT: &str = "__RUNEX_PROMPT__> ";
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy)]
pub enum PtyShell {
Bash,
Zsh,
Pwsh,
Nu,
}
pub struct PtySession {
inner: Session,
}
impl PtySession {
pub fn spawn(shell: PtyShell, runex_bin: &str, config: &Path) -> Option<Self> {
let launch = launch_command(shell, runex_bin);
let mut session = expectrl::spawn(&launch).ok()?;
session.set_expect_timeout(Some(DEFAULT_TIMEOUT));
bootstrap(&mut session, shell, runex_bin, config)?;
Some(Self { inner: session })
}
pub fn send_line(&mut self, s: &str) -> Option<()> {
self.inner.send_line(s).ok()
}
pub fn send(&mut self, s: &str) -> Option<()> {
self.inner.send(s).ok()
}
pub fn expect_regex(&mut self, pattern: &str) -> Option<()> {
self.inner.expect(Regex(pattern)).ok().map(|_| ())
}
pub fn expect_prompt(&mut self) -> Option<()> {
self.expect_regex(SENTINEL_PROMPT)
}
pub fn quit(mut self) {
let _ = self.inner.send_line("exit");
}
}
fn launch_command(shell: PtyShell, _runex_bin: &str) -> String {
match shell {
PtyShell::Bash => "bash --norc --noprofile -i".to_string(),
PtyShell::Zsh => "zsh -f -i".to_string(),
PtyShell::Pwsh => "pwsh -NoLogo -NoProfile".to_string(),
PtyShell::Nu => "nu --no-config-file".to_string(),
}
}
fn bootstrap(
session: &mut Session,
shell: PtyShell,
runex_bin: &str,
config: &Path,
) -> Option<()> {
let cfg = config.display();
match shell {
PtyShell::Bash => {
session
.send_line("bind 'set enable-bracketed-paste off' 2>/dev/null")
.ok()?;
session.send_line(&format!("PS1='{SENTINEL_PROMPT}'")).ok()?;
session.send_line(&format!("export RUNEX_CONFIG={cfg}")).ok()?;
session
.send_line(&format!(
r#"eval "$('{runex_bin}' export bash --bin '{runex_bin}')""#
))
.ok()?;
}
PtyShell::Zsh => {
session.send_line(&format!("PROMPT='{SENTINEL_PROMPT}'")).ok()?;
session.send_line(&format!("export RUNEX_CONFIG={cfg}")).ok()?;
session
.send_line(&format!(
r#"eval "$('{runex_bin}' export zsh --bin '{runex_bin}')""#
))
.ok()?;
}
PtyShell::Pwsh => {
session
.send_line(&format!(
"function prompt {{ '{SENTINEL_PROMPT}' }}"
))
.ok()?;
session
.send_line(&format!("$env:RUNEX_CONFIG = '{cfg}'"))
.ok()?;
session
.send_line(&format!(
"Invoke-Expression (& '{runex_bin}' export pwsh --bin '{runex_bin}' | Out-String)"
))
.ok()?;
}
PtyShell::Nu => {
let nu_path = std::env::temp_dir()
.join(format!("runex-pty-{}.nu", std::process::id()));
let out = std::process::Command::new(runex_bin)
.args(["--config"])
.arg(config)
.args(["export", "nu", "--bin", runex_bin])
.output()
.ok()?;
if !out.status.success() {
return None;
}
std::fs::write(&nu_path, &out.stdout).ok()?;
session
.send_line(&format!(
"$env.PROMPT_COMMAND = '{SENTINEL_PROMPT}'; $env.PROMPT_COMMAND_RIGHT = ''; $env.PROMPT_INDICATOR = ''; $env.PROMPT_INDICATOR_VI_INSERT = ''; $env.PROMPT_INDICATOR_VI_NORMAL = ''; $env.PROMPT_MULTILINE_INDICATOR = ''"
))
.ok()?;
session
.send_line(&format!("$env.RUNEX_CONFIG = '{cfg}'"))
.ok()?;
session
.send_line(&format!("source '{}'", nu_path.display()))
.ok()?;
}
}
let pat = format!(r"{SENTINEL_PROMPT}.*{SENTINEL_PROMPT}");
session.expect(Regex(&pat)).ok()?;
Some(())
}