use std::io::IsTerminal;
use super::tmux_query::query_lines;
struct Row {
session: String,
event: String,
command: String,
}
pub(crate) fn run(socket: &str) -> i32 {
let sessions = query_lines(socket, &["list-sessions", "-F", "#{session_name}"]);
let mut pairs: Vec<(String, String)> = Vec::new();
for session in &sessions {
let session = session.trim();
if session.is_empty() {
continue;
}
for line in query_lines(socket, &["show-hooks", "-t", session]) {
pairs.push((session.to_string(), line));
}
}
let rows = build_rows(pairs);
let json = std::env::args().any(|a| a == "--json")
|| std::env::args()
.collect::<Vec<_>>()
.windows(2)
.any(|w| w[0] == "-o" && w[1] == "json");
if json {
print!("{}", render_json(&rows));
} else {
print!("{}", render_text(&rows, std::io::stdout().is_terminal()));
}
0
}
fn parse_hook_line(line: &str) -> Option<(String, String)> {
let (head, rest) = line.trim().split_once(char::is_whitespace)?;
let event = head.split('[').next().unwrap_or(head);
let command = rest.trim();
if event.is_empty() || command.is_empty() {
return None;
}
Some((event.to_string(), command.to_string()))
}
fn build_rows(pairs: Vec<(String, String)>) -> Vec<Row> {
let mut rows: Vec<Row> = pairs
.into_iter()
.filter_map(|(session, line)| {
let (event, command) = parse_hook_line(&line)?;
Some(Row {
session,
event,
command,
})
})
.collect();
rows.sort_by(|a, b| a.session.cmp(&b.session).then(a.event.cmp(&b.event)));
rows
}
fn render_text(rows: &[Row], color: bool) -> String {
let paint = |s: &str, code: &str| -> String {
if color {
format!("\x1b[{code}m{s}\x1b[0m")
} else {
s.to_string()
}
};
let mut out = String::new();
out.push_str(&format!(
"{}\n",
paint(
&format!("{:<12} {:<24} {}", "SESSION", "EVENT", "COMMAND"),
"1"
)
));
for r in rows {
out.push_str(&format!(
"{:<12} {:<24} {}\n",
r.session, r.event, r.command
));
}
out
}
fn render_json(rows: &[Row]) -> String {
let arr: Vec<serde_json::Value> = rows
.iter()
.map(|r| {
serde_json::json!({
"session": r.session,
"event": r.event,
"command": r.command,
})
})
.collect();
format!(
"{}\n",
serde_json::to_string_pretty(&serde_json::Value::Array(arr)).unwrap_or_default()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_hook_line_with_array_index() {
let (e, c) = parse_hook_line("after-select-window[0] display-message hi").unwrap();
assert_eq!(e, "after-select-window");
assert_eq!(c, "display-message hi");
}
#[test]
fn parses_a_hook_line_without_index() {
let (e, c) = parse_hook_line("pane-died respawn-pane").unwrap();
assert_eq!(e, "pane-died");
assert_eq!(c, "respawn-pane");
}
#[test]
fn rejects_malformed_lines() {
assert!(parse_hook_line("").is_none());
assert!(parse_hook_line("just-an-event").is_none()); }
#[test]
fn build_rows_sorts_by_session_then_event() {
let pairs = vec![
("work".to_string(), "pane-died[0] respawn-pane".to_string()),
(
"work".to_string(),
"after-select-window[0] display x".to_string(),
),
(
"admin".to_string(),
"client-attached[0] display y".to_string(),
),
];
let rows = build_rows(pairs);
assert_eq!(rows.len(), 3);
assert_eq!(rows[0].session, "admin");
assert_eq!(rows[1].event, "after-select-window");
assert_eq!(rows[2].event, "pane-died");
}
#[test]
fn json_carries_session_event_command() {
let pairs = vec![("a".to_string(), "pane-died respawn-pane".to_string())];
let rows = build_rows(pairs);
let v: serde_json::Value = serde_json::from_str(&render_json(&rows)).unwrap();
assert_eq!(v[0]["session"], "a");
assert_eq!(v[0]["event"], "pane-died");
assert_eq!(v[0]["command"], "respawn-pane");
}
}