use anyhow::{Context, Result};
use ccboard_core::hook_event::{status_from_event, HookPayload};
use ccboard_core::hook_state::{make_session_key, LiveSessionFile};
use fd_lock::RwLock as FileLock;
use std::io::{self, Read};
use std::time::Duration;
pub fn run_hook(event_name: String) -> Result<()> {
let mut input = String::new();
io::stdin()
.read_to_string(&mut input)
.context("Failed to read stdin")?;
let mut payload: HookPayload = if input.trim().is_empty() {
HookPayload {
session_id: "unknown".to_string(),
cwd: std::env::current_dir()
.ok()
.and_then(|p| p.to_str().map(String::from))
.unwrap_or_else(|| "/tmp".to_string()),
..Default::default()
}
} else {
serde_json::from_str(&input).context("Failed to parse hook JSON payload")?
};
payload.event_name = event_name.clone();
let tty = detect_tty();
let status = status_from_event(&event_name, &payload);
let session_key = make_session_key(&payload.session_id, &tty);
let base_dir = dirs::home_dir()
.context("Cannot determine home directory")?
.join(".ccboard");
std::fs::create_dir_all(&base_dir).context("Failed to create ~/.ccboard/")?;
let file_path = base_dir.join("live-sessions.json");
let lock_path = base_dir.join("live-sessions.lock");
let lock_file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&lock_path)
.context("Failed to open lock file")?;
let mut file_lock = FileLock::new(lock_file);
let _lock_guard = file_lock
.write()
.context("Failed to acquire write lock on live-sessions")?;
let mut session_file = LiveSessionFile::load(&file_path).unwrap_or_else(|e| {
eprintln!("[ccboard] Warning: failed to parse live-sessions.json: {e}. Starting fresh.");
LiveSessionFile {
version: 1,
..Default::default()
}
});
session_file.upsert(
session_key,
payload.session_id.clone(),
payload.cwd.clone(),
tty,
status,
event_name.clone(),
);
session_file.prune_stopped(Duration::from_secs(30 * 60));
session_file.prune_stale_running(Duration::from_secs(10 * 60));
session_file.updated_at = Some(chrono::Utc::now());
session_file
.save(&file_path)
.context("Failed to save live-sessions.json")?;
drop(_lock_guard);
#[cfg(target_os = "macos")]
{
use ccboard_core::hook_state::HookSessionStatus;
if status == HookSessionStatus::Stopped {
let project_name = std::path::Path::new(&payload.cwd)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let safe_name = project_name.replace('\\', "\\\\").replace('"', "\\\"");
let _ = std::process::Command::new("osascript")
.args([
"-e",
&format!(
"display notification \"Session terminée : {}\" with title \"ccboard\"",
safe_name
),
])
.spawn(); }
}
Ok(())
}
fn detect_tty() -> String {
if let Ok(tty) = std::env::var("TTY") {
if !tty.is_empty() {
return tty;
}
}
if let Ok(output) = std::process::Command::new("tty").output() {
if output.status.success() {
let tty = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !tty.is_empty() && !tty.contains("not a tty") {
return tty;
}
}
}
"unknown".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_tty_fallback() {
let tty = detect_tty();
assert!(!tty.is_empty());
}
}