use std::io;
use std::time::Instant;
#[cfg(windows)]
use std::path::PathBuf;
use std::io::Write;
use crate::types::{AppState, Mode, Action, FocusDir, LayoutKind, MenuItem, Menu, Node};
use crate::tree::{compute_rects, kill_all_children, get_active_pane_id};
use crate::pane::{create_window, split_active, kill_active_pane};
use crate::copy_mode::{enter_copy_mode, scroll_copy_up, switch_with_copy_save, paste_latest,
capture_active_pane, save_latest_buffer};
use crate::session::{send_control_to_port, list_all_sessions_tree};
use crate::window_ops::toggle_zoom;
pub(crate) fn parse_popup_dim_local(spec: &str, term_dim: u16, default: u16) -> u16 {
if let Some(pct_str) = spec.strip_suffix('%') {
if let Ok(pct) = pct_str.parse::<u16>() {
let pct = pct.min(100);
(term_dim as u32 * pct as u32 / 100) as u16
} else {
default
}
} else {
spec.parse().unwrap_or(default)
}
}
pub(crate) const DISPLAY_MESSAGE_DEFAULT_FMT: &str =
"[#{session_name}] #{window_index}:#{window_name}#{window_flags} \"#{pane_title}\" #{pane_index} #{pane_current_command}";
pub fn resolve_run_shell() -> (String, Vec<String>) {
#[cfg(windows)]
{
if let Ok(path) = which::which("pwsh") {
return (path.to_string_lossy().into_owned(), vec!["-NoProfile".to_string(), "-Command".to_string()]);
}
if let Ok(path) = which::which("powershell") {
return (path.to_string_lossy().into_owned(), vec!["-NoProfile".to_string(), "-Command".to_string()]);
}
if let Ok(system_root) = std::env::var("SystemRoot").or_else(|_| std::env::var("SYSTEMROOT")) {
let powershell = PathBuf::from(&system_root)
.join("System32")
.join("WindowsPowerShell")
.join("v1.0")
.join("powershell.exe");
if powershell.is_file() {
return (powershell.to_string_lossy().into_owned(), vec!["-NoProfile".to_string(), "-Command".to_string()]);
}
let cmd = PathBuf::from(&system_root).join("System32").join("cmd.exe");
if cmd.is_file() {
return (cmd.to_string_lossy().into_owned(), vec!["/c".to_string()]);
}
}
if let Ok(comspec) = std::env::var("ComSpec").or_else(|_| std::env::var("COMSPEC")) {
let trimmed = comspec.trim();
if !trimmed.is_empty() {
return (trimmed.to_string(), vec!["/c".to_string()]);
}
}
("cmd".to_string(), vec!["/c".to_string()])
}
#[cfg(not(windows))]
{
("sh".to_string(), vec!["-c".to_string()])
}
}
#[cfg(windows)]
fn resolve_shell_binary(name: &str) -> String {
let lower = name.to_lowercase();
let is_pwsh = lower == "pwsh" || lower == "pwsh.exe";
let is_powershell = lower == "powershell" || lower == "powershell.exe";
if is_pwsh {
if which::which("pwsh").is_ok() {
return name.to_string();
}
if let Ok(p) = which::which("powershell") {
return p.to_string_lossy().into_owned();
}
} else if is_powershell {
if which::which("powershell").is_ok() {
return name.to_string();
}
if let Ok(p) = which::which("pwsh") {
return p.to_string_lossy().into_owned();
}
}
name.to_string()
}
#[cfg(windows)]
fn find_file_in_command(cmd: &str) -> Option<(String, String)> {
let trimmed = cmd.trim();
if trimmed.is_empty() { return None; }
let bytes = trimmed.as_bytes();
let mut end = 0;
loop {
while end < bytes.len() && !bytes[end].is_ascii_whitespace() {
end += 1;
}
let candidate = &trimmed[..end];
if std::path::Path::new(candidate).is_file() {
let rest = trimmed[end..].trim_start().to_string();
return Some((candidate.to_string(), rest));
}
if end >= bytes.len() { return None; }
while end < bytes.len() && bytes[end].is_ascii_whitespace() {
end += 1;
}
if end >= bytes.len() { return None; }
}
}
pub fn build_run_shell_command(shell_cmd: &str) -> std::process::Command {
#[cfg(windows)]
{
use crate::platform::HideWindowCommandExt;
let lower = shell_cmd.trim_start().to_lowercase();
if lower.starts_with("pwsh ") || lower.starts_with("pwsh.exe ")
|| lower.starts_with("powershell ") || lower.starts_with("powershell.exe ")
|| lower.starts_with("cmd ") || lower.starts_with("cmd.exe ")
{
let parts = parse_command_line(shell_cmd);
if parts.len() >= 2 {
let prog = resolve_shell_binary(&parts[0]);
let mut c = std::process::Command::new(&prog);
for p in &parts[1..] { c.arg(p); }
c.hide_window();
return c;
}
}
let trimmed = shell_cmd.trim();
let trimmed = if (trimmed.starts_with('\'') && trimmed.ends_with('\'') && trimmed.len() >= 2)
|| (trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2) {
&trimmed[1..trimmed.len()-1]
} else {
trimmed
};
if let Some((file_path, rest_args)) = find_file_in_command(trimmed) {
let lower_path = file_path.to_lowercase();
if lower_path.ends_with(".ps1") {
let shell = if which::which("pwsh").is_ok() { "pwsh" } else { "powershell" };
let mut c = std::process::Command::new(shell);
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &file_path]);
if !rest_args.is_empty() {
for a in &parse_command_line(&rest_args) { c.arg(a); }
}
c.hide_window();
return c;
}
if file_path.contains(' ') {
let ext = std::path::Path::new(&file_path).extension()
.and_then(|e| e.to_str()).map(|e| e.to_lowercase());
match ext.as_deref() {
Some("exe") | Some("com") => {
let mut c = std::process::Command::new(&file_path);
if !rest_args.is_empty() {
for a in &parse_command_line(&rest_args) { c.arg(a); }
}
c.hide_window();
return c;
}
Some("cmd") | Some("bat") => {
let mut c = std::process::Command::new("cmd.exe");
c.arg("/c");
c.arg(&file_path);
if !rest_args.is_empty() {
for a in &parse_command_line(&rest_args) { c.arg(a); }
}
c.hide_window();
return c;
}
_ => {
let (shell_prog, shell_args) = resolve_run_shell();
let lower_shell = shell_prog.to_lowercase();
let is_powershell = lower_shell.contains("pwsh")
|| lower_shell.contains("powershell");
let mut c = std::process::Command::new(&shell_prog);
for a in &shell_args { c.arg(a); }
if is_powershell {
let escaped = file_path.replace('\'', "''");
let wrapped = if rest_args.is_empty() {
format!("& '{}'", escaped)
} else {
format!("& '{}' {}", escaped, rest_args)
};
c.arg(&wrapped);
} else {
c.arg(&file_path);
if !rest_args.is_empty() {
for a in &parse_command_line(&rest_args) { c.arg(a); }
}
}
c.hide_window();
return c;
}
}
}
}
let (shell_prog, shell_args) = resolve_run_shell();
let mut c = std::process::Command::new(&shell_prog);
for a in &shell_args { c.arg(a); }
c.arg(shell_cmd);
c.hide_window();
c
}
#[cfg(not(windows))]
{
let (shell_prog, shell_args) = resolve_run_shell();
let mut c = std::process::Command::new(&shell_prog);
for a in &shell_args { c.arg(a); }
c.arg(shell_cmd);
c
}
}
fn show_output_popup(app: &mut AppState, title: &str, output: String) {
let lines: Vec<&str> = output.lines().collect();
let width = lines.iter().map(|l| l.len()).max().unwrap_or(40).max(20) as u16 + 4;
let height = (lines.len() as u16 + 2).max(5);
app.mode = Mode::PopupMode {
command: title.to_string(),
output,
process: None,
width: width.min(120),
height,
close_on_exit: false,
popup_pane: None,
scroll_offset: 0,
};
}
fn generate_list_windows(app: &AppState) -> String {
crate::util::list_windows_tmux(app)
}
fn generate_list_panes(app: &AppState) -> String {
let win = &app.windows[app.active_idx];
fn collect(node: &Node, panes: &mut Vec<(usize, u16, u16)>) {
match node {
Node::Leaf(p) => { panes.push((p.id, p.last_cols, p.last_rows)); }
Node::Split { children, .. } => { for c in children { collect(c, panes); } }
}
}
let mut panes = Vec::new();
collect(&win.root, &mut panes);
let active_id = get_active_pane_id(&win.root, &win.active_path);
let mut output = String::new();
for (pos, (id, cols, rows)) in panes.iter().enumerate() {
let idx = pos + app.pane_base_index;
let marker = if active_id == Some(*id) { " (active)" } else { "" };
output.push_str(&format!("{}: [{}x{}] [history {}/{}, 0 bytes] %{}{}\n",
idx, cols, rows, app.history_limit, app.history_limit, id, marker));
}
output
}
fn generate_list_clients(app: &AppState) -> String {
format!("/dev/pts/0: {}: {} [{}x{}] (utf8)\n",
app.session_name,
app.windows[app.active_idx].name,
app.last_window_area.width,
app.last_window_area.height)
}
fn generate_show_hooks(app: &AppState) -> String {
let mut output = String::new();
for (name, commands) in &app.hooks {
if commands.len() == 1 {
output.push_str(&format!("{} -> {}\n", name, commands[0]));
} else {
for (i, cmd) in commands.iter().enumerate() {
output.push_str(&format!("{}[{}] -> {}\n", name, i, cmd));
}
}
}
if output.is_empty() {
output.push_str("(no hooks)\n");
}
output
}
fn generate_show_options(app: &AppState) -> String {
let mut output = String::new();
output.push_str(&format!("prefix {}\n", crate::config::format_key_binding(&app.prefix_key)));
output.push_str(&format!("base-index {}\n", app.window_base_index));
output.push_str(&format!("pane-base-index {}\n", app.pane_base_index));
output.push_str(&format!("escape-time {}\n", app.escape_time_ms));
output.push_str(&format!("mouse {}\n", if app.mouse_enabled { "on" } else { "off" }));
output.push_str(&format!("scroll-enter-copy-mode {}\n", if app.scroll_enter_copy_mode { "on" } else { "off" }));
output.push_str(&format!("choose-tree-preview {}\n", if app.choose_tree_preview { "on" } else { "off" }));
output.push_str(&format!("status {}\n", if app.status_visible { "on" } else { "off" }));
output.push_str(&format!("status-position {}\n", app.status_position));
output.push_str(&format!("status-left \"{}\"\n", app.status_left));
output.push_str(&format!("status-right \"{}\"\n", app.status_right));
output.push_str(&format!("history-limit {}\n", app.history_limit));
output.push_str(&format!("display-time {}\n", app.display_time_ms));
output.push_str(&format!("mode-keys {}\n", app.mode_keys));
output.push_str(&format!("focus-events {}\n", if app.focus_events { "on" } else { "off" }));
output.push_str(&format!("renumber-windows {}\n", if app.renumber_windows { "on" } else { "off" }));
output.push_str(&format!("automatic-rename {}\n", if app.automatic_rename { "on" } else { "off" }));
output.push_str(&format!("monitor-activity {}\n", if app.monitor_activity { "on" } else { "off" }));
output.push_str(&format!("synchronize-panes {}\n", if app.sync_input { "on" } else { "off" }));
output.push_str(&format!("remain-on-exit {}\n", if app.remain_on_exit { "on" } else { "off" }));
output.push_str(&format!("allow-predictions {}\n", if app.allow_predictions { "on" } else { "off" }));
for (key, val) in &app.user_options {
output.push_str(&format!("{} \"{}\"\n", key, val));
}
output
}
fn join_pane_local(app: &mut AppState, src_win: Option<usize>, src_pane: Option<usize>,
target_win: Option<usize>, target_pane: Option<usize>, horizontal: bool) {
let src_idx = src_win.unwrap_or(app.active_idx);
let raw_target_win = target_win.unwrap_or(app.active_idx);
if src_idx < app.windows.len() && raw_target_win < app.windows.len() && src_idx != raw_target_win {
let src_path = if let Some(pidx) = src_pane {
let mut leaves = Vec::new();
crate::tree::collect_leaf_paths_pub(&app.windows[src_idx].root, &mut Vec::new(), &mut leaves);
if let Some((_, p)) = leaves.get(pidx) {
p.clone()
} else {
app.windows[src_idx].active_path.clone()
}
} else {
app.windows[src_idx].active_path.clone()
};
let src_root = std::mem::replace(&mut app.windows[src_idx].root,
Node::Split { kind: LayoutKind::Horizontal, sizes: vec![], children: vec![] });
let (remaining, extracted) = crate::tree::extract_node(src_root, &src_path);
if let Some(pane_node) = extracted {
let src_empty = remaining.is_none();
if let Some(rem) = remaining {
app.windows[src_idx].root = rem;
app.windows[src_idx].active_path = crate::tree::first_leaf_path(&app.windows[src_idx].root);
}
let tgt = if src_empty && raw_target_win > src_idx { raw_target_win - 1 } else { raw_target_win };
if src_empty {
app.windows.remove(src_idx);
if app.active_idx >= app.windows.len() {
app.active_idx = app.windows.len().saturating_sub(1);
}
}
if tgt < app.windows.len() {
let tgt_path = if let Some(tpidx) = target_pane {
let mut leaves = Vec::new();
crate::tree::collect_leaf_paths_pub(&app.windows[tgt].root, &mut Vec::new(), &mut leaves);
if let Some((_, p)) = leaves.get(tpidx) {
p.clone()
} else {
app.windows[tgt].active_path.clone()
}
} else {
app.windows[tgt].active_path.clone()
};
let split_kind = if horizontal { LayoutKind::Horizontal } else { LayoutKind::Vertical };
crate::tree::replace_leaf_with_split(&mut app.windows[tgt].root, &tgt_path, split_kind, pane_node);
app.active_idx = tgt;
}
} else {
if let Some(rem) = remaining {
app.windows[src_idx].root = rem;
}
}
}
}
fn generate_list_commands() -> String {
crate::help::cli_command_lines().join("\n")
}
pub fn build_choose_tree(app: &AppState) -> Vec<crate::session::TreeEntry> {
let current_windows: Vec<(String, usize, String, bool)> = app.windows.iter().enumerate().map(|(i, w)| {
let panes = crate::tree::count_panes(&w.root);
let size = format!("{}x{}", app.last_window_area.width, app.last_window_area.height);
(w.name.clone(), panes, size, i == app.active_idx)
}).collect();
list_all_sessions_tree(&app.session_name, ¤t_windows)
}
fn parse_window_target(target: &str) -> Option<usize> {
let s = target.trim_start_matches(':').trim_start_matches('=');
s.parse::<usize>().ok()
}
pub fn parse_command_to_action(cmd: &str) -> Option<Action> {
let parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.is_empty() { return None; }
match parts[0] {
"display-panes" | "displayp" => Some(Action::DisplayPanes),
"new-window" | "neww" => {
let has_extra = parts.len() > 1;
if has_extra {
Some(Action::Command(cmd.to_string()))
} else {
Some(Action::NewWindow)
}
}
"split-window" | "splitw" => {
let has_extra = parts.iter().any(|p| matches!(*p, "-c" | "-d" | "-p" | "-l" | "-F" | "-P" | "-b" | "-f" | "-I" | "-Z" | "-e"))
|| parts.iter().any(|p| !p.starts_with('-') && *p != "split-window" && *p != "splitw");
if has_extra {
Some(Action::Command(cmd.to_string()))
} else if parts.iter().any(|p| *p == "-h") {
Some(Action::SplitHorizontal)
} else {
Some(Action::SplitVertical)
}
}
"kill-pane" | "killp" => Some(Action::KillPane),
"next-window" | "next" => Some(Action::NextWindow),
"previous-window" | "prev" => Some(Action::PrevWindow),
"copy-mode" => {
if parts.iter().any(|p| *p == "-u") {
Some(Action::Command(cmd.to_string()))
} else {
Some(Action::CopyMode)
}
}
"paste-buffer" | "pasteb" => Some(Action::Paste),
"detach-client" | "detach" => Some(Action::Detach),
"rename-window" | "renamew" => Some(Action::RenameWindow),
"choose-window" | "choose-tree" => Some(Action::WindowChooser),
"choose-session" => Some(Action::SessionChooser),
"resize-pane" | "resizep" if parts.iter().any(|p| *p == "-Z") => Some(Action::ZoomPane),
"zoom-pane" => Some(Action::ZoomPane),
"select-pane" | "selectp" => {
if parts.iter().any(|p| *p == "-Z") {
Some(Action::Command(cmd.to_string()))
} else if parts.iter().any(|p| *p == "-U") {
Some(Action::MoveFocus(FocusDir::Up))
} else if parts.iter().any(|p| *p == "-D") {
Some(Action::MoveFocus(FocusDir::Down))
} else if parts.iter().any(|p| *p == "-L") {
Some(Action::MoveFocus(FocusDir::Left))
} else if parts.iter().any(|p| *p == "-R") {
Some(Action::MoveFocus(FocusDir::Right))
} else {
Some(Action::Command(cmd.to_string()))
}
}
"last-window" | "last" => Some(Action::Command("last-window".to_string())),
"last-pane" | "lastp" => Some(Action::Command("last-pane".to_string())),
"swap-pane" | "swapp" => Some(Action::Command(cmd.to_string())),
"resize-pane" | "resizep" => Some(Action::Command(cmd.to_string())),
"rotate-window" | "rotatew" => Some(Action::Command(cmd.to_string())),
"break-pane" | "breakp" => Some(Action::Command(cmd.to_string())),
"respawn-pane" | "respawnp" => Some(Action::Command(cmd.to_string())),
"respawn-window" | "respawnw" => Some(Action::Command(cmd.to_string())),
"kill-window" | "killw" => Some(Action::Command(cmd.to_string())),
"kill-session" | "kill-ses" => Some(Action::Command(cmd.to_string())),
"kill-server" => Some(Action::Command(cmd.to_string())),
"select-window" | "selectw" => Some(Action::Command(cmd.to_string())),
"toggle-sync" => Some(Action::Command("toggle-sync".to_string())),
"send-keys" | "send" => Some(Action::Command(cmd.to_string())),
"send-prefix" => Some(Action::Command(cmd.to_string())),
"set-option" | "set" | "setw" | "set-window-option" => Some(Action::Command(cmd.to_string())),
"show-options" | "show" | "show-window-options" | "showw" => Some(Action::Command(cmd.to_string())),
"source-file" | "source" => Some(Action::Command(cmd.to_string())),
"select-layout" | "selectl" => Some(Action::Command(cmd.to_string())),
"next-layout" | "nextl" => Some(Action::Command("next-layout".to_string())),
"previous-layout" | "prevl" => Some(Action::Command("previous-layout".to_string())),
"confirm-before" | "confirm" => Some(Action::Command(cmd.to_string())),
"display-menu" | "menu" => Some(Action::Command(cmd.to_string())),
"display-popup" | "popup" => Some(Action::Command(cmd.to_string())),
"display-message" | "display" => Some(Action::Command(cmd.to_string())),
"pipe-pane" | "pipep" => Some(Action::Command(cmd.to_string())),
"rename-session" | "rename" => Some(Action::Command(cmd.to_string())),
"clear-history" | "clearhist" => Some(Action::Command("clear-history".to_string())),
"set-buffer" | "setb" => Some(Action::Command(cmd.to_string())),
"delete-buffer" | "deleteb" => Some(Action::Command("delete-buffer".to_string())),
"list-buffers" | "lsb" => Some(Action::Command(cmd.to_string())),
"show-buffer" | "showb" => Some(Action::Command(cmd.to_string())),
"choose-buffer" | "chooseb" => Some(Action::Command(cmd.to_string())),
"load-buffer" | "loadb" => Some(Action::Command(cmd.to_string())),
"save-buffer" | "saveb" => Some(Action::Command(cmd.to_string())),
"capture-pane" | "capturep" => Some(Action::Command(cmd.to_string())),
"list-windows" | "lsw" => Some(Action::Command(cmd.to_string())),
"list-panes" | "lsp" => Some(Action::Command(cmd.to_string())),
"list-clients" | "lsc" => Some(Action::Command(cmd.to_string())),
"list-commands" | "lscm" => Some(Action::Command(cmd.to_string())),
"list-keys" | "lsk" => Some(Action::Command(cmd.to_string())),
"list-sessions" | "ls" => Some(Action::Command(cmd.to_string())),
"show-hooks" => Some(Action::Command(cmd.to_string())),
"show-messages" | "showmsgs" => Some(Action::Command(cmd.to_string())),
"clock-mode" => Some(Action::Command(cmd.to_string())),
"command-prompt" => Some(Action::Command(cmd.to_string())),
"has-session" | "has" => Some(Action::Command(cmd.to_string())),
"move-window" | "movew" => Some(Action::Command(cmd.to_string())),
"swap-window" | "swapw" => Some(Action::Command(cmd.to_string())),
"link-window" | "linkw" => Some(Action::Command(cmd.to_string())),
"unlink-window" | "unlinkw" => Some(Action::Command(cmd.to_string())),
"find-window" | "findw" => Some(Action::Command(cmd.to_string())),
"move-pane" | "movep" => Some(Action::Command(cmd.to_string())),
"join-pane" | "joinp" => Some(Action::Command(cmd.to_string())),
"resize-window" | "resizew" => Some(Action::Command(cmd.to_string())),
"run-shell" | "run" => Some(Action::Command(cmd.to_string())),
"if-shell" | "if" => Some(Action::Command(cmd.to_string())),
"wait-for" | "wait" => Some(Action::Command(cmd.to_string())),
"set-environment" | "setenv" => Some(Action::Command(cmd.to_string())),
"show-environment" | "showenv" => Some(Action::Command(cmd.to_string())),
"set-hook" => Some(Action::Command(cmd.to_string())),
"bind-key" | "bind" => Some(Action::Command(cmd.to_string())),
"unbind-key" | "unbind" => Some(Action::Command(cmd.to_string())),
"attach-session" | "attach" | "a" | "at" => Some(Action::Command(cmd.to_string())),
"new-session" | "new" => Some(Action::Command(cmd.to_string())),
"server-info" | "info" => Some(Action::Command(cmd.to_string())),
"start-server" | "start" => Some(Action::Command(cmd.to_string())),
"lock-client" | "lockc" => Some(Action::Command(cmd.to_string())),
"lock-server" | "lock" => Some(Action::Command(cmd.to_string())),
"lock-session" | "locks" => Some(Action::Command(cmd.to_string())),
"refresh-client" | "refresh" => Some(Action::Command(cmd.to_string())),
"suspend-client" | "suspendc" => Some(Action::Command(cmd.to_string())),
"switch-client" | "switchc" => {
if let Some(pos) = parts.iter().position(|p| *p == "-T") {
if let Some(table) = parts.get(pos + 1) {
Some(Action::SwitchTable(table.to_string()))
} else {
Some(Action::Command(cmd.to_string()))
}
} else {
Some(Action::Command(cmd.to_string()))
}
}
_ => Some(Action::Command(cmd.to_string()))
}
}
pub fn format_action(action: &Action) -> String {
match action {
Action::DisplayPanes => "display-panes".to_string(),
Action::NewWindow => "new-window".to_string(),
Action::SplitHorizontal => "split-window -h".to_string(),
Action::SplitVertical => "split-window -v".to_string(),
Action::KillPane => "kill-pane".to_string(),
Action::NextWindow => "next-window".to_string(),
Action::PrevWindow => "previous-window".to_string(),
Action::CopyMode => "copy-mode".to_string(),
Action::Paste => "paste-buffer".to_string(),
Action::Detach => "detach-client".to_string(),
Action::RenameWindow => "rename-window".to_string(),
Action::WindowChooser => "choose-window".to_string(),
Action::SessionChooser => "choose-session".to_string(),
Action::ZoomPane => "resize-pane -Z".to_string(),
Action::MoveFocus(dir) => {
let flag = match dir {
FocusDir::Up => "-U",
FocusDir::Down => "-D",
FocusDir::Left => "-L",
FocusDir::Right => "-R",
};
format!("select-pane {}", flag)
}
Action::Command(cmd) => cmd.clone(),
Action::CommandChain(cmds) => cmds.join(" \\; "),
Action::SwitchTable(table) => format!("switch-client -T {}", table),
}
}
pub fn parse_command_line(line: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
let mut in_double_quotes = false;
let mut in_single_quotes = false;
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if in_single_quotes {
if c == '\'' {
in_single_quotes = false;
} else {
current.push(c);
}
} else if c == '\\' && in_double_quotes {
if i + 1 < chars.len() && chars[i + 1] == '"' {
current.push('"');
i += 1; } else if i + 1 < chars.len() && chars[i + 1] == '\\' {
current.push('\\');
i += 1; } else {
current.push(c); }
} else if c == '"' {
in_double_quotes = !in_double_quotes;
} else if c == '\'' && !in_double_quotes {
in_single_quotes = true;
} else if c.is_whitespace() && !in_double_quotes {
if !current.is_empty() {
args.push(current.clone());
current.clear();
}
} else {
current.push(c);
}
i += 1;
}
if !current.is_empty() {
args.push(current);
}
args
}
pub fn parse_menu_definition(def: &str, x: Option<i16>, y: Option<i16>) -> Menu {
let mut menu = Menu {
title: String::new(),
items: Vec::new(),
selected: 0,
x,
y,
};
let parts: Vec<&str> = def.split_whitespace().collect();
if parts.is_empty() {
return menu;
}
let mut i = 0;
while i < parts.len() {
if parts[i] == "-T" {
if let Some(title) = parts.get(i + 1) {
menu.title = title.trim_matches('"').to_string();
i += 2;
continue;
}
}
if let Some(name) = parts.get(i) {
let name = name.trim_matches('"').to_string();
if name.is_empty() || name == "-" {
menu.items.push(MenuItem {
name: String::new(),
key: None,
command: String::new(),
is_separator: true,
});
i += 1;
} else {
let key = parts.get(i + 1).map(|k| k.trim_matches('"').chars().next()).flatten();
let command = parts.get(i + 2).map(|c| c.trim_matches('"').to_string()).unwrap_or_default();
menu.items.push(MenuItem {
name,
key,
command,
is_separator: false,
});
i += 3;
}
} else {
break;
}
}
if menu.items.is_empty() && !def.is_empty() {
menu.title = "Menu".to_string();
menu.items.push(MenuItem {
name: def.to_string(),
key: Some('1'),
command: def.to_string(),
is_separator: false,
});
}
menu
}
pub fn ensure_background(cmd: &str) -> String {
let t = cmd.trim_start();
let prefix = if t.starts_with("run-shell ") {
Some("run-shell")
} else if t.starts_with("run ") {
Some("run")
} else {
None
};
if let Some(p) = prefix {
let rest = t[p.len()..].trim_start();
if !rest.starts_with("-b") {
return format!("{} -b {}", p, rest);
}
}
cmd.to_string()
}
pub fn fire_hooks(app: &mut AppState, event: &str) {
if let Some(commands) = app.hooks.get(event).cloned() {
for cmd in commands {
let bg_cmd = ensure_background(&cmd);
let _ = execute_command_string(app, &bg_cmd);
}
}
}
pub fn execute_action(app: &mut AppState, action: &Action) -> io::Result<bool> {
match action {
Action::DisplayPanes => {
let win = &app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, ratatui::prelude::Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
app.display_map.clear();
for (i, (path, _)) in rects.into_iter().enumerate() {
if i >= 10 { break; }
let digit = (i + app.pane_base_index) % 10;
app.display_map.push((digit, path));
}
app.mode = Mode::PaneChooser { opened_at: Instant::now() };
}
Action::MoveFocus(dir) => {
let d = *dir;
switch_with_copy_save(app, |app| { crate::input::move_focus(app, d); });
}
Action::NewWindow => {
let pty_system = portable_pty::native_pty_system();
create_window(&*pty_system, app, None, None)?;
}
Action::SplitHorizontal => {
split_active(app, LayoutKind::Horizontal)?;
}
Action::SplitVertical => {
split_active(app, LayoutKind::Vertical)?;
}
Action::KillPane => {
kill_active_pane(app)?;
}
Action::NextWindow => {
if !app.windows.is_empty() {
switch_with_copy_save(app, |app| {
app.last_window_idx = app.active_idx;
app.active_idx = (app.active_idx + 1) % app.windows.len();
});
}
}
Action::PrevWindow => {
if !app.windows.is_empty() {
switch_with_copy_save(app, |app| {
app.last_window_idx = app.active_idx;
app.active_idx = (app.active_idx + app.windows.len() - 1) % app.windows.len();
});
}
}
Action::CopyMode => {
enter_copy_mode(app);
}
Action::Paste => {
paste_latest(app)?;
}
Action::Detach => {
return Ok(true);
}
Action::RenameWindow => {
app.mode = Mode::RenamePrompt { input: String::new() };
}
Action::WindowChooser | Action::SessionChooser => {
let tree = build_choose_tree(app);
let selected = tree.iter().position(|e| e.is_current_session && e.is_active_window && !e.is_session_header).unwrap_or(0);
app.mode = Mode::WindowChooser { selected, tree };
}
Action::ZoomPane => {
toggle_zoom(app);
}
Action::Command(cmd) => {
execute_command_string(app, cmd)?;
}
Action::CommandChain(cmds) => {
for cmd in cmds {
execute_command_string(app, cmd)?;
}
}
Action::SwitchTable(table) => {
app.current_key_table = Some(table.clone());
}
}
Ok(false)
}
pub fn execute_command_prompt(app: &mut AppState) -> io::Result<()> {
let cmdline = match &app.mode { Mode::CommandPrompt { input, .. } => input.clone(), _ => String::new() };
app.mode = Mode::Passthrough;
let sub_commands = crate::config::split_chained_commands_pub(&cmdline);
if sub_commands.len() > 1 {
for sub in &sub_commands {
execute_command_string(app, sub)?;
}
return Ok(());
}
let parts: Vec<&str> = cmdline.split_whitespace().collect();
if parts.is_empty() { return Ok(()); }
match parts[0] {
"new-window" | "neww" => {
let pty_system = portable_pty::native_pty_system();
create_window(&*pty_system, app, None, None)?;
}
"split-window" | "splitw" => {
let kind = if parts.iter().any(|p| *p == "-h") { LayoutKind::Horizontal } else { LayoutKind::Vertical };
split_active(app, kind)?;
}
"kill-pane" | "killp" => { kill_active_pane(app)?; }
"capture-pane" | "capturep" => { capture_active_pane(app)?; }
"save-buffer" | "saveb" => { if let Some(file) = parts.get(1) { save_latest_buffer(app, file)?; } }
"list-sessions" | "ls" => { println!("default"); }
"attach-session" | "attach" | "a" | "at" => { }
_ => {
execute_command_string(app, &cmdline)?;
}
}
Ok(())
}
pub fn execute_command_string(app: &mut AppState, cmd: &str) -> io::Result<()> {
let sub_commands = crate::config::split_chained_commands_pub(cmd);
if sub_commands.len() > 1 {
for sub in &sub_commands {
execute_command_string_single(app, sub)?;
}
return Ok(());
}
execute_command_string_single(app, cmd)
}
fn execute_command_string_single(app: &mut AppState, cmd: &str) -> io::Result<()> {
let parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.is_empty() { return Ok(()); }
match parts[0] {
"new-window" | "neww" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "new-window\n", &app.session_key);
}
}
"split-window" | "splitw" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
}
}
"kill-pane" => {
let _ = kill_active_pane(app);
}
"kill-window" | "killw" => {
if app.windows.len() > 1 {
let mut win = app.windows.remove(app.active_idx);
kill_all_children(&mut win.root);
if app.active_idx >= app.windows.len() {
app.active_idx = app.windows.len() - 1;
}
}
}
"next-window" | "next" => {
if !app.windows.is_empty() {
switch_with_copy_save(app, |app| {
app.last_window_idx = app.active_idx;
app.active_idx = (app.active_idx + 1) % app.windows.len();
});
}
}
"previous-window" | "prev" => {
if !app.windows.is_empty() {
switch_with_copy_save(app, |app| {
app.last_window_idx = app.active_idx;
app.active_idx = (app.active_idx + app.windows.len() - 1) % app.windows.len();
});
}
}
"last-window" | "last" => {
if app.last_window_idx < app.windows.len() {
switch_with_copy_save(app, |app| {
let tmp = app.active_idx;
app.active_idx = app.last_window_idx;
app.last_window_idx = tmp;
});
}
}
"select-window" | "selectw" => {
if let Some(t_pos) = parts.iter().position(|p| *p == "-t") {
if let Some(t) = parts.get(t_pos + 1) {
if let Some(idx) = parse_window_target(t) {
if idx >= app.window_base_index {
let internal_idx = idx - app.window_base_index;
if internal_idx < app.windows.len() {
switch_with_copy_save(app, |app| {
app.last_window_idx = app.active_idx;
app.active_idx = internal_idx;
});
}
}
}
}
}
}
"select-pane" | "selectp" => {
let is_last = parts.iter().any(|p| *p == "-l");
if is_last {
switch_with_copy_save(app, |app| {
let win = &mut app.windows[app.active_idx];
if !app.last_pane_path.is_empty() {
let tmp = win.active_path.clone();
win.active_path = app.last_pane_path.clone();
app.last_pane_path = tmp;
}
});
return Ok(());
}
let keep_zoom = parts.iter().any(|p| *p == "-Z");
let dir = if parts.iter().any(|p| *p == "-U") { FocusDir::Up }
else if parts.iter().any(|p| *p == "-D") { FocusDir::Down }
else if parts.iter().any(|p| *p == "-L") { FocusDir::Left }
else if parts.iter().any(|p| *p == "-R") { FocusDir::Right }
else { return Ok(()); };
if keep_zoom {
switch_with_copy_save(app, |app| {
let win = &app.windows[app.active_idx];
app.last_pane_path = win.active_path.clone();
crate::input::move_focus_preserving_zoom(app, dir);
});
} else if app.windows[app.active_idx].zoom_saved.is_some() {
let saved = app.windows[app.active_idx].zoom_saved.take();
if let Some(ref s) = saved {
let win = &mut app.windows[app.active_idx];
for (p, sz) in s.iter() {
if let Some(Node::Split { sizes, .. }) = crate::tree::get_split_mut(&mut win.root, p) { *sizes = sz.clone(); }
}
}
crate::tree::resize_all_panes(app);
let win = &app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, ratatui::layout::Rect)> = Vec::new();
crate::tree::compute_rects(&win.root, app.last_window_area, &mut rects);
let active_idx = rects.iter().position(|(path, _)| *path == win.active_path);
let has_target = if let Some(ai) = active_idx {
let (_, arect) = &rects[ai];
crate::input::find_best_pane_in_direction(&rects, ai, arect, dir, &[], &[])
.is_some()
} else { false };
if has_target {
switch_with_copy_save(app, |app| {
let win = &app.windows[app.active_idx];
app.last_pane_path = win.active_path.clone();
crate::input::move_focus(app, dir);
});
} else {
if let Some(s) = saved {
let win = &mut app.windows[app.active_idx];
for (p, sz) in s.iter() {
if let Some(Node::Split { sizes, .. }) = crate::tree::get_split_mut(&mut win.root, p) { *sizes = sz.clone(); }
}
win.zoom_saved = Some(s);
}
crate::tree::resize_all_panes(app);
}
} else {
switch_with_copy_save(app, |app| {
let win = &app.windows[app.active_idx];
app.last_pane_path = win.active_path.clone();
crate::input::move_focus(app, dir);
});
}
}
"last-pane" | "lastp" => {
switch_with_copy_save(app, |app| {
let win = &mut app.windows[app.active_idx];
if !app.last_pane_path.is_empty() {
let tmp = win.active_path.clone();
win.active_path = app.last_pane_path.clone();
app.last_pane_path = tmp;
}
});
}
"rename-window" | "renamew" => {
if let Some(name) = parts.get(1) {
if app.active_idx < app.windows.len() {
let win = &mut app.windows[app.active_idx];
win.name = name.to_string();
win.manual_rename = true;
}
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("rename-window {}\n", crate::util::quote_arg(name)), &app.session_key);
}
}
}
"list-windows" | "lsw" => {
let output = generate_list_windows(app);
show_output_popup(app, "list-windows", output);
}
"list-panes" | "lsp" => {
let output = generate_list_panes(app);
show_output_popup(app, "list-panes", output);
}
"list-clients" | "lsc" => {
let output = generate_list_clients(app);
show_output_popup(app, "list-clients", output);
}
"list-commands" | "lscm" => {
let output = generate_list_commands();
show_output_popup(app, "list-commands", output);
}
"show-hooks" => {
let output = generate_show_hooks(app);
show_output_popup(app, "show-hooks", output);
}
"zoom-pane" | "zoom" | "resizep -Z" => {
toggle_zoom(app);
}
"copy-mode" => {
enter_copy_mode(app);
if parts.iter().any(|a| *a == "-u") {
let half = app.windows.get(app.active_idx)
.and_then(|w| crate::tree::active_pane(&w.root, &w.active_path))
.map(|p| p.last_rows as usize).unwrap_or(20);
scroll_copy_up(app, half);
}
}
"display-panes" | "displayp" => {
let win = &app.windows[app.active_idx];
let mut rects: Vec<(Vec<usize>, ratatui::layout::Rect)> = Vec::new();
compute_rects(&win.root, app.last_window_area, &mut rects);
app.display_map.clear();
for (i, (path, _)) in rects.into_iter().enumerate() {
if i >= 10 { break; }
let digit = (i + app.pane_base_index) % 10;
app.display_map.push((digit, path));
}
app.mode = Mode::PaneChooser { opened_at: Instant::now() };
}
"confirm-before" | "confirm" => {
let rest = parts[1..].join(" ");
app.mode = Mode::ConfirmMode {
prompt: format!("Run '{}'?", rest),
command: rest,
input: String::new(),
};
}
"display-menu" | "menu" => {
let rest = parts[1..].join(" ");
let menu = parse_menu_definition(&rest, None, None);
if !menu.items.is_empty() {
app.mode = Mode::MenuMode { menu };
}
}
"display-popup" | "popup" => {
let mut width_spec = "80".to_string();
let mut height_spec = "24".to_string();
let mut start_dir: Option<String> = None;
let close_on_exit = parts.iter().any(|p| *p == "-E");
let mut skip_indices = std::collections::HashSet::new();
skip_indices.insert(0); let mut i = 1;
while i < parts.len() {
match parts[i] {
"-w" => { if let Some(v) = parts.get(i + 1) { width_spec = v.to_string(); skip_indices.insert(i); skip_indices.insert(i + 1); i += 1; } }
"-h" => { if let Some(v) = parts.get(i + 1) { height_spec = v.to_string(); skip_indices.insert(i); skip_indices.insert(i + 1); i += 1; } }
"-d" | "-c" => { if let Some(v) = parts.get(i + 1) { start_dir = Some(v.to_string()); skip_indices.insert(i); skip_indices.insert(i + 1); i += 1; } }
"-E" | "-K" => { skip_indices.insert(i); }
_ => {}
}
i += 1;
}
let (term_w, term_h) = crossterm::terminal::size().unwrap_or((120, 40));
let width = parse_popup_dim_local(&width_spec, term_w, 80);
let height = parse_popup_dim_local(&height_spec, term_h, 24);
let rest: String = parts.iter().enumerate()
.filter(|(idx, _)| !skip_indices.contains(idx))
.map(|(_, a)| *a)
.collect::<Vec<&str>>()
.join(" ");
let pane_result = if !rest.is_empty() {
crate::popup::create_popup_pane(
&rest,
start_dir.as_deref(),
height.saturating_sub(2),
width.saturating_sub(2),
app.next_pane_id,
"1", &app.environment,
)
} else { None };
app.mode = Mode::PopupMode {
command: rest,
output: String::new(),
process: None,
width,
height,
close_on_exit,
popup_pane: pane_result,
scroll_offset: 0,
};
}
"resize-pane" | "resizep" => {
if parts.iter().any(|p| *p == "-Z") {
toggle_zoom(app);
} else if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let amount = parts.windows(2).find(|w| w[0] == "-x" || w[0] == "-y")
.and_then(|w| w[1].parse::<i16>().ok());
if parts.iter().any(|p| *p == "-U" || *p == "-D") {
let amt = amount.unwrap_or(1);
let adj = if parts.iter().any(|p| *p == "-U") { -amt } else { amt };
crate::window_ops::resize_pane_vertical(app, adj);
} else if parts.iter().any(|p| *p == "-L" || *p == "-R") {
let amt = amount.unwrap_or(1);
let adj = if parts.iter().any(|p| *p == "-L") { -amt } else { amt };
crate::window_ops::resize_pane_horizontal(app, adj);
}
}
}
"swap-pane" | "swapp" => {
if let Some(port) = app.control_port {
let dir = if parts.iter().any(|p| *p == "-U") { "-U" } else { "-D" };
let _ = send_control_to_port(port, &format!("swap-pane {}\n", dir), &app.session_key);
} else {
let dir = if parts.iter().any(|p| *p == "-U") { FocusDir::Up } else { FocusDir::Down };
crate::window_ops::swap_pane(app, dir);
}
}
"rotate-window" | "rotatew" => {
if let Some(port) = app.control_port {
let flag = if parts.iter().any(|p| *p == "-D") { "-D" } else { "" };
let _ = send_control_to_port(port, &format!("rotate-window {}\n", flag), &app.session_key);
} else {
crate::window_ops::rotate_panes(app, !parts.iter().any(|p| *p == "-D"));
}
}
"break-pane" | "breakp" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "break-pane\n", &app.session_key);
} else {
crate::window_ops::break_pane_to_window(app);
}
}
"respawn-pane" | "respawnp" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let kill = parts.iter().any(|p| *p == "-k");
crate::window_ops::respawn_active_pane(app, None, None, kill)?;
}
}
"toggle-sync" => {
app.sync_input = !app.sync_input;
}
"set-option" | "set" | "set-window-option" | "setw" => {
crate::config::parse_config_line(app, cmd);
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
}
}
"bind-key" | "bind" => {
crate::config::parse_config_line(app, cmd);
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
}
}
"unbind-key" | "unbind" => {
crate::config::parse_config_line(app, cmd);
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
}
}
"source-file" | "source" => {
if let Some(path) = parts.get(1) {
crate::config::source_file(app, path);
}
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
}
}
"send-keys" | "send" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let literal = parts.iter().any(|p| *p == "-l");
let key_parts: Vec<&str> = parts[1..].iter().filter(|p| !p.starts_with('-')).copied().collect();
if !key_parts.is_empty() {
if literal {
let text = key_parts.join(" ");
if let Some(win) = app.windows.get_mut(app.active_idx) {
if let Some(p) = crate::tree::active_pane_mut(&mut win.root, &win.active_path) {
let _ = p.writer.write_all(text.as_bytes());
let _ = p.writer.flush();
}
}
} else {
for key in &key_parts {
let key_upper = key.to_uppercase();
let expanded = match key_upper.as_str() {
"ENTER" => "\r".to_string(),
"TAB" => "\t".to_string(),
"BTAB" | "BACKTAB" => "\x1b[Z".to_string(),
"ESCAPE" | "ESC" => "\x1b".to_string(),
"SPACE" => " ".to_string(),
"BSPACE" | "BACKSPACE" => "\x7f".to_string(),
"UP" => "\x1b[A".to_string(),
"DOWN" => "\x1b[B".to_string(),
"RIGHT" => "\x1b[C".to_string(),
"LEFT" => "\x1b[D".to_string(),
"HOME" => "\x1b[H".to_string(),
"END" => "\x1b[F".to_string(),
"PAGEUP" | "PPAGE" => "\x1b[5~".to_string(),
"PAGEDOWN" | "NPAGE" => "\x1b[6~".to_string(),
"DELETE" | "DC" => "\x1b[3~".to_string(),
"INSERT" | "IC" => "\x1b[2~".to_string(),
"F1" => "\x1bOP".to_string(),
"F2" => "\x1bOQ".to_string(),
"F3" => "\x1bOR".to_string(),
"F4" => "\x1bOS".to_string(),
"F5" => "\x1b[15~".to_string(),
"F6" => "\x1b[17~".to_string(),
"F7" => "\x1b[18~".to_string(),
"F8" => "\x1b[19~".to_string(),
"F9" => "\x1b[20~".to_string(),
"F10" => "\x1b[21~".to_string(),
"F11" => "\x1b[23~".to_string(),
"F12" => "\x1b[24~".to_string(),
s if crate::input::parse_modified_special_key(s).is_some() => {
crate::input::parse_modified_special_key(s).unwrap()
}
s if s.starts_with("C-M-") || s.starts_with("C-m-") => {
if let Some(c) = key.chars().nth(4) {
if let Some(ctrl) = crate::input::ctrl_char_send_keys_byte(c) {
format!("\x1b{}", ctrl as char)
} else {
String::new()
}
} else {
key.to_string()
}
}
s if s.starts_with("C-") => {
if let Some(c) = s.chars().nth(2) {
if let Some(ctrl) = crate::input::ctrl_char_send_keys_byte(c) {
#[cfg(windows)]
if ctrl == 0x03 {
if let Some(win) = app.windows.get_mut(app.active_idx) {
if let Some(p) = crate::tree::active_pane_mut(&mut win.root, &win.active_path) {
if p.child_pid.is_none() {
p.child_pid = crate::platform::mouse_inject::get_child_pid(&*p.child);
}
if let Some(pid) = p.child_pid {
crate::platform::mouse_inject::send_ctrl_c_event(pid, false);
}
}
}
}
String::from(ctrl as char)
} else {
String::new()
}
} else {
key.to_string()
}
}
s if s.starts_with("M-") => {
if let Some(c) = key.chars().nth(2) {
format!("\x1b{}", c)
} else {
key.to_string()
}
}
_ => key.to_string(),
};
if let Some(win) = app.windows.get_mut(app.active_idx) {
if let Some(p) = crate::tree::active_pane_mut(&mut win.root, &win.active_path) {
let _ = p.writer.write_all(expanded.as_bytes());
let _ = p.writer.flush();
}
}
}
}
}
}
}
"detach-client" | "detach" => {
}
"rename-session" => {
if let Some(name) = parts.get(1) {
app.session_name = name.to_string();
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("rename-session {}\n", crate::util::quote_arg(name)), &app.session_key);
}
}
}
"select-layout" | "selectl" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let layout = parts.get(1).unwrap_or(&"tiled");
crate::layout::apply_layout(app, layout);
}
}
"next-layout" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "next-layout\n", &app.session_key);
} else {
crate::layout::cycle_layout(app);
}
}
"pipe-pane" | "pipep" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
}
}
"choose-tree" | "choose-window" | "choose-session" => {
let tree = build_choose_tree(app);
let selected = tree.iter().position(|e| e.is_current_session && e.is_active_window && !e.is_session_header).unwrap_or(0);
app.mode = Mode::WindowChooser { selected, tree };
}
"command-prompt" => {
let initial = parts.windows(2).find(|w| w[0] == "-I").map(|w| w[1].to_string()).unwrap_or_default();
app.command_vi_normal = false;
app.mode = Mode::CommandPrompt { input: initial.clone(), cursor: initial.len() };
}
"paste-buffer" | "pasteb" => {
paste_latest(app)?;
}
"set-buffer" | "setb" => {
let mut i = 1;
let mut buf_name: Option<String> = None;
let mut content: Option<String> = None;
while i < parts.len() {
if parts[i] == "-b" {
if let Some(name) = parts.get(i + 1) {
buf_name = Some(name.to_string());
}
i += 2; } else if parts[i].starts_with('-') {
i += 1; } else {
content = Some(parts[i..].join(" "));
break;
}
}
if let Some(text) = content {
if let Some(name) = buf_name {
app.named_buffers.insert(name, text);
} else {
app.paste_buffers.insert(0, text);
if app.paste_buffers.len() > 10 { app.paste_buffers.pop(); }
}
}
}
"delete-buffer" | "deleteb" => {
let buf_name: Option<String> = parts.windows(2).find(|w| w[0] == "-b").map(|w| w[1].to_string());
if let Some(name) = buf_name {
if let Ok(idx) = name.parse::<usize>() {
if idx < app.paste_buffers.len() { app.paste_buffers.remove(idx); }
} else {
app.named_buffers.remove(&name);
}
} else {
if !app.paste_buffers.is_empty() { app.paste_buffers.remove(0); }
}
}
"list-buffers" | "lsb" => {
let mut output = String::new();
for (i, buf) in app.paste_buffers.iter().enumerate() {
output.push_str(&format!("buffer{}: {} bytes: \"{}\"\n", i,
buf.len(), &buf.chars().take(50).collect::<String>()));
}
let mut names: Vec<&String> = app.named_buffers.keys().collect();
names.sort();
for name in names {
let buf = &app.named_buffers[name];
let preview: String = buf.chars().take(50).collect();
output.push_str(&format!("{}: {} bytes: \"{}\"\n", name, buf.len(), preview));
}
if output.is_empty() { output.push_str("(no buffers)\n"); }
show_output_popup(app, "list-buffers", output);
}
"show-buffer" | "showb" => {
let buf_name: Option<String> = parts.windows(2).find(|w| w[0] == "-b").map(|w| w[1].to_string());
if let Some(name) = buf_name {
if let Ok(idx) = name.parse::<usize>() {
if let Some(buf) = app.paste_buffers.get(idx) {
show_output_popup(app, "show-buffer", buf.clone());
}
} else if let Some(buf) = app.named_buffers.get(&name) {
show_output_popup(app, "show-buffer", buf.clone());
}
} else if let Some(buf) = app.paste_buffers.first() {
show_output_popup(app, "show-buffer", buf.clone());
}
}
"choose-buffer" | "chooseb" => {
app.mode = Mode::BufferChooser { selected: 0 };
}
"clear-history" | "clearhist" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "clear-history\n", &app.session_key);
} else {
let win = &mut app.windows[app.active_idx];
if let Some(p) = crate::tree::active_pane_mut(&mut win.root, &win.active_path) {
if let Ok(mut parser) = p.term.lock() {
*parser = vt100::Parser::new(p.last_rows, p.last_cols, app.history_limit);
}
}
}
}
"kill-session" | "kill-ses" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "kill-session\n", &app.session_key);
}
}
"kill-server" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "kill-server\n", &app.session_key);
}
}
"has-session" | "has" => {
}
"capture-pane" | "capturep" => {
capture_active_pane(app)?;
}
"save-buffer" | "saveb" => {
if let Some(file) = parts.get(1) {
save_latest_buffer(app, file)?;
}
}
"load-buffer" | "loadb" => {
if let Some(path) = parts.get(1) {
if let Ok(data) = std::fs::read_to_string(path) {
app.paste_buffers.insert(0, data);
if app.paste_buffers.len() > 10 { app.paste_buffers.pop(); }
}
}
}
"clock-mode" => {
app.mode = Mode::ClockMode;
}
"list-sessions" | "ls" => {
let output = crate::session::list_session_names().join("\n") + "\n";
show_output_popup(app, "list-sessions", output);
}
"list-keys" | "lsk" => {
let mut output = String::new();
for (table_name, binds) in &app.key_tables {
for bind in binds {
let key_str = crate::config::format_key_binding(&bind.key);
let cmd_str = format_action(&bind.action);
output.push_str(&format!("bind-key -T {} {} {}\n", table_name, key_str, cmd_str));
}
}
if output.is_empty() { output.push_str("(no bindings)\n"); }
show_output_popup(app, "list-keys", output);
}
"show-options" | "show" | "show-window-options" | "showw" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let output = generate_show_options(app);
show_output_popup(app, "show-options", output);
}
}
"display-message" | "display" => {
if let Some(port) = app.control_port {
let effective_cmd = if parts.len() <= 1 {
format!("display-message \"{}\"", DISPLAY_MESSAGE_DEFAULT_FMT)
} else {
cmd.to_string()
};
let _ = send_control_to_port(port, &format!("{}\n", effective_cmd), &app.session_key);
} else {
let mut msg_parts: Vec<&str> = Vec::new();
let mut duration_ms: Option<u64> = None;
let mut idx = 1;
while idx < parts.len() {
match parts[idx] {
"-d" => {
if idx + 1 < parts.len() {
duration_ms = parts[idx + 1].parse::<u64>().ok();
}
idx += 1;
}
"-I" | "-t" => { idx += 1; }
"-p" => {}
other => { msg_parts.push(other); }
}
idx += 1;
}
let raw = msg_parts.join(" ");
let msg = if raw.is_empty() {
DISPLAY_MESSAGE_DEFAULT_FMT.to_string()
} else {
raw.trim_matches('"').trim_matches('\'').to_string()
};
let expanded = crate::format::expand_format(&msg, app);
app.status_message = Some((expanded, Instant::now(), duration_ms));
}
}
"show-messages" | "showmsgs" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
show_output_popup(app, "show-messages", "(no messages)\n".to_string());
}
}
"set-environment" | "setenv" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let has_u = parts.iter().any(|p| *p == "-u");
let non_flag: Vec<&str> = parts[1..].iter().filter(|p| !p.starts_with('-')).copied().collect();
if has_u {
if let Some(key) = non_flag.first() {
app.environment.remove(*key);
std::env::remove_var(key);
}
} else if non_flag.len() >= 2 {
app.environment.insert(non_flag[0].to_string(), non_flag[1].to_string());
std::env::set_var(non_flag[0], non_flag[1]);
} else if non_flag.len() == 1 {
app.environment.insert(non_flag[0].to_string(), String::new());
std::env::set_var(non_flag[0], "");
}
}
}
"show-environment" | "showenv" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let mut output = String::new();
for (key, value) in &app.environment {
output.push_str(&format!("{}={}\n", key, value));
}
if output.is_empty() { output.push_str("(no environment variables)\n"); }
show_output_popup(app, "show-environment", output);
}
}
"set-hook" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let has_unset = parts.iter().any(|p| *p == "-u" || *p == "-gu" || *p == "-ug");
let has_append = parts.iter().any(|p| *p == "-a" || *p == "-ga" || *p == "-ag");
let non_flag: Vec<&str> = parts[1..].iter().filter(|p| !p.starts_with('-')).copied().collect();
if has_unset {
if let Some(name) = non_flag.first() {
app.hooks.remove(*name);
}
} else if non_flag.len() >= 2 {
let hook_name = non_flag[0];
let hook_cmd = if let Some(pos) = cmd.find(hook_name) {
let after_name = pos + hook_name.len();
cmd[after_name..].trim().to_string()
} else {
non_flag[1..].join(" ")
};
if has_append {
app.hooks.entry(hook_name.to_string()).or_default().push(hook_cmd);
} else {
app.hooks.insert(hook_name.to_string(), vec![hook_cmd]);
}
}
}
}
"send-prefix" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "send-prefix\n", &app.session_key);
} else {
let prefix = app.prefix_key;
let encoded: Vec<u8> = match prefix.0 {
crossterm::event::KeyCode::Char(c) if prefix.1.contains(crossterm::event::KeyModifiers::CONTROL) => {
vec![(c.to_ascii_lowercase() as u8) & 0x1F]
}
crossterm::event::KeyCode::Char(c) => format!("{}", c).into_bytes(),
_ => vec![],
};
if !encoded.is_empty() {
if let Some(win) = app.windows.get_mut(app.active_idx) {
if let Some(p) = crate::tree::active_pane_mut(&mut win.root, &win.active_path) {
let _ = p.writer.write_all(&encoded);
let _ = p.writer.flush();
}
}
}
}
}
"if-shell" | "if" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let parsed = parse_command_line(cmd);
let format_mode = parsed.iter().any(|p| p == "-F" || p == "-bF" || p == "-Fb");
let positional: Vec<&str> = parsed[1..].iter()
.filter(|p| !p.starts_with('-'))
.map(|s| s.as_str())
.collect();
if positional.len() >= 2 {
let condition = positional[0];
let true_cmd = positional[1];
let false_cmd = positional.get(2).copied();
let success = if format_mode {
let expanded = crate::format::expand_format(condition, app);
!expanded.is_empty() && expanded != "0"
} else if condition == "true" || condition == "1" {
true
} else if condition == "false" || condition == "0" {
false
} else {
{
let (shell_prog, mut shell_args) = resolve_run_shell();
shell_args.push(condition.to_string());
let mut cmd = std::process::Command::new(&shell_prog);
cmd.args(shell_args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
#[cfg(windows)]
{ use crate::platform::HideWindowCommandExt; cmd.hide_window(); }
cmd.status()
.map(|s| s.success()).unwrap_or(false)
}
};
if let Some(chosen) = if success { Some(true_cmd) } else { false_cmd } {
execute_command_string(app, chosen)?;
}
}
}
}
"wait-for" | "wait" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
}
}
"find-window" | "findw" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let pattern = parts[1..].iter().find(|p| !p.starts_with('-')).unwrap_or(&"");
let mut output = String::new();
for (i, win) in app.windows.iter().enumerate() {
if win.name.contains(pattern) {
output.push_str(&format!("{}: {}\n", i + app.window_base_index, win.name));
}
}
if output.is_empty() { output.push_str(&format!("(no windows matching '{}')\n", pattern)); }
show_output_popup(app, "find-window", output);
}
}
"move-window" | "movew" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let target = parts[1..].iter().find(|a| a.parse::<usize>().is_ok()).and_then(|s| s.parse().ok());
if let Some(t) = target {
let t: usize = t;
if t < app.windows.len() && app.active_idx != t {
let win = app.windows.remove(app.active_idx);
let insert_idx = if t > app.active_idx { t - 1 } else { t };
app.windows.insert(insert_idx.min(app.windows.len()), win);
app.active_idx = insert_idx.min(app.windows.len() - 1);
}
}
}
}
"swap-window" | "swapw" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
if let Some(target) = parts[1..].iter().find(|a| a.parse::<usize>().is_ok()).and_then(|s| s.parse::<usize>().ok()) {
if target < app.windows.len() && app.active_idx != target {
app.windows.swap(app.active_idx, target);
}
}
}
}
"link-window" | "linkw" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let src_idx = parts.windows(2).find(|w| w[0] == "-s")
.and_then(|w| w[1].trim_start_matches(':').parse::<usize>().ok());
let dst_idx = parts.windows(2).find(|w| w[0] == "-t")
.and_then(|w| w[1].trim_start_matches(':').parse::<usize>().ok());
let src = src_idx.unwrap_or(app.active_idx);
if src < app.windows.len() {
let src_id = app.windows[src].id;
let src_name = app.windows[src].name.clone();
let pty_system = portable_pty::native_pty_system();
if let Ok(()) = crate::pane::create_window(&*pty_system, app, None, None) {
let new_idx = app.windows.len() - 1;
app.windows[new_idx].linked_from = Some(src_id);
app.windows[new_idx].name = src_name;
if let Some(dst) = dst_idx {
if dst < new_idx {
let win = app.windows.remove(new_idx);
app.windows.insert(dst, win);
}
}
fire_hooks(app, "window-linked");
}
} else {
app.status_message = Some(("link-window: source window not found".to_string(), Instant::now(), None));
}
}
}
"unlink-window" | "unlinkw" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else if app.windows.len() > 1 {
let mut win = app.windows.remove(app.active_idx);
kill_all_children(&mut win.root);
if app.active_idx >= app.windows.len() {
app.active_idx = app.windows.len() - 1;
}
fire_hooks(app, "window-unlinked");
}
}
"move-pane" | "movep" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let horizontal = parts[1..].iter().any(|a| *a == "-h");
let mut src_win: Option<usize> = None;
let mut src_pane: Option<usize> = None;
let mut tgt_win: Option<usize> = None;
let mut tgt_pane: Option<usize> = None;
let mut pi = 1;
while pi < parts.len() {
match parts[pi] {
"-s" => {
if let Some(sv) = parts.get(pi + 1) {
let pt = crate::cli::parse_target(sv);
src_win = pt.window;
src_pane = pt.pane;
}
pi += 2; continue;
}
"-t" => {
if let Some(tv) = parts.get(pi + 1) {
let pt = crate::cli::parse_target(tv);
tgt_win = pt.window;
tgt_pane = pt.pane;
}
pi += 2; continue;
}
_ => {}
}
pi += 1;
}
if tgt_win.is_none() {
tgt_win = parts[1..].iter()
.filter(|a| !a.starts_with('-'))
.find(|a| a.parse::<usize>().is_ok())
.and_then(|s| s.parse::<usize>().ok());
}
join_pane_local(app, src_win, src_pane, tgt_win, tgt_pane, horizontal);
}
}
"join-pane" | "joinp" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else {
let horizontal = parts[1..].iter().any(|a| *a == "-h");
let mut src_win: Option<usize> = None;
let mut src_pane: Option<usize> = None;
let mut tgt_win: Option<usize> = None;
let mut tgt_pane: Option<usize> = None;
let mut pi = 1;
while pi < parts.len() {
match parts[pi] {
"-s" => {
if let Some(sv) = parts.get(pi + 1) {
let pt = crate::cli::parse_target(sv);
src_win = pt.window;
src_pane = pt.pane;
}
pi += 2; continue;
}
"-t" => {
if let Some(tv) = parts.get(pi + 1) {
let pt = crate::cli::parse_target(tv);
tgt_win = pt.window;
tgt_pane = pt.pane;
}
pi += 2; continue;
}
_ => {}
}
pi += 1;
}
if tgt_win.is_none() {
tgt_win = parts[1..].iter()
.filter(|a| !a.starts_with('-'))
.find(|a| a.parse::<usize>().is_ok())
.and_then(|s| s.parse::<usize>().ok());
}
join_pane_local(app, src_win, src_pane, tgt_win, tgt_pane, horizontal);
}
}
"resize-window" | "resizew" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
}
}
"respawn-window" | "respawnw" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
}
}
"previous-layout" | "prevl" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "previous-layout\n", &app.session_key);
} else {
crate::layout::cycle_layout_reverse(app);
}
}
"attach-session" | "attach" | "a" | "at" => {
}
"start-server" | "start" => {
}
"server-info" | "info" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "server-info\n", &app.session_key);
} else {
let output = format!("psmux {}\nSession: {}\nWindows: {}\nActive: {}\n",
crate::types::VERSION, app.session_name, app.windows.len(), app.active_idx);
show_output_popup(app, "server-info", output);
}
}
"new-session" | "new" => {
let mut session_name: Option<String> = None;
let mut detached = false;
let mut window_name: Option<String> = None;
let mut start_dir: Option<String> = None;
let mut env_vars: Vec<(String, String)> = Vec::new();
let mut initial_command: Option<String> = None;
{
let mut i = 1;
while i < parts.len() {
match parts[i] {
"-s" => { i += 1; if i < parts.len() { session_name = Some(parts[i].trim_matches('"').to_string()); } }
"-n" => { i += 1; if i < parts.len() { window_name = Some(parts[i].trim_matches('"').to_string()); } }
"-c" => { i += 1; if i < parts.len() { start_dir = Some(parts[i].trim_matches('"').to_string()); } }
"-e" => {
i += 1;
match crate::util::parse_new_session_e_value_token(parts.get(i).copied()) {
Ok(p) => env_vars.push(p),
Err(e) => {
app.status_message = Some((format!("psmux: {}", e), Instant::now(), None));
return Ok(());
}
}
}
"-d" => { detached = true; }
"-A" | "-D" | "-E" | "-P" | "-X" => { }
"-F" | "-f" | "-t" | "-x" | "-y" => { i += 1; }
other => {
if !other.starts_with('-') {
initial_command = Some(parts[i..].iter().map(|s| s.trim_matches('"').to_string()).collect::<Vec<_>>().join(" "));
break;
}
}
}
i += 1;
}
}
let ns_prefix = app.socket_name.as_deref();
let name = session_name.unwrap_or_else(|| crate::session::next_session_name(ns_prefix));
let port_file_base = if let Some(ref sn) = app.socket_name {
format!("{}__{}", sn, name)
} else {
name.clone()
};
let home = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap_or_default();
let port_path = format!("{}\\.psmux\\{}.port", home, port_file_base);
if std::path::Path::new(&port_path).exists() {
if let Ok(port_str) = std::fs::read_to_string(&port_path) {
if let Ok(port) = port_str.trim().parse::<u16>() {
let addr = format!("127.0.0.1:{}", port);
if std::net::TcpStream::connect_timeout(
&addr.parse().unwrap(),
std::time::Duration::from_millis(100),
).is_ok() {
app.status_message = Some((format!("session '{}' already exists", name), Instant::now(), None));
return Ok(());
}
}
}
let _ = std::fs::remove_file(&port_path);
}
let warm_disabled = std::env::var("PSMUX_NO_WARM").map(|v| v == "1" || v == "true").unwrap_or(false)
|| crate::config::is_warm_disabled_by_config();
let claimed_warm = if !warm_disabled && initial_command.is_none() && start_dir.is_none() && env_vars.is_empty() {
let warm_base = if let Some(ref sn) = app.socket_name {
format!("{}____warm__", sn)
} else {
"__warm__".to_string()
};
let warm_port_path = format!("{}\\.psmux\\{}.port", home, warm_base);
if std::path::Path::new(&warm_port_path).exists() {
if let Ok(warm_port_str) = std::fs::read_to_string(&warm_port_path) {
if let Ok(warm_port) = warm_port_str.trim().parse::<u16>() {
let warm_addr = format!("127.0.0.1:{}", warm_port);
if std::net::TcpStream::connect_timeout(
&warm_addr.parse().unwrap(),
std::time::Duration::from_millis(100),
).is_ok() {
let warm_key = crate::session::read_session_key(&warm_base).unwrap_or_default();
if !warm_key.is_empty() {
let claim_cmd = format!("claim-session {}\n", crate::util::quote_arg(&name));
match crate::session::send_auth_cmd_response(
&warm_addr, &warm_key,
claim_cmd.as_bytes(),
) {
Ok(resp) if resp.contains("OK") => {
if let Some(ref wn) = window_name {
let new_key = crate::session::read_session_key(&port_file_base).unwrap_or_default();
let _ = crate::session::send_auth_cmd(
&warm_addr, &new_key,
format!("rename-window {}\n", crate::util::quote_arg(wn)).as_bytes(),
);
}
if !env_vars.is_empty() {
let new_key = crate::session::read_session_key(&port_file_base).unwrap_or_default();
for (k, v) in &env_vars {
let _ = crate::session::send_auth_cmd(
&warm_addr, &new_key,
format!("set-environment {} {}\n", crate::util::quote_arg(k), crate::util::quote_arg(v)).as_bytes(),
);
}
}
true
}
_ => false,
}
} else { false }
} else { false }
} else { false }
} else { false }
} else { false }
} else { false };
if !claimed_warm {
let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("psmux"));
let mut server_args: Vec<String> = vec!["server".into(), "-s".into(), name.clone()];
if let Some(ref sn) = app.socket_name {
server_args.push("-L".into());
server_args.push(sn.clone());
}
if let Some(ref dir) = start_dir {
server_args.push("-d".into());
server_args.push(dir.clone());
}
if let Some(ref wn) = window_name {
server_args.push("-n".into());
server_args.push(wn.clone());
}
if let Some(ref cmd) = initial_command {
server_args.push("-c".into());
server_args.push(cmd.clone());
}
let area = app.last_window_area;
if area.width > 1 && area.height > 1 {
server_args.push("-x".into());
server_args.push(area.width.to_string());
server_args.push("-y".into());
server_args.push(area.height.to_string());
}
for (k, v) in &env_vars {
server_args.push("-e".into());
server_args.push(format!("{}={}", k, v));
}
#[cfg(windows)]
{ let _ = crate::platform::spawn_server_hidden(&exe, &server_args); }
#[cfg(not(windows))]
{
let mut cmd_proc = std::process::Command::new(&exe);
for a in &server_args { cmd_proc.arg(a); }
cmd_proc.stdin(std::process::Stdio::null());
cmd_proc.stdout(std::process::Stdio::null());
cmd_proc.stderr(std::process::Stdio::null());
let _ = cmd_proc.spawn();
}
}
for _ in 0..500 {
if std::path::Path::new(&port_path).exists() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
if std::path::Path::new(&port_path).exists() {
if !detached {
if let Some(port) = app.control_port {
let switch_cmd = format!("switch-client -t {}\n", crate::util::quote_arg(&name));
let _ = send_control_to_port(port, &switch_cmd, &app.session_key);
}
}
app.status_message = Some((format!("created session '{}'", name), Instant::now(), None));
} else {
app.status_message = Some((format!("failed to create session '{}'", name), Instant::now(), None));
}
}
"lock-client" | "lockc" | "lock-server" | "lock" | "lock-session" | "locks" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "lock-server\n", &app.session_key);
}
app.status_message = Some(("lock: not available on Windows".to_string(), Instant::now(), None));
}
"refresh-client" | "refresh" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "refresh-client\n", &app.session_key);
}
app.status_message = Some(("client refreshed".to_string(), Instant::now(), None));
}
"suspend-client" | "suspendc" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "suspend-client\n", &app.session_key);
}
app.status_message = Some(("suspend: not available on Windows".to_string(), Instant::now(), None));
}
"choose-client" => {
app.status_message = Some(("choose-client: single-client model (you are the only client)".to_string(), Instant::now(), None));
}
"customize-mode" => {
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, "customize-mode\n", &app.session_key);
} else {
let options = crate::server::option_catalog::build_option_list(app);
app.mode = Mode::CustomizeMode {
options,
selected: 0,
scroll_offset: 0,
editing: false,
edit_buffer: String::new(),
edit_cursor: 0,
filter: String::new(),
};
}
}
"run-shell" | "run" => {
let args = parse_command_line(cmd);
let mut cmd_parts: Vec<&str> = Vec::new();
let mut background = false;
for arg in &args[1..] {
if arg == "-b" { background = true; }
else { cmd_parts.push(arg); }
}
let shell_cmd = cmd_parts.join(" ");
if shell_cmd.is_empty() {
app.status_message = Some((
"usage: run-shell [-b] shell-command".to_string(),
Instant::now(),
None,
));
} else {
let shell_cmd = crate::util::expand_run_shell_path(&shell_cmd);
let target_session = app.port_file_base();
if background {
let mut c = build_run_shell_command(&shell_cmd);
if !target_session.is_empty() {
c.env("PSMUX_TARGET_SESSION", &target_session);
}
let _ = c.spawn();
} else {
if app.run_shell_tx.is_none() {
let (tx, rx) = std::sync::mpsc::channel();
app.run_shell_tx = Some(tx);
app.run_shell_rx = Some(rx);
}
let tx = app.run_shell_tx.as_ref().unwrap().clone();
let shell_cmd = shell_cmd.clone();
let shell_cmd_display = shell_cmd.clone();
let target_session = target_session.clone();
std::thread::spawn(move || {
let mut c = build_run_shell_command(&shell_cmd);
if !target_session.is_empty() {
c.env("PSMUX_TARGET_SESSION", &target_session);
}
c.stdin(std::process::Stdio::null());
match c.output() {
Ok(output) => {
let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
text.push_str(&stderr);
}
let _ = tx.send(("run-shell".to_string(), text));
}
Err(e) => {
let _ = tx.send(("run-shell".to_string(), format!("run-shell: {}", e)));
}
}
});
app.status_message = Some((
format!("running: {}", shell_cmd_display),
Instant::now(),
None,
));
}
}
}
_ => {
let old_shell = app.default_shell.clone();
crate::config::parse_config_line(app, cmd);
if app.default_shell != old_shell {
if let Some(mut wp) = app.warm_pane.take() {
wp.child.kill().ok();
}
}
if let Some(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
}
}
}
Ok(())
}
#[cfg(test)]
#[path = "../tests-rs/test_commands.rs"]
mod tests;
#[cfg(test)]
#[path = "../tests-rs/test_commands_new.rs"]
mod tests_new_commands;
#[cfg(test)]
#[path = "../tests-rs/test_commands_audit.rs"]
mod tests_commands_audit;
#[cfg(test)]
#[path = "../tests-rs/test_parity.rs"]
mod tests_parity;
#[cfg(test)]
#[path = "../tests-rs/test_issue179_bind_key_uppercase.rs"]
mod tests_issue179_bind_key_uppercase;
#[cfg(test)]
#[path = "../tests-rs/test_issue192_command_chaining.rs"]
mod tests_issue192_command_chaining;
#[cfg(test)]
#[path = "../tests-rs/test_issue200_new_session.rs"]
mod tests_issue200_new_session;
#[cfg(test)]
#[path = "../tests-rs/test_run_shell_resolve.rs"]
mod tests_run_shell_resolve;
#[cfg(test)]
#[path = "../tests-rs/test_hide_window.rs"]
mod tests_hide_window;
#[cfg(test)]
#[path = "../tests-rs/test_issue209_tmux_compat.rs"]
mod tests_issue209_tmux_compat;
#[cfg(test)]
#[path = "../tests-rs/test_gastown_scenarios.rs"]
mod tests_gastown_scenarios;
#[cfg(test)]
#[path = "../tests-rs/test_issue210_gastown_fixes.rs"]
mod tests_issue210_gastown_fixes;
#[cfg(test)]
#[path = "../tests-rs/test_issue210_gastown_captures.rs"]
mod tests_issue210_gastown_captures;
#[cfg(test)]
#[path = "../tests-rs/test_issue215_session_persistence.rs"]
mod tests_issue215_session_persistence;
#[cfg(test)]
#[path = "../tests-rs/test_mega_unit_coverage.rs"]
mod tests_mega_unit_coverage;
#[cfg(test)]
#[path = "../tests-rs/test_flag_parity.rs"]
mod tests_flag_parity;
#[cfg(test)]
#[path = "../tests-rs/test_issue227_remain_on_exit_hooks.rs"]
mod tests_issue227_remain_on_exit_hooks;
#[cfg(test)]
#[path = "../tests-rs/test_issue235_display_panes_base_index.rs"]
mod tests_issue235_display_panes_base_index;
#[cfg(test)]
#[path = "../tests-rs/test_issue244_capture_scrollback.rs"]
mod tests_issue244_capture_scrollback;
#[cfg(test)]
#[path = "../tests-rs/test_issue245_mouse_selection.rs"]
mod tests_issue245_mouse_selection;
#[cfg(test)]
#[path = "../tests-rs/test_pr255_active_border.rs"]
mod tests_pr255_active_border;
#[cfg(test)]
#[path = "../tests-rs/test_pr207_compat_bugs.rs"]
mod tests_pr207_compat_bugs;
#[cfg(test)]
#[path = "../tests-rs/test_named_buffers.rs"]
mod tests_named_buffers;