use crate::cli::Arguments;
use crate::config::{TwmGlobal, TwmLayout};
use crate::layout::{get_commands_from_layout, get_commands_from_layout_name, get_layout_names};
use crate::ui::Tui;
use crate::ui::{Picker, PickerSelection};
use anyhow::{Context, Result, bail};
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Command, Output};
pub struct SessionName {
name: String,
}
impl SessionName {
pub fn new(path: &str, path_components: usize) -> Self {
let mut path_parts: Vec<&str> = path.split('/').rev().take(path_components).collect();
path_parts.reverse();
let raw_name = path_parts.join("/");
Self::from(raw_name.as_str())
}
pub fn as_str(&self) -> &str {
&self.name
}
}
impl From<&str> for SessionName {
fn from(s: &str) -> Self {
let name: String = s
.chars()
.map(|c| match c {
'.' => '_',
_ => c,
})
.collect();
SessionName { name }
}
}
fn run_tmux_command(args: &[&str]) -> Result<Output> {
let output = Command::new("tmux")
.args(args)
.output()
.with_context(|| format!("Failed to run tmux command with args {args:?}"))?;
if !output.status.success() {
bail!(
"tmux command with args {:?} failed because: {}",
args,
String::from_utf8_lossy(&output.stderr)
);
}
Ok(output)
}
pub fn get_tmux_sessions() -> Result<Vec<String>> {
let output = run_tmux_command(&["list-sessions", "-F", "#{session_name}"])?;
let out_str = String::from_utf8_lossy(&output.stdout);
let sessions: Vec<String> = out_str.lines().map(|s| s.to_string()).collect();
Ok(sessions)
}
fn create_tmux_session(name: &SessionName, workspace_type: Option<&str>, path: &str) -> Result<()> {
run_tmux_command(&[
"new-session",
"-ds",
&name.name,
"-t",
&name.name,
"-c",
path,
"-e",
"TWM=1",
"-e",
&format!("TWM_ROOT={}", path),
"-e",
&format!("TWM_TYPE={}", workspace_type.unwrap_or("")),
"-e",
&format!("TWM_NAME={}", name.name),
])
.with_context(|| {
format!(
"Failed to create tmux session with name {} at path {path}",
&name.name
)
})?;
Ok(())
}
fn create_tmux_session_in_group(group_session_name: &str, name: &SessionName) -> Result<()> {
run_tmux_command(&["new-session", "-ds", &name.name, "-t", group_session_name]).with_context(
|| {
format!(
"Failed to create tmux session {} in group {}",
&name.name, group_session_name
)
},
)?;
Ok(())
}
fn attach_to_tmux_session_inside_tmux(session_name: &str) -> Result<()> {
run_tmux_command(&["switch", "-t", session_name]).with_context(|| {
format!("Failed to attach to tmux session with name {session_name} inside tmux")
})?;
Ok(())
}
pub fn attach_to_tmux_session(session_name: &str) -> Result<()> {
if std::env::var("TMUX").is_ok() {
attach_to_tmux_session_inside_tmux(session_name)
} else {
attach_to_tmux_session_outside_tmux(session_name)
}
}
fn attach_to_tmux_session_outside_tmux(session_name: &str) -> Result<()> {
let shell = std::env::var("SHELL").unwrap_or("sh".to_string());
let exec_error = Command::new(shell)
.args(["-c", format!("tmux attach -t {}", session_name).as_str()])
.exec();
anyhow::bail!(
"Failed to attach to tmux session with name {session_name} outside tmux: {exec_error}",
session_name = session_name,
exec_error = exec_error
);
}
fn tmux_has_session(session_name: &SessionName) -> bool {
match run_tmux_command(&["has-session", "-t", &session_name.name]) {
Ok(output) => output.status.success(),
Err(_) => false,
}
}
fn get_twm_root_for_session(session_name: &SessionName) -> Result<String> {
let output = run_tmux_command(&["showenv", "-t", &session_name.name])?;
let out_str = String::from_utf8_lossy(&output.stdout);
let twm_root = out_str
.lines()
.find(|line| line.starts_with("TWM_ROOT="))
.with_context(|| {
format!(
"Failed to find TWM_ROOT variable in tmux session {}",
session_name.name
)
})?
.strip_prefix("TWM_ROOT=")
.with_context(|| {
format!(
"Failed to strip TWM_ROOT= prefix from tmux session {}",
session_name.name
)
})?
.to_string();
Ok(twm_root)
}
fn send_commands_to_session(session_name: &str, commands: &[&str]) -> Result<()> {
for command in commands {
run_tmux_command(&["send-keys", "-t", session_name, command, "C-m"])?;
}
Ok(())
}
fn get_layout_selection(twm_config: &TwmGlobal, tui: &mut Tui) -> Result<String> {
Ok(
match Picker::new(
&get_layout_names(&twm_config.layouts),
"Select a layout: ".into(),
)
.get_selection(tui)?
{
PickerSelection::None => bail!("No layout selected"),
PickerSelection::Selection(s) => s,
PickerSelection::ModifiedSelection(s) => s,
},
)
}
fn get_workspace_commands<'a>(
workspace_type: Option<&str>,
twm_config: &'a TwmGlobal,
cli_layout: Option<&'a str>,
local_config: Option<&'a TwmLayout>,
) -> Result<Option<Vec<&'a str>>> {
if let Some(cli_layout) = cli_layout {
return Ok(Some(get_commands_from_layout_name(
cli_layout,
&twm_config.layouts,
)));
}
if let Some(local) = local_config {
return Ok(Some(get_commands_from_layout(
&local.layout,
&twm_config.layouts,
)));
}
match workspace_type {
Some(t) => {
for workspace_definition in &twm_config.workspace_definitions {
if workspace_definition.name == t {
if let Some(layout_name) = &workspace_definition.default_layout {
return Ok(Some(get_commands_from_layout_name(
layout_name,
&twm_config.layouts,
)));
} else {
return Ok(None);
}
}
}
Ok(None)
}
None => Ok(None),
}
}
fn find_config_file(workspace_path: &Path) -> Result<Option<TwmLayout>> {
let local_config = TwmLayout::load(workspace_path)?;
if let Some(local_config) = local_config {
return Ok(Some(local_config));
}
match workspace_path.parent() {
Some(parent) => find_config_file(parent),
None => Ok(None),
}
}
pub fn session_name_for_path_recursive(
path: &str,
path_components: usize,
) -> Result<Option<SessionName>> {
let name = SessionName::new(path, path_components);
if !tmux_has_session(&name) {
return Ok(None);
}
if let Ok(twm_root) = get_twm_root_for_session(&name)
&& twm_root == path
{
return Ok(Some(name));
}
session_name_for_path_recursive(path, path_components + 1)
}
fn get_session_name_recursive(path: &str, path_components: usize) -> Result<SessionName> {
let name = SessionName::new(path, path_components);
if !tmux_has_session(&name) {
return Ok(name);
}
match get_twm_root_for_session(&name) {
Ok(twm_root) => {
if twm_root == path {
Ok(name)
} else {
let new_name = get_session_name_recursive(path, path_components + 1)?;
Ok(new_name)
}
}
Err(_) => {
let new_name = get_session_name_recursive(path, path_components + 2)?;
Ok(new_name)
}
}
}
fn get_group_session_name(group_session_name: &str) -> Result<SessionName> {
let mut name_iter = 1;
let mut temp_name = format!("{}-{}", group_session_name, name_iter);
let mut name = SessionName::from(temp_name.as_str());
while tmux_has_session(&name) {
name_iter += 1;
temp_name = format!("{}-{}", group_session_name, name_iter);
name = SessionName::from(temp_name.as_str());
}
Ok(name)
}
pub fn open_workspace(
workspace_path: &str,
workspace_type: Option<&str>,
config: &TwmGlobal,
args: &Arguments,
tui: &mut Tui,
) -> Result<()> {
let tmux_name = match &args.name {
Some(name) => SessionName::from(name.as_str()),
None => get_session_name_recursive(workspace_path, config.session_name_path_components)?,
};
if !tmux_has_session(&tmux_name) {
create_tmux_session(&tmux_name, workspace_type, workspace_path)?;
let local_config = find_config_file(Path::new(workspace_path))?;
let cli_layout = if args.command.is_none() && args.layout {
Some(get_layout_selection(config, tui)?)
} else {
None
};
let commands = if let Some(command) = &args.command {
Some(vec![command.as_str()])
} else {
get_workspace_commands(
workspace_type,
config,
cli_layout.as_deref(),
local_config.as_ref(),
)?
};
if let Some(layout_commands) = commands {
send_commands_to_session(&tmux_name.name, &layout_commands)?;
}
}
tui.exit()?;
if args.print_workspace_name {
println!("{}", tmux_name.as_str());
}
if !args.dont_attach {
attach_to_tmux_session(&tmux_name.name)?;
}
Ok(())
}
pub fn open_workspace_in_group(group_session_name: &str, args: &Arguments) -> Result<()> {
let tmux_name = match &args.name {
Some(name) => SessionName::from(name.as_str()),
None => get_group_session_name(group_session_name)?,
};
if args.print_workspace_name {
println!("{}", tmux_name.as_str());
}
create_tmux_session_in_group(group_session_name, &tmux_name)?;
if !args.dont_attach {
attach_to_tmux_session(&tmux_name.name)?;
}
Ok(())
}