use anyhow::{Context, Result, anyhow};
use regex::Regex;
use crate::git;
use crate::multiplexer::MuxHandle;
use crate::multiplexer::util::prefixed;
use crate::prompt::Prompt;
use tracing::info;
use super::context::WorkflowContext;
use super::setup;
use super::types::{CreateResult, SetupOptions};
use crate::config::MuxMode;
pub fn open(
name: &str,
context: &WorkflowContext,
options: SetupOptions,
new_window: bool,
session_override: bool,
prompt_file_only: Option<&Prompt>,
) -> Result<CreateResult> {
info!(
name = name,
run_hooks = options.run_hooks,
run_file_ops = options.run_file_ops,
new_window = new_window,
session_override = session_override,
"open:start"
);
if context.config.panes.is_some() && context.config.windows.is_some() {
anyhow::bail!("Cannot specify both 'panes' and 'windows' in configuration.");
}
if let Some(panes) = &context.config.panes {
crate::config::validate_panes_config(panes)?;
}
context.ensure_mux_running()?;
let (worktree_path, branch_name) = git::find_worktree(name).map_err(|_| {
anyhow!(
"Worktree '{}' not found. Use 'workmux list' to see available worktrees.",
name
)
})?;
let base_handle = worktree_path
.file_name()
.ok_or_else(|| anyhow!("Invalid worktree path: no directory name"))?
.to_string_lossy()
.to_string();
let stored_mode = git::get_worktree_mode_opt(&base_handle);
let mode = if session_override {
MuxMode::Session
} else if let Some(m) = stored_mode {
m
} else {
options.mode
};
if let Some(windows) = &context.config.windows {
if mode != MuxMode::Session {
anyhow::bail!(
"'windows' configuration requires 'mode: session'. \
Add 'mode: session' to your config."
);
}
crate::config::validate_windows_config(windows)?;
}
if mode == MuxMode::Session && stored_mode != Some(MuxMode::Session) {
let prior_mode = stored_mode.unwrap_or(MuxMode::Window);
let all_names = context.mux.get_all_window_names()?;
let full_base = prefixed(&context.prefix, &base_handle);
let full_base_dash = format!("{}-", full_base);
for name in &all_names {
let is_exact = *name == full_base;
let is_numeric_suffix = name
.strip_prefix(&full_base_dash)
.is_some_and(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()));
if is_exact || is_numeric_suffix {
info!(
handle = base_handle,
window = name,
"open:closing window before mode conversion"
);
MuxHandle::kill_full(context.mux.as_ref(), prior_mode, name)?;
}
}
}
let options = SetupOptions { mode, ..options };
let target = MuxHandle::new(context.mux.as_ref(), mode, &context.prefix, &base_handle);
let target_exists = target.exists()?;
if target_exists && !new_window {
if stored_mode != Some(mode) {
let mode_str = if mode == MuxMode::Session {
"session"
} else {
"window"
};
let _ = git::set_worktree_meta(&base_handle, "mode", mode_str);
}
if options.focus_window {
target.select()?;
}
info!(
handle = base_handle,
branch = branch_name,
path = %worktree_path.display(),
kind = target.kind(),
focus = options.focus_window,
"open:switched to existing target"
);
return Ok(CreateResult {
worktree_path,
branch_name,
post_create_hooks_run: 0,
base_branch: None,
did_switch: true,
resolved_handle: base_handle,
mode,
});
}
if new_window && target.is_session() {
return Err(anyhow!(
"--new is not supported in session mode. Each worktree can only have one session."
));
}
if stored_mode != Some(mode) {
let mode_str = if mode == MuxMode::Session {
"session"
} else {
"window"
};
git::set_worktree_meta(&base_handle, "mode", mode_str)
.context("Failed to persist worktree mode")?;
info!(
handle = base_handle,
mode = mode_str,
"open:persisted worktree mode"
);
}
let (handle, after_window) = if new_window && target_exists {
let unique_handle = resolve_unique_handle(context, &base_handle)?;
let after = context
.mux
.find_last_window_with_base_handle(&context.prefix, &base_handle)
.unwrap_or(None);
(unique_handle, after)
} else {
(base_handle, None)
};
let working_dir = if !context.config_rel_dir.as_os_str().is_empty() {
let subdir_in_worktree = worktree_path.join(&context.config_rel_dir);
if subdir_in_worktree.exists() {
Some(subdir_in_worktree)
} else {
None
}
} else {
None
};
let config_root = if !context.config_rel_dir.as_os_str().is_empty() {
Some(context.config_source_dir.clone())
} else {
None
};
if let Some(prompt) = prompt_file_only {
setup::write_prompt_file(Some(&worktree_path), &branch_name, prompt)?;
}
let options_with_workdir = SetupOptions {
working_dir,
config_root,
..options
};
let result = setup::setup_environment(
context.mux.as_ref(),
&branch_name,
&handle,
&worktree_path,
&context.config,
&options_with_workdir,
None,
after_window,
)?;
info!(
handle = handle,
branch = branch_name,
path = %result.worktree_path.display(),
hooks_run = result.post_create_hooks_run,
"open:completed"
);
Ok(result)
}
fn resolve_unique_handle(context: &WorkflowContext, base_handle: &str) -> Result<String> {
let all_names = context.mux.get_all_window_names()?;
let prefix = &context.prefix;
let full_base = prefixed(prefix, base_handle);
if !all_names.contains(&full_base) {
return Ok(base_handle.to_string());
}
let escaped_base = regex::escape(&full_base);
let pattern = format!(r"^{}-(\d+)$", escaped_base);
let re = Regex::new(&pattern).expect("Invalid regex pattern");
let mut max_suffix: u32 = 1;
for name in &all_names {
if let Some(caps) = re.captures(name)
&& let Some(num_match) = caps.get(1)
&& let Ok(num) = num_match.as_str().parse::<u32>()
{
max_suffix = max_suffix.max(num);
}
}
let new_handle = format!("{}-{}", base_handle, max_suffix + 1);
info!(
base_handle = base_handle,
new_handle = new_handle,
"open:generated unique handle for duplicate"
);
Ok(new_handle)
}