use crate::error::{BosunError, Result};
use crate::tmux::client::sync_tmux;
#[derive(Debug, Clone)]
pub struct BarSession {
pub internal: String,
pub display: String,
pub attached: bool,
}
const STATUS_LEFT: &str = "";
const STATUS_LEFT_LEN: &str = "0";
const STATUS_RIGHT_LEN: &str = "120";
const STATUS_STYLE: &str = "bg=default,fg=#e6e9ef";
pub fn install_globals(socket: Option<&str>, sessions: &[BarSession]) -> Result<()> {
bind_jump_keys(socket, sessions)
}
pub fn uninstall_globals(socket: Option<&str>) {
unbind_jump_keys(socket);
}
pub fn configure_session(
socket: Option<&str>,
session: &str,
_sessions: &[BarSession],
) -> Result<()> {
let hint = build_hint(socket);
let target = &["-t", session];
run_targeted(socket, target, &["set-option", "status", "on"])?;
run_targeted(
socket,
target,
&["set-option", "status-style", STATUS_STYLE],
)?;
run_targeted(socket, target, &["set-option", "status-left", STATUS_LEFT])?;
run_targeted(
socket,
target,
&["set-option", "status-left-length", STATUS_LEFT_LEN],
)?;
run_targeted(socket, target, &["set-option", "status-justify", "left"])?;
run_targeted(socket, target, &["set-option", "status-right", &hint])?;
run_targeted(
socket,
target,
&["set-option", "status-right-length", STATUS_RIGHT_LEN],
)?;
Ok(())
}
pub fn emergency_uninstall(socket: Option<&str>) {
unbind_jump_keys(socket);
}
fn bind_jump_keys(socket: Option<&str>, sessions: &[BarSession]) -> Result<()> {
for i in 0..9 {
let key = digit_key(i + 1);
match sessions.get(i) {
Some(entry) => {
let target = tmux_quote(&entry.internal);
let cmd = format!("switch-client -t {}", target);
run(socket, &["bind-key", "-T", "prefix", &key, &cmd])?;
}
None => {
let _ = run(socket, &["unbind-key", "-T", "prefix", &key]);
}
}
}
Ok(())
}
fn unbind_jump_keys(socket: Option<&str>) {
for i in 1..=9 {
let key = digit_key(i);
let _ = run(socket, &["unbind-key", "-T", "prefix", &key]);
}
}
fn digit_key(n: usize) -> String {
n.to_string()
}
fn build_hint(socket: Option<&str>) -> String {
let prefix = show_option(socket, "prefix");
let prefix = if prefix.is_empty() || prefix == "None" {
"C-b"
} else {
prefix.as_str()
};
format!("#[fg=#7c8495]^Q detach · S-←→ cycle · {} 1-9 jump ", prefix)
}
fn tmux_quote(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{}\"", escaped)
}
fn show_option(socket: Option<&str>, opt: &str) -> String {
let out = sync_tmux(socket, ["show-options", "-gqv", opt]).output();
match out {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
_ => String::new(),
}
}
fn run(socket: Option<&str>, args: &[&str]) -> Result<()> {
let output = sync_tmux(socket, args).output().map_err(BosunError::Io)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(BosunError::Tmux(format!(
"tmux {:?} failed: {}",
args,
stderr.trim()
)));
}
Ok(())
}
fn run_targeted(socket: Option<&str>, target: &[&str], args: &[&str]) -> Result<()> {
let mut full: Vec<&str> = Vec::with_capacity(args.len() + target.len());
full.push(args[0]);
full.extend_from_slice(target);
full.extend_from_slice(&args[1..]);
run(socket, &full)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tmux_quote_wraps_and_escapes() {
assert_eq!(tmux_quote("plain"), "\"plain\"");
assert_eq!(tmux_quote("has space"), "\"has space\"");
assert_eq!(tmux_quote("has\"quote"), "\"has\\\"quote\"");
}
#[test]
fn status_left_is_empty() {
assert_eq!(STATUS_LEFT, "");
}
#[test]
fn hint_includes_shift_arrow_cycle() {
let hint = build_hint(None);
assert!(hint.contains("S-←→ cycle"));
assert!(hint.contains("^Q detach"));
assert!(hint.contains("1-9 jump"));
}
#[test]
fn digit_key_is_bare_digit() {
assert_eq!(digit_key(1), "1");
assert_eq!(digit_key(9), "9");
}
}