use std::ffi::OsString;
use std::fmt;
use std::path::PathBuf;
use std::sync::Arc;
use portable_pty::CommandBuilder;
use tastty_core::{
CellPixelSize, ClipboardTarget, HostProfile, HostQuery, KeyEvent, KeyScreenState, ReplyAction,
TerminalSize,
};
use crate::error::{Error, Result};
use crate::osc_policy::{ClipboardPolicy, OscPolicy};
use crate::session::{KeyAction, SessionOptions};
#[derive(Debug)]
enum CommandSpec {
Command {
program: OsString,
args: Vec<OsString>,
},
Shell(String),
}
pub struct Builder {
command: Option<CommandSpec>,
cwd: Option<PathBuf>,
env: Vec<(OsString, OsString)>,
clear_env: bool,
shell: OsString,
controlling_tty: bool,
opts: SessionOptions,
}
impl Default for Builder {
fn default() -> Self {
Self {
command: None,
cwd: None,
env: Vec::new(),
clear_env: false,
shell: OsString::from("/bin/sh"),
controlling_tty: true,
opts: SessionOptions::default(),
}
}
}
impl fmt::Debug for Builder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Builder")
.field("command", &self.command)
.field("cwd", &self.cwd)
.field("env", &self.env)
.field("clear_env", &self.clear_env)
.field("shell", &self.shell)
.field("controlling_tty", &self.controlling_tty)
.field("opts", &self.opts)
.finish()
}
}
impl Builder {
#[must_use]
pub fn command(program: impl Into<OsString>) -> Self {
Self::default().program(program)
}
#[must_use]
pub fn shell_command(command: impl Into<String>) -> Self {
Self {
command: Some(CommandSpec::Shell(command.into())),
..Self::default()
}
}
#[must_use]
pub fn program(mut self, program: impl Into<OsString>) -> Self {
self.command = Some(CommandSpec::Command {
program: program.into(),
args: Vec::new(),
});
self
}
#[must_use]
pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
match self.command {
Some(CommandSpec::Command { ref mut args, .. }) => args.push(arg.into()),
Some(CommandSpec::Shell(_)) | None => {
self.command = Some(CommandSpec::Command {
program: arg.into(),
args: Vec::new(),
});
}
}
self
}
#[must_use]
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
for arg in args {
self = self.arg(arg);
}
self
}
#[must_use]
pub fn cwd(mut self, cwd: impl Into<Option<PathBuf>>) -> Self {
self.cwd = cwd.into();
self
}
#[must_use]
pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
self.env.push((key.into(), value.into()));
self
}
#[must_use]
pub fn envs<I, K, V>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<OsString>,
V: Into<OsString>,
{
for (key, value) in vars {
self.env.push((key.into(), value.into()));
}
self
}
#[must_use]
pub fn clear_env(mut self) -> Self {
self.clear_env = true;
self
}
#[must_use]
pub fn shell(mut self, program: impl Into<OsString>) -> Self {
self.shell = program.into();
self
}
#[must_use]
pub fn controlling_tty(mut self, enabled: bool) -> Self {
self.controlling_tty = enabled;
self
}
#[must_use]
pub fn size(mut self, size: TerminalSize) -> Self {
self.opts.rows = size.rows;
self.opts.cols = size.cols;
self
}
#[must_use]
pub fn scrollback(mut self, rows: u32) -> Self {
self.opts.scrollback = rows;
self
}
#[must_use]
pub fn pixel_cell_size(mut self, size: CellPixelSize) -> Self {
self.opts.pixel_cell_size = size;
self
}
#[must_use]
pub fn host_profile(mut self, profile: HostProfile) -> Self {
self.opts.host_profile = Some(profile);
self
}
#[must_use]
pub fn on_host_query<F>(mut self, f: F) -> Self
where
F: Fn(&HostQuery, &HostProfile) -> ReplyAction + Send + Sync + 'static,
{
self.opts.host_query_callback = Arc::new(f);
self
}
#[must_use]
pub fn echo(mut self, enabled: bool) -> Self {
self.opts.echo = enabled;
self
}
#[must_use]
pub fn virtual_cols(mut self, cols: u16) -> Self {
self.opts.virtual_cols = Some(cols);
self
}
#[must_use]
pub fn on_redraw<F>(mut self, f: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.opts.redraw_callback = Some(Arc::new(f));
self
}
#[must_use]
pub fn on_output<F>(mut self, f: F) -> Self
where
F: Fn(&[u8]) + Send + Sync + 'static,
{
self.opts.output_callback = Some(Arc::new(f));
self
}
#[must_use]
pub fn on_input<F>(mut self, f: F) -> Self
where
F: Fn(&[u8]) + Send + Sync + 'static,
{
self.opts.input_callback = Some(Arc::new(f));
self
}
#[must_use]
pub fn on_key<F>(mut self, f: F) -> Self
where
F: Fn(&KeyEvent, KeyScreenState) -> KeyAction + Send + Sync + 'static,
{
self.opts.key_callback = Some(Arc::new(f));
self
}
#[must_use]
pub fn clipboard_policy(mut self, policy: ClipboardPolicy) -> Self {
self.opts.clipboard_policy = policy;
self
}
#[must_use]
pub fn with_clipboard_read(mut self, target: ClipboardTarget, policy: OscPolicy) -> Self {
self.opts.clipboard_policy.read.set(target, policy);
self
}
#[must_use]
pub fn with_clipboard_write(mut self, target: ClipboardTarget, policy: OscPolicy) -> Self {
self.opts.clipboard_policy.write.set(target, policy);
self
}
pub(crate) fn into_parts(self) -> Result<(CommandBuilder, SessionOptions)> {
let mut cmd = match self.command {
Some(CommandSpec::Command { program, args }) => {
let mut c = CommandBuilder::new(program);
for arg in args {
c.arg(arg);
}
c
}
Some(CommandSpec::Shell(s)) => {
let mut c = CommandBuilder::new(self.shell);
c.arg("-c");
c.arg(s);
c
}
None => return Err(Error::MissingCommand),
};
if self.clear_env {
cmd.env_clear();
}
if let Some(cwd) = self.cwd.or_else(|| std::env::current_dir().ok()) {
cmd.cwd(cwd);
}
for (key, value) in self.env {
cmd.env(key, value);
}
cmd.set_controlling_tty(self.controlling_tty);
Ok((cmd, self.opts))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn debug_includes_cwd(builder: &Builder, want: &Path) -> bool {
format!("{builder:?}").contains(&want.display().to_string())
}
#[test]
fn cwd_accepts_bare_path_buf() {
let path = PathBuf::from("/tmp/example");
let b = Builder::command("/bin/true").cwd(path.clone());
assert!(debug_includes_cwd(&b, &path));
}
#[test]
fn into_parts_defaults_cwd_to_current_dir_when_unset() {
let (cmd, _) = Builder::shell_command("true")
.into_parts()
.expect("builder has a command");
let current = std::env::current_dir().expect("a current dir");
assert_eq!(cmd.get_cwd().map(PathBuf::from), Some(current));
}
#[test]
fn cwd_accepts_some_path_buf() {
let path = PathBuf::from("/tmp/example");
let b = Builder::command("/bin/true").cwd(Some(path.clone()));
assert!(debug_includes_cwd(&b, &path));
}
#[test]
fn cwd_with_none_clears_a_previously_set_cwd() {
let path = PathBuf::from("/tmp/example");
let b = Builder::command("/bin/true").cwd(path).cwd(None::<PathBuf>);
assert!(!format!("{b:?}").contains("/tmp/example"));
}
#[test]
fn cwd_with_none_is_noop_on_unset_builder() {
let b = Builder::command("/bin/true").cwd(None::<PathBuf>);
assert!(!format!("{b:?}").contains("/tmp/example"));
}
#[test]
fn cwd_threads_option_path_buf_through_unchanged() {
let configured: Option<PathBuf> = Some(PathBuf::from("/tmp/example"));
let unset: Option<PathBuf> = None;
let b1 = Builder::command("/bin/true").cwd(configured.clone());
let b2 = Builder::command("/bin/true").cwd(unset);
assert!(debug_includes_cwd(&b1, configured.as_deref().unwrap()));
assert!(!format!("{b2:?}").contains("/tmp/example"));
}
}