use std::io::{self, Write};
use std::time::Duration;
use std::env;
pub fn is_warm_session(base: &str) -> bool {
base == "__warm__" || base.ends_with("____warm__")
}
pub fn next_session_name(ns_prefix: Option<&str>) -> String {
let home = match env::var("USERPROFILE").or_else(|_| env::var("HOME")) {
Ok(h) => h,
Err(_) => return "0".to_string(),
};
let psmux_dir = format!("{}\\.psmux", home);
let mut used: std::collections::HashSet<u32> = std::collections::HashSet::new();
if let Ok(entries) = std::fs::read_dir(&psmux_dir) {
for entry in entries.flatten() {
if let Some(fname) = entry.file_name().to_str() {
if let Some((base, ext)) = fname.rsplit_once('.') {
if ext != "port" { continue; }
if is_warm_session(base) { continue; }
let session_part = if let Some(pfx) = ns_prefix {
let full_pfx = format!("{}__", pfx);
if base.starts_with(&full_pfx) {
&base[full_pfx.len()..]
} else {
continue; }
} else {
if base.contains("__") { continue; } base
};
if let Ok(n) = session_part.parse::<u32>() {
used.insert(n);
}
}
}
}
}
let mut id = 0u32;
while used.contains(&id) {
id += 1;
}
id.to_string()
}
pub fn cleanup_stale_port_files() {
let home = match env::var("USERPROFILE").or_else(|_| env::var("HOME")) {
Ok(h) => h,
Err(_) => return,
};
let psmux_dir = format!("{}\\.psmux", home);
if let Ok(entries) = std::fs::read_dir(&psmux_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map(|e| e == "port").unwrap_or(false) {
if let Ok(port_str) = std::fs::read_to_string(&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(),
Duration::from_millis(5)
).is_err() {
let _ = std::fs::remove_file(&path);
let key_path = path.with_extension("key");
let _ = std::fs::remove_file(&key_path);
}
} else {
let _ = std::fs::remove_file(&path);
let key_path = path.with_extension("key");
let _ = std::fs::remove_file(&key_path);
}
}
}
}
}
}
pub fn read_session_key(session: &str) -> io::Result<String> {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
let keypath = format!("{}\\.psmux\\{}.key", home, session);
std::fs::read_to_string(&keypath).map(|s| s.trim().to_string())
}
pub fn send_auth_cmd(addr: &str, key: &str, cmd: &[u8]) -> io::Result<()> {
let sock_addr: std::net::SocketAddr = addr.parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
if let Ok(mut s) = std::net::TcpStream::connect_timeout(&sock_addr, Duration::from_millis(50)) {
let _ = s.set_nodelay(true);
let _ = write!(s, "AUTH {}\n", key);
let _ = std::io::Write::write_all(&mut s, cmd);
let _ = s.flush();
}
Ok(())
}
pub fn send_auth_cmd_response(addr: &str, key: &str, cmd: &[u8]) -> io::Result<String> {
let mut s = std::net::TcpStream::connect(addr)?;
let _ = s.set_nodelay(true);
let _ = s.set_read_timeout(Some(Duration::from_millis(500)));
let _ = write!(s, "AUTH {}\n", key);
let _ = std::io::Write::write_all(&mut s, cmd);
let _ = s.flush();
let mut br = std::io::BufReader::new(&mut s);
let mut auth_line = String::new();
let _ = std::io::BufRead::read_line(&mut br, &mut auth_line);
let mut buf = String::new();
let _ = std::io::Read::read_to_string(&mut br, &mut buf);
Ok(buf)
}
pub fn send_control(line: String) -> io::Result<()> {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
let mut target = env::var("PSMUX_TARGET_SESSION").ok().unwrap_or_else(|| "default".to_string());
if is_warm_session(&target) {
target = resolve_last_session_name().unwrap_or_else(|| "default".to_string());
}
let full_target = env::var("PSMUX_TARGET_FULL").ok();
let path = format!("{}\\.psmux\\{}.port", home, target);
let port = std::fs::read_to_string(&path).ok().and_then(|s| s.trim().parse::<u16>().ok()).ok_or_else(|| io::Error::new(io::ErrorKind::Other, format!("no server running on session '{}'", target)))?.clone();
let session_key = read_session_key(&target).unwrap_or_default();
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap();
let mut stream = std::net::TcpStream::connect_timeout(&addr, Duration::from_millis(100))?;
let _ = stream.set_nodelay(true);
let _ = stream.set_read_timeout(Some(Duration::from_millis(50)));
let _ = write!(stream, "AUTH {}\n", session_key);
if let Some(ref ft) = full_target {
let _ = write!(stream, "TARGET {}\n", ft);
}
let _ = write!(stream, "{}", line);
let _ = stream.flush();
let mut buf = [0u8; 64];
let _ = std::io::Read::read(&mut stream, &mut buf);
Ok(())
}
pub fn send_control_with_response(line: String) -> io::Result<String> {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
let mut target = env::var("PSMUX_TARGET_SESSION").ok().unwrap_or_else(|| "default".to_string());
if is_warm_session(&target) {
target = resolve_last_session_name().unwrap_or_else(|| "default".to_string());
}
let full_target = env::var("PSMUX_TARGET_FULL").ok();
let path = format!("{}\\.psmux\\{}.port", home, target);
let port = std::fs::read_to_string(&path).ok().and_then(|s| s.trim().parse::<u16>().ok()).ok_or_else(|| io::Error::new(io::ErrorKind::Other, format!("no server running on session '{}'", target)))?.clone();
let session_key = read_session_key(&target).unwrap_or_default();
let addr = format!("127.0.0.1:{}", port);
let mut stream = std::net::TcpStream::connect(&addr)?;
let _ = stream.set_nodelay(true);
let _ = stream.set_read_timeout(Some(Duration::from_millis(2000)));
let _ = write!(stream, "AUTH {}\n", session_key);
if let Some(ref ft) = full_target {
let _ = write!(stream, "TARGET {}\n", ft);
}
let _ = write!(stream, "{}", line);
let _ = stream.flush();
let mut buf = Vec::new();
let mut temp = [0u8; 4096];
loop {
match std::io::Read::read(&mut stream, &mut temp) {
Ok(0) => break,
Ok(n) => buf.extend_from_slice(&temp[..n]),
Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => break,
Err(_) => break,
}
}
let result = String::from_utf8_lossy(&buf).to_string();
let result = if result.starts_with("OK\n") {
result[3..].to_string()
} else if result.starts_with("OK\r\n") {
result[4..].to_string()
} else {
result
};
Ok(result)
}
pub fn send_control_to_port(port: u16, msg: &str, session_key: &str) -> io::Result<()> {
let addr = format!("127.0.0.1:{}", port);
if let Ok(mut stream) = std::net::TcpStream::connect(&addr) {
let _ = stream.set_nodelay(true);
let _ = write!(stream, "AUTH {}\n", session_key);
let _ = stream.write_all(msg.as_bytes());
let _ = stream.flush();
let mut buf = [0u8; 64];
let _ = stream.set_read_timeout(Some(Duration::from_millis(50)));
let _ = std::io::Read::read(&mut stream, &mut buf);
}
Ok(())
}
pub fn resolve_last_session_name() -> Option<String> {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).ok()?;
let dir = format!("{}\\.psmux", home);
let last = std::fs::read_to_string(format!("{}\\last_session", dir)).ok();
if let Some(name) = last {
let name = name.trim().to_string();
let p = format!("{}\\{}.port", dir, name);
if std::path::Path::new(&p).exists() { return Some(name); }
}
let mut picks: Vec<(String, std::time::SystemTime)> = Vec::new();
if let Ok(rd) = std::fs::read_dir(&dir) {
for e in rd.flatten() {
if let Some(fname) = e.file_name().to_str() {
if let Some((base, ext)) = fname.rsplit_once('.') {
if ext == "port" { if let Ok(md) = e.metadata() { picks.push((base.to_string(), md.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH))); } }
}
}
}
}
picks.retain(|(n, _)| !is_warm_session(n));
picks.sort_by_key(|(_, t)| *t);
picks.last().map(|(n, _)| n.clone())
}
pub fn resolve_default_session_name() -> Option<String> {
if let Ok(name) = env::var("PSMUX_DEFAULT_SESSION") {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).ok()?;
let p = format!("{}\\.psmux\\{}.port", home, name);
if std::path::Path::new(&p).exists() { return Some(name); }
}
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).ok()?;
let candidates = [format!("{}\\.psmuxrc", home), format!("{}\\.psmux\\pmuxrc", home)];
for cfg in candidates.iter() {
if let Ok(text) = std::fs::read_to_string(cfg) {
let line = text.lines().find(|l| !l.trim().is_empty())?;
let name = if let Some(rest) = line.strip_prefix("default-session ") { rest.trim().to_string() } else { line.trim().to_string() };
let p = format!("{}\\.psmux\\{}.port", home, name);
if std::path::Path::new(&p).exists() { return Some(name); }
}
}
None
}
pub fn reap_children_placeholder() -> io::Result<bool> { Ok(false) }
pub fn list_session_names() -> Vec<String> {
let home = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap_or_default();
let dir = format!("{}\\.psmux", home);
let mut names = Vec::new();
if let Ok(entries) = std::fs::read_dir(&dir) {
for e in entries.flatten() {
if let Some(fname) = e.file_name().to_str().map(|s| s.to_string()) {
if let Some((base, ext)) = fname.rsplit_once('.') {
if ext == "port" {
if is_warm_session(base) { continue; }
names.push(base.to_string());
}
}
}
}
}
names.sort();
names
}
#[derive(Clone, Debug)]
pub struct TreeEntry {
pub session_name: String,
pub session_port: u16,
pub is_session_header: bool,
pub window_index: Option<usize>,
pub window_name: String,
pub window_panes: usize,
pub window_size: String,
pub is_current_session: bool,
pub is_active_window: bool,
}
pub fn list_all_sessions_tree(current_session: &str, current_windows: &[(String, usize, String, bool)]) -> Vec<TreeEntry> {
let home = match env::var("USERPROFILE").or_else(|_| env::var("HOME")) {
Ok(h) => h,
Err(_) => return vec![],
};
let psmux_dir = format!("{}\\.psmux", home);
let mut sessions: Vec<(String, u16, std::time::SystemTime)> = Vec::new();
if let Ok(entries) = std::fs::read_dir(&psmux_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map(|e| e == "port").unwrap_or(false) {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
if is_warm_session(stem) { continue; }
if let Ok(port_str) = std::fs::read_to_string(&path) {
if let Ok(port) = port_str.trim().parse::<u16>() {
let mtime = entry.metadata()
.and_then(|m| m.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
sessions.push((stem.to_string(), port, mtime));
}
}
}
}
}
}
sessions.sort_by_key(|(name, _, _)| name.clone());
let mut tree = Vec::new();
for (name, port, _) in &sessions {
let is_current = name == current_session;
tree.push(TreeEntry {
session_name: name.clone(),
session_port: *port,
is_session_header: true,
window_index: None,
window_name: String::new(),
window_panes: 0,
window_size: String::new(),
is_current_session: is_current,
is_active_window: false,
});
if is_current {
for (i, (wname, panes, size, is_active)) in current_windows.iter().enumerate() {
tree.push(TreeEntry {
session_name: name.clone(),
session_port: *port,
is_session_header: false,
window_index: Some(i),
window_name: wname.clone(),
window_panes: *panes,
window_size: size.clone(),
is_current_session: true,
is_active_window: *is_active,
});
}
} else {
let key = read_session_key(name).unwrap_or_default();
let addr = format!("127.0.0.1:{}", port);
if let Ok(resp) = send_auth_cmd_response(&addr, &key, b"list-windows -F \"#{window_index}:#{window_name}:#{window_panes}:#{window_width}x#{window_height}:#{window_active}\"\n") {
for line in resp.lines() {
let line = line.trim();
if line.is_empty() { continue; }
let parts: Vec<&str> = line.splitn(5, ':').collect();
if parts.len() >= 5 {
let wi = parts[0].parse::<usize>().unwrap_or(0);
let wn = parts[1].to_string();
let wp = parts[2].parse::<usize>().unwrap_or(1);
let ws = parts[3].to_string();
let wa = parts[4] == "1";
tree.push(TreeEntry {
session_name: name.clone(),
session_port: *port,
is_session_header: false,
window_index: Some(wi),
window_name: wn,
window_panes: wp,
window_size: ws,
is_current_session: false,
is_active_window: wa,
});
}
}
}
}
}
tree
}
#[cfg(windows)]
pub fn kill_remaining_server_processes() {
const TH32CS_SNAPPROCESS: u32 = 0x00000002;
const PROCESS_TERMINATE: u32 = 0x0001;
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const INVALID_HANDLE: isize = -1;
#[repr(C)]
struct PROCESSENTRY32W {
dw_size: u32,
cnt_usage: u32,
th32_process_id: u32,
th32_default_heap_id: usize,
th32_module_id: u32,
cnt_threads: u32,
th32_parent_process_id: u32,
pc_pri_class_base: i32,
dw_flags: u32,
sz_exe_file: [u16; 260],
}
#[link(name = "kernel32")]
extern "system" {
fn CreateToolhelp32Snapshot(dw_flags: u32, th32_process_id: u32) -> isize;
fn Process32FirstW(h_snapshot: isize, lppe: *mut PROCESSENTRY32W) -> i32;
fn Process32NextW(h_snapshot: isize, lppe: *mut PROCESSENTRY32W) -> i32;
fn OpenProcess(desired_access: u32, inherit_handle: i32, process_id: u32) -> isize;
fn TerminateProcess(h_process: isize, exit_code: u32) -> i32;
fn CloseHandle(handle: isize) -> i32;
}
let my_pid = std::process::id();
unsafe {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snap == INVALID_HANDLE || snap == 0 { return; }
let mut pe: PROCESSENTRY32W = std::mem::zeroed();
pe.dw_size = std::mem::size_of::<PROCESSENTRY32W>() as u32;
let target_names: &[&str] = &["psmux.exe", "pmux.exe", "tmux.exe"];
let mut pids_to_kill: Vec<u32> = Vec::new();
if Process32FirstW(snap, &mut pe) != 0 {
loop {
let pid = pe.th32_process_id;
if pid != my_pid {
let len = pe.sz_exe_file.iter().position(|&c| c == 0).unwrap_or(260);
let name = String::from_utf16_lossy(&pe.sz_exe_file[..len]);
let name_lower = name.to_lowercase();
for target in target_names {
if name_lower == *target || name_lower.ends_with(&format!("\\{}", target)) {
pids_to_kill.push(pid);
break;
}
}
}
if Process32NextW(snap, &mut pe) == 0 { break; }
}
}
CloseHandle(snap);
for pid in &pids_to_kill {
let h = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, 0, *pid);
if h != 0 && h != INVALID_HANDLE {
let _ = TerminateProcess(h, 1);
CloseHandle(h);
}
}
}
}
#[cfg(not(windows))]
pub fn kill_remaining_server_processes() {
let _ = std::process::Command::new("pkill")
.args(&["-f", "psmux|pmux"])
.status();
}