mod proc;
mod shell_init;
mod spawn;
#[cfg(feature = "x11")]
mod x11;
use anyhow::{Context, Result};
use clap::{Args, Parser, Subcommand};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use crate::proc::Terminal;
#[derive(Debug, Parser)]
#[command(name = "clonetty", version, about)]
#[command(args_conflicts_with_subcommands = true, subcommand_precedence_over_arg = true)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[command(flatten)]
clone: CloneArgs,
}
#[derive(Debug, Subcommand)]
enum Command {
ShellInit {
#[arg(value_enum, default_value_t = shell_init::Shell::Bash)]
shell: shell_init::Shell,
},
}
#[derive(Debug, Args)]
struct CloneArgs {
#[arg(short, long, value_name = "PID")]
pid: Option<u32>,
#[cfg(feature = "x11")]
#[arg(short, long, conflicts_with = "pid")]
focused: bool,
#[arg(short, long)]
base: bool,
#[arg(short, long)]
reuse: bool,
#[arg(short = 'n', long)]
dry_run: bool,
#[arg(trailing_var_arg = true, allow_hyphen_values = true, value_name = "COMMAND")]
command: Vec<String>,
}
fn main() -> Result<()> {
let cli = Cli::parse();
if let Some(Command::ShellInit { shell }) = cli.command {
print!("{}", shell_init::snippet(shell));
return Ok(());
}
let cli = cli.clone;
#[cfg(feature = "x11")]
let target_pid = if cli.focused {
Some(x11::focused_window_pid()?)
} else {
cli.pid
};
#[cfg(not(feature = "x11"))]
let target_pid = cli.pid;
let term = match target_pid {
Some(pid) => Terminal::from_pid(pid)?,
None => Terminal::current()?,
};
if !cli.reuse && !term.found_alacritty {
eprintln!(
"clonetty: warning: no alacritty ancestor found; the base environment \
is best-effort and may still contain nix-shell/subshell variables."
);
}
if cli.reuse && !cli.base && term.levels.len() > 1 {
eprintln!(
"clonetty: warning: --reuse cannot reconstruct nested shells; the new \
window will not peel back on Ctrl-D."
);
}
let plan = spawn::build_plan(&term, &cli.command, cli.base, cli.reuse);
if cli.dry_run {
println!("{}", spawn::describe(&plan.cmd));
println!("# {}", plan.summary.replace('\n', "\n# "));
for (path, content) in &plan.rc_files {
println!("\n# ===== {} =====", path.display());
print!("{content}");
}
return Ok(());
}
if let Some(dir) = &plan.tmp_dir {
fs::create_dir_all(dir)
.with_context(|| format!("creating temp dir {}", dir.display()))?;
fs::set_permissions(dir, fs::Permissions::from_mode(0o700))
.with_context(|| format!("securing temp dir {}", dir.display()))?;
for (path, content) in &plan.rc_files {
fs::write(path, content)
.with_context(|| format!("writing {}", path.display()))?;
fs::set_permissions(path, fs::Permissions::from_mode(0o600))
.with_context(|| format!("securing {}", path.display()))?;
}
}
let mut cmd = plan.cmd;
if cli.reuse {
let status = cmd
.status()
.context("running `alacritty msg create-window` (is alacritty running?)")?;
if !status.success() {
anyhow::bail!("`alacritty msg create-window` failed with {status}");
}
} else {
cmd.spawn()
.context("launching alacritty (is it on your PATH?)")?;
}
Ok(())
}