use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[cfg(unix)]
mod host;
pub use host::{Held, Keys, left_running, serve, start, stopped};
pub const HELD_VAR: &str = cellix_agent::hook::HELD_VAR;
#[cfg(unix)]
const STOPPED: &str = "stopped";
#[derive(Clone, Debug)]
pub struct Stopped {
pub job: Job,
pub output: Vec<u8>,
pub agent: Option<cellix_agent::hook::Resumable>,
pub ended: Option<std::time::SystemTime>,
notes: PathBuf,
}
impl Stopped {
pub fn by_restart(&self) -> bool {
matches!((self.ended, booted_at()), (Some(ended), Some(booted)) if ended <= booted)
}
pub fn forget(&self) {
for extension in ["json", "log", "agent"] {
let _ = std::fs::remove_file(self.notes.with_extension(extension));
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Job {
pub name: String,
pub argv: Vec<String>,
pub env: Vec<String>,
pub folder: PathBuf,
pub size: (u16, u16),
pub pixels: (u16, u16),
}
pub fn booted_at() -> Option<std::time::SystemTime> {
let seconds: u64 = if cfg!(target_os = "linux") {
let stat = std::fs::read_to_string("/proc/stat").ok()?;
let line = stat.lines().find(|l| l.starts_with("btime "))?;
line[6..].trim().parse().ok()?
} else if cfg!(target_os = "macos") {
let output = std::process::Command::new("sysctl")
.args(["-n", "kern.boottime"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
let after = text.split("sec = ").nth(1)?;
after.split(',').next()?.trim().parse().ok()?
} else {
return None;
};
Some(std::time::UNIX_EPOCH + std::time::Duration::from_secs(seconds))
}
pub fn sessions_base() -> PathBuf {
let var = |name: &str| std::env::var_os(name).map(PathBuf::from);
let cache = var("XDG_CACHE_HOME").or_else(|| var("HOME").map(|home| home.join(".cache")));
cache
.unwrap_or_else(std::env::temp_dir)
.join("cellix")
.join("sessions")
}
pub fn folder_of(project: &Path) -> PathBuf {
let mut hasher = DefaultHasher::new();
project.hash(&mut hasher);
sessions_base().join(format!("{:016x}", hasher.finish()))
}
pub fn mark_project(project: &Path) {
let folder = folder_of(project);
let file = folder.join(PROJECT);
let Some(named) = project.to_str() else {
return;
};
if std::fs::read_to_string(&file).is_ok_and(|known| known == named) {
return;
}
if std::fs::create_dir_all(&folder).is_ok() {
let _ = std::fs::write(file, named);
}
}
const PROJECT: &str = "project";
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Activity {
pub running: Vec<String>,
pub stopped: Vec<String>,
}
impl Activity {
pub fn is_empty(&self) -> bool {
self.running.is_empty() && self.stopped.is_empty()
}
}
pub fn activity(project: &Path) -> Activity {
let folder = folder_of(project);
let stopped = stopped(&folder).into_iter().map(|s| s.job.name).collect();
let running = left_running(&folder)
.into_iter()
.map(|(name, _)| name)
.collect();
Activity { running, stopped }
}
pub fn projects_with_activity() -> Vec<(PathBuf, Activity)> {
let mut found = Vec::new();
for entry in std::fs::read_dir(sessions_base())
.into_iter()
.flatten()
.flatten()
{
let Ok(named) = std::fs::read_to_string(entry.path().join(PROJECT)) else {
continue;
};
let project = PathBuf::from(named);
if folder_of(&project) != entry.path() || !project.is_dir() {
continue;
}
let activity = activity(&project);
if !activity.is_empty() {
found.push((project, activity));
}
}
found.sort_by(|a, b| a.0.cmp(&b.0));
found
}
#[cfg(not(unix))]
mod host {
use std::io::{self, ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;
use super::Job;
fn unsupported() -> io::Error {
io::Error::new(
ErrorKind::Unsupported,
"sessions that outlive Cellix need Unix",
)
}
pub fn start(_program: &Path, _folder: &Path, _job: &Job) -> io::Result<Held> {
Err(unsupported())
}
pub fn left_running(_folder: &Path) -> Vec<(String, PathBuf)> {
Vec::new()
}
pub fn stopped(_folder: &Path) -> Vec<super::Stopped> {
Vec::new()
}
pub fn serve(_socket: &Path) -> io::Result<()> {
Err(unsupported())
}
pub enum Held {}
pub enum Keys {}
impl Write for Keys {
fn write(&mut self, _: &[u8]) -> io::Result<usize> {
match *self {}
}
fn flush(&mut self) -> io::Result<()> {
match *self {}
}
}
impl Held {
pub fn connect(_socket: &Path) -> io::Result<Self> {
Err(unsupported())
}
pub fn keys(&self) -> io::Result<Keys> {
match *self {}
}
pub fn listen(&self, _output: Sender<Vec<u8>>, _done: Sender<()>) -> io::Result<()> {
match *self {}
}
pub fn at_prompt(&self) -> bool {
match *self {}
}
pub fn resize(&self, _size: (u16, u16), _pixels: (u16, u16)) {
match *self {}
}
pub fn kill(&self) {
match *self {}
}
}
}