use std::{ collections::{hash_map::Entry, HashMap}, fmt::Display, io::{BufWriter, Write}, iter::IntoIterator, time::Duration, }; use clap::Args; use serde::{Deserialize, Serialize}; pub trait SessionWriter { type Error: Display; type Writer: Write; fn writer(&self) -> Result; fn format(&self, session: &Session) -> Result; fn filter(&self, session: &Session) -> bool; } #[derive(Debug, Clone, Args)] #[group(skip)] pub struct Config { /// Enable sorting #[arg(long, default_value_t = true)] pub sort: bool, } #[derive(Debug, Default)] pub struct Sessions { inner: HashMap, sort: bool, } impl Sessions { pub fn new(Config { sort }: Config) -> Self { Self { sort, ..Default::default() } } pub fn write_sessions(&self, writer: W) -> std::io::Result<()> { let mut buf_writer = BufWriter::new(writer.writer()?); let mut sessions: Vec = self.inner.iter().map(Session::from).collect(); if self.sort { sessions.sort(); } for session in sessions { match writer.format(&session) { Ok(fmt) if writer.filter(&session) => writeln!(buf_writer, "{fmt}")?, Err(err) => tracing::warn!(%err, "Failed to format session"), _ => tracing::debug!(%session, "Skipping filtered session"), } } Ok(()) } pub fn add(&mut self, item: Session) { let span = tracing::trace_span!("Entry", ?item); let _guard = span.enter(); match self.inner.entry(item.name) { Entry::Occupied(mut o) if item.state.is_better_than(o.get()) => { tracing::trace!(prev=?o, ?item.state, "New entry is more recent or accurate, replacing"); o.insert(item.state); } Entry::Occupied(o) => { tracing::trace!(existing=?o, ?item.state, "Existing entry is more recent or accurate, skipping"); } Entry::Vacant(v) => { tracing::trace!(?item.state, "No previous entry exists, inserting"); v.insert(item.state); } } } } impl Extend for Sessions { fn extend>(&mut self, iter: T) { for item in iter { self.add(item) } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum State { Discovered, #[serde(with = "epoch_timestamp")] Created(Duration), #[serde(with = "epoch_timestamp")] Attached(Duration), LocalHost, } impl State { pub fn is_better_than(&self, state: &State) -> bool { match (self, state) { (&State::LocalHost, _) => false, (_, &State::LocalHost) => true, _ => self > state, } } } mod epoch_timestamp { use std::time::Duration; use serde::{self, Deserialize, Deserializer, Serializer}; pub fn serialize(duration: &Duration, serializer: S) -> Result where S: Serializer, { serializer.serialize_u64(duration.as_secs()) } pub fn deserialize<'de, D>(d: D) -> Result where D: Deserializer<'de>, { u64::deserialize(d).map(Duration::from_secs) } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct Session { pub state: State, pub name: String, } impl Session { pub fn discover(name: impl Into) -> Self { Self { name: name.into(), state: State::Discovered, } } pub fn localhost(name: impl Into) -> Self { Self { name: name.into(), state: State::LocalHost, } } } impl Display for Session { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name) } } impl From<(String, State)> for Session { fn from((name, state): (String, State)) -> Self { Self { state, name } } } impl From<(&String, &State)> for Session { fn from((name, &state): (&String, &State)) -> Self { Self { state, name: name.to_owned(), } } }