use std::env;
use std::path::PathBuf;
use std::process::Command;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use color_eyre::eyre::eyre;
use crate::command::CommandExt;
use crate::config::TmuxConfig;
use crate::config::TmuxSessionMode;
use crate::execution_context::ExecutionContext;
use crate::utils::which;
use rust_i18n::t;
#[cfg(unix)]
use std::os::unix::process::CommandExt as _;
struct Tmux {
tmux: PathBuf,
args: Option<Vec<String>>,
}
impl Tmux {
fn new(args: Vec<String>) -> Self {
Self {
tmux: which("tmux").expect("Could not find tmux"),
args: if args.is_empty() { None } else { Some(args) },
}
}
#[allow(clippy::disallowed_methods)]
fn build(&self) -> Command {
let mut command = Command::new(&self.tmux);
if let Some(args) = self.args.as_ref() {
command.args(args).env_remove("TMUX");
}
command
}
fn has_session(&self, session_name: &str) -> Result<bool> {
Ok(self
.build()
.args(["has-session", "-t", session_name])
.output_checked_with(|_| Ok(()))?
.status
.success())
}
fn new_session(&self, session_name: &str, window_name: &str, command: &str) -> Result<()> {
let _ = self
.build()
.args(["new-session", "-d", "-s", session_name, "-n", window_name, command])
.output_checked()?;
Ok(())
}
fn new_unique_session(&self, session_name: &str, window_name: &str, command: &str) -> Result<String> {
let mut session = session_name.to_owned();
for i in 1.. {
if !self
.has_session(&session)
.context("Error determining if a tmux session exists")?
{
self.new_session(&session, window_name, command)
.context("Error running Topgrade in tmux")?;
return Ok(session);
}
session = format!("{session_name}-{i}");
}
unreachable!()
}
fn new_window(&self, session_name: &str, window_name: &str, command: &str) -> Result<()> {
self.build()
.args([
"new-window",
"-a",
"-t",
&format!("{session_name}:{window_name}"),
"-n",
window_name,
command,
])
.env_remove("TMUX")
.status_checked()
}
fn window_indices(&self, session_name: &str) -> Result<Vec<usize>> {
self.build()
.args(["list-windows", "-F", "#{window_index}", "-t", session_name])
.output_checked_utf8()?
.stdout
.lines()
.map(str::parse)
.collect::<Result<Vec<usize>, _>>()
.context("Failed to compute tmux windows")
}
}
pub fn run_in_tmux(config: TmuxConfig) -> Result<()> {
let command = {
let mut command = vec![
String::from("env"),
String::from("TOPGRADE_KEEP_END=1"),
String::from("TOPGRADE_INSIDE_TMUX=1"),
];
command.extend(env::args());
shell_words::join(command)
};
let tmux = Tmux::new(config.args);
let session_name = "topgrade";
let window_name = "topgrade";
let session = tmux.new_unique_session(session_name, window_name, &command)?;
let is_inside_tmux = env::var("TMUX").is_ok();
let err = match config.session_mode {
TmuxSessionMode::AttachIfNotInSession => {
if is_inside_tmux {
println!("{}", t!("Topgrade launched in a new tmux session"));
return Ok(());
} else {
tmux.build().args(["attach-session", "-t", &session]).exec()
}
}
TmuxSessionMode::AttachAlways => {
if is_inside_tmux {
tmux.build().args(["switch-client", "-t", &session]).exec()
} else {
tmux.build().args(["attach-session", "-t", &session]).exec()
}
}
};
Err(eyre!("{err}")).context("Failed to `execvp(3)` tmux")
}
pub fn run_command(ctx: &ExecutionContext, window_name: &str, command: &str) -> Result<()> {
let tmux = Tmux::new(ctx.config().tmux_config()?.args);
if let Some(session_name) = ctx.get_tmux_session() {
let indices = tmux.window_indices(&session_name)?;
let last_window = indices
.iter()
.last()
.ok_or_else(|| eyre!("tmux session {session_name} has no windows"))?;
tmux.new_window(&session_name, &format!("{last_window}"), command)?;
} else {
let name = tmux.new_unique_session("topgrade", window_name, command)?;
ctx.set_tmux_session(name);
}
Ok(())
}