use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) enum PowerState {
Awake,
Doze,
DeepSleep,
}
pub(crate) const PROBE_FORMAT: &str =
"#{session_attached},#{window_active},#{pane_active},#{window_zoomed_flag},#{client_activity}";
const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
pub(crate) fn probe(pane: &str) -> Option<String> {
use std::io::Read;
use wait_timeout::ChildExt;
let mut child = std::process::Command::new("tmux")
.args(["display-message", "-p", "-t", pane, PROBE_FORMAT])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.ok()?;
match child.wait_timeout(PROBE_TIMEOUT) {
Ok(Some(status)) if status.success() => {
let mut out = String::new();
child.stdout.take()?.read_to_string(&mut out).ok()?;
Some(out.trim().to_string())
}
Ok(Some(_)) | Err(_) => None,
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
None
}
}
}
pub(crate) fn parse_power_state(
output: &str,
now_epoch: u64,
doze_after: Duration,
) -> Option<PowerState> {
let mut fields = output.trim().split(',');
let mut next = || fields.next()?.trim().parse::<u64>().ok();
let session_attached = next()?;
let window_active = next()?;
let pane_active = next()?;
let window_zoomed = next()?;
let zoomed_away = window_zoomed == 1 && pane_active == 0;
let visible = session_attached >= 1 && window_active == 1 && !zoomed_away;
if !visible {
return Some(PowerState::DeepSleep);
}
let idle = match next() {
Some(client_activity) => now_epoch.saturating_sub(client_activity) >= doze_after.as_secs(),
None => false,
};
Some(if idle {
PowerState::Doze
} else {
PowerState::Awake
})
}
#[cfg(test)]
mod tests {
use super::*;
const DOZE: Duration = Duration::from_secs(120);
const NOW: u64 = 10_000;
#[test]
fn visible_with_recent_input_is_awake() {
let s = parse_power_state("1,1,1,0,9990", NOW, DOZE);
assert_eq!(s, Some(PowerState::Awake));
}
#[test]
fn visible_but_input_idle_dozes() {
let s = parse_power_state("1,1,0,0,9880", NOW, DOZE);
assert_eq!(s, Some(PowerState::Doze));
}
#[test]
fn detached_session_deep_sleeps_even_with_recent_input() {
let s = parse_power_state("0,1,1,0,9999", NOW, DOZE);
assert_eq!(s, Some(PowerState::DeepSleep));
}
#[test]
fn background_window_deep_sleeps() {
let s = parse_power_state("1,0,1,0,9999", NOW, DOZE);
assert_eq!(s, Some(PowerState::DeepSleep));
}
#[test]
fn zoomed_sibling_pane_hides_us() {
let s = parse_power_state("1,1,0,1,9999", NOW, DOZE);
assert_eq!(s, Some(PowerState::DeepSleep));
}
#[test]
fn zoomed_self_stays_awake() {
let s = parse_power_state("1,1,1,1,9999", NOW, DOZE);
assert_eq!(s, Some(PowerState::Awake));
}
#[test]
fn multiple_attached_clients_count_as_attached() {
let s = parse_power_state("2,1,1,0,9999", NOW, DOZE);
assert_eq!(s, Some(PowerState::Awake));
}
#[test]
fn clock_skew_ahead_of_now_is_not_idle() {
let s = parse_power_state("1,1,1,0,10500", NOW, DOZE);
assert_eq!(s, Some(PowerState::Awake));
}
#[test]
fn no_clients_anywhere_still_deep_sleeps() {
assert_eq!(
parse_power_state("0,1,0,0,", NOW, DOZE),
Some(PowerState::DeepSleep)
);
}
#[test]
fn visible_with_unreadable_activity_stays_awake() {
assert_eq!(
parse_power_state("1,1,1,0,", NOW, DOZE),
Some(PowerState::Awake)
);
assert_eq!(
parse_power_state("1,1,1,0", NOW, DOZE),
Some(PowerState::Awake)
);
}
#[test]
fn garbage_server_fields_fail_open_to_none() {
assert_eq!(parse_power_state("", NOW, DOZE), None);
assert_eq!(parse_power_state("no tmux server", NOW, DOZE), None);
assert_eq!(parse_power_state("1,x,1,0,9999", NOW, DOZE), None);
assert_eq!(parse_power_state("1,1", NOW, DOZE), None);
}
}