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, 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()])
}
}
pub fn build_run_shell_command(shell_cmd: &str) -> std::process::Command {
#[cfg(windows)]
{
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 mut c = std::process::Command::new(&parts[0]);
for p in &parts[1..] { c.arg(p); }
return c;
}
}
let trimmed = shell_cmd.trim();
let first_token = trimmed.split_whitespace().next().unwrap_or("");
let first_unquoted = first_token.trim_matches('"').trim_matches('\'');
if first_unquoted.ends_with(".ps1") && std::path::Path::new(first_unquoted).exists() {
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"]);
let parts = parse_command_line(trimmed);
for p in &parts { c.arg(p); }
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
}
#[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!("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, target_win: usize) {
let src_idx = app.active_idx;
if target_win < app.windows.len() && target_win != src_idx {
let src_path = 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 && target_win > src_idx { target_win - 1 } else { 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 = app.windows[tgt].active_path.clone();
crate::tree::replace_leaf_with_split(&mut app.windows[tgt].root, &tgt_path, LayoutKind::Vertical, 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" => 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" | "choose-session" => Some(Action::WindowChooser),
"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 == "-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::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 fire_hooks(app: &mut AppState, event: &str) {
if let Some(commands) = app.hooks.get(event).cloned() {
for cmd in commands {
let _ = execute_command_string(app, &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 => {
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 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 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 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 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) {
let win = &mut app.windows[app.active_idx];
win.name = name.to_string();
}
}
"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);
}
"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, "respawn-pane\n", &app.session_key);
} else {
crate::window_ops::respawn_active_pane(app, None)?;
}
}
"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(port) = app.control_port {
let _ = send_control_to_port(port, &format!("{}\n", cmd), &app.session_key);
} else if let Some(path) = parts.get(1) {
crate::config::source_file(app, path);
}
}
"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();
let text = key_parts.join(" ");
if !text.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) {
if literal {
let _ = p.writer.write_all(text.as_bytes());
let _ = p.writer.flush();
} else {
let expanded = match text.to_uppercase().as_str() {
"ENTER" => "\r".to_string(),
"SPACE" => " ".to_string(),
"ESCAPE" | "ESC" => "\x1b".to_string(),
"TAB" => "\t".to_string(),
"BSPACE" | "BACKSPACE" => "\x7f".to_string(),
_ => text,
};
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();
}
}
"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" => {
if let Some(text) = parts.get(1) {
app.paste_buffers.insert(0, text.to_string());
if app.paste_buffers.len() > 10 { app.paste_buffers.pop(); }
}
}
"delete-buffer" | "deleteb" => {
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>()));
}
if output.is_empty() { output.push_str("(no buffers)\n"); }
show_output_popup(app, "list-buffers", output);
}
"show-buffer" | "showb" => {
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 msg = if parts.len() <= 1 {
DISPLAY_MESSAGE_DEFAULT_FMT.to_string()
} else {
let raw = parts[1..].join(" ");
raw.trim_matches('"').trim_matches('\'').to_string()
};
let expanded = crate::format::expand_format(&msg, app);
app.status_message = Some((expanded, Instant::now()));
}
}
"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_cmd = non_flag[1..].join(" ");
if has_append {
app.hooks.entry(non_flag[0].to_string()).or_default().push(hook_cmd);
} else {
app.hooks.insert(non_flag[0].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());
std::process::Command::new(&shell_prog)
.args(shell_args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.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()));
}
}
}
"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 {
if let Some(target) = parts[1..].iter().find(|a| a.parse::<usize>().is_ok()).and_then(|s| s.parse::<usize>().ok()) {
join_pane_local(app, target);
}
}
}
"join-pane" | "joinp" => {
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()) {
join_pane_local(app, target);
}
}
}
"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" => {
show_output_popup(app, "new-session", "(cannot create a new session from inside a session)\n".to_string());
}
"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()));
}
"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()));
}
"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()));
}
"choose-client" => {
app.status_message = Some(("choose-client: single-client model (you are the only client)".to_string(), Instant::now()));
}
"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(),
));
} 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(),
));
}
}
}
_ => {
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;