use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use serde::Serialize;
pub const KASL_ENV: &str = "RIGGER_KASL";
const PATIENCE: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case", tag = "state", content = "said")]
pub enum Told {
Taken,
Absent,
Refused(String),
}
fn program() -> Option<std::ffi::OsString> {
match std::env::var_os(KASL_ENV) {
Some(named) if !named.is_empty() => Some(named),
_ if crate::paths::scratch() => None,
_ => Some("kasl".into()),
}
}
pub fn start(task: &str) -> Told {
run(&["focus", "start", task, "--from", "rigger"])
}
pub fn stop() -> Told {
run(&["focus", "stop", "--from", "rigger"])
}
fn run(args: &[&str]) -> Told {
let Some(program) = program() else {
return Told::Absent;
};
let child = Command::new(&program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn();
let mut child = match child {
Ok(child) => child,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Told::Absent,
Err(e) => return Told::Refused(e.to_string()),
};
let began = Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) if status.success() => return Told::Taken,
Ok(Some(_)) => {
let mut said = String::new();
if let Some(mut stderr) = child.stderr.take() {
let _ = std::io::Read::read_to_string(&mut stderr, &mut said);
}
let first = said.lines().find(|l| !l.trim().is_empty()).unwrap_or("kasl said no").trim();
return Told::Refused(first.to_string());
}
Ok(None) if began.elapsed() < PATIENCE => std::thread::sleep(Duration::from_millis(25)),
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
return Told::Refused(format!("kasl did not answer within {} seconds", PATIENCE.as_secs()));
}
Err(e) => return Told::Refused(e.to_string()),
}
}
}
pub fn task_name(project: &str, stage: Option<(&str, Option<&str>)>) -> String {
match stage {
Some((version, Some(title))) => format!("{project} {version} · {title}"),
Some((version, None)) => format!("{project} {version}"),
None => project.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_session_is_named_by_its_stage() {
assert_eq!(task_name("widget", Some(("v0.2.0", Some("Second")))), "widget v0.2.0 · Second");
assert_eq!(task_name("widget", Some(("v0.2.0", None))), "widget v0.2.0");
assert_eq!(task_name("widget", None), "widget");
}
}