modular session management daemon
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516mod session_core_v1 { hyprwire::include_protocol!("session_core_v1"); pub use client::*; pub use spec::*;}
use anyhow::Context;use clap::{CommandFactory, Parser, Subcommand, ValueEnum};use hyprwire::client;use owo_colors::OwoColorize;use rustix::process;use session_core_v1::{session_core_manager, session_core_session};
struct Session { id: u32, object: session_core_session::SessionCoreSession, uid: u32, username: String, vt: u32, seat: Option<String>, leader: u32, timestamp: u32, active: session_core_v1::ActiveState, done: bool,}
struct State { sessions: Vec<Session>, done: bool, error: Option<String>,}
impl hyprwire::Dispatch<session_core_manager::SessionCoreManager> for State { fn event( &mut self, object: &session_core_manager::SessionCoreManager, event: <session_core_manager::SessionCoreManager as hyprwire::Object>::Event<'_>, ) { match event { session_core_manager::Event::Sessions { session_ids } => { for session_id in session_ids { self.add_session(object, session_id); } } session_core_manager::Event::SessionAdded { session_id } => { self.add_session(object, session_id); } session_core_manager::Event::SessionRemoved { session_id } => { self.sessions.retain(|session| session.id != session_id); } } }}
impl State { fn add_session(&mut self, manager: &session_core_manager::SessionCoreManager, session_id: u32) { let Some(object) = manager.send_get_session::<Self>(session_id) else { self.error = Some(format!( "failed to create session object for id {session_id}" )); self.done = true; return; };
self.sessions.push(Session { id: session_id, object, uid: 0, username: String::new(), vt: 0, seat: None, leader: 0, timestamp: 0, done: false, active: session_core_v1::ActiveState::Online, }); }}
impl hyprwire::Dispatch<session_core_session::SessionCoreSession> for State { fn event( &mut self, object: &session_core_session::SessionCoreSession, event: <session_core_session::SessionCoreSession as hyprwire::Object>::Event<'_>, ) { let Some(session) = self .sessions .iter_mut() .find(|session| session.object == *object) else { return; };
match event { session_core_session::Event::Uid { uid } => session.uid = uid, session_core_session::Event::Username { username } => session.username = username, session_core_session::Event::Vt { vt } => session.vt = vt, session_core_session::Event::Seat { seat } => session.seat = Some(seat), session_core_session::Event::Leader { leader } => session.leader = leader, session_core_session::Event::Timestamp { timestamp } => session.timestamp = timestamp, session_core_session::Event::Done => session.done = true, session_core_session::Event::Ok => { self.done = true; } session_core_session::Event::Error { code, message } => { self.error = Some(format!("{message} (code={code})")); self.done = true; } session_core_session::Event::Active { state } => { session.active = state; } _ => {} } }}
#[derive(Subcommand)]enum Command { #[command( subcommand, about = "inspect and control sessions", arg_required_else_help = true )] Session(SessionCommand), #[command(about = "generate shell completions")] Completions { #[arg(value_name = "SHELL")] shell: CompletionShell, },}
#[derive(Clone, Copy, ValueEnum)]enum CompletionShell { Bash, Elvish, Fish, Nushell, Zsh,}
impl CompletionShell { fn generate(self, command: &mut clap::Command) { let mut stdout = std::io::stdout();
match self { Self::Bash => clap_complete::generate( clap_complete::Shell::Bash, command, "sessionctl", &mut stdout, ), Self::Elvish => clap_complete::generate( clap_complete::Shell::Elvish, command, "sessionctl", &mut stdout, ), Self::Fish => clap_complete::generate( clap_complete::Shell::Fish, command, "sessionctl", &mut stdout, ), Self::Nushell => clap_complete::generate( clap_complete_nushell::Nushell, command, "sessionctl", &mut stdout, ), Self::Zsh => clap_complete::generate( clap_complete::Shell::Zsh, command, "sessionctl", &mut stdout, ), } }}
#[derive(Subcommand)]enum SessionCommand { #[command(about = "show live sessions")] List, #[command( about = "ask sessions to exit", group(clap::ArgGroup::new("target").required(true).args(["ids", "current"])) )] Close { #[arg(long, help = "target your current session")] current: bool, #[arg(value_name = "SESSION", help = "session id; omit with --current")] ids: Vec<u32>, }, #[command( about = "force-stop sessions", group(clap::ArgGroup::new("target").required(true).args(["ids", "current"])) )] Kill { #[arg(long, help = "target your current session")] current: bool, #[arg(value_name = "SESSION", help = "session id; omit with --current")] ids: Vec<u32>, }, #[command(about = "lock sessions")] Lock { #[arg(value_name = "SESSION", help = "session id; defaults to your session")] ids: Vec<u32>, }, #[command(about = "unlock sessions")] Unlock { #[arg(value_name = "SESSION", help = "session id; defaults to your session")] ids: Vec<u32>, },}
#[derive(Parser)]#[command( name = "sessionctl", bin_name = "sessionctl", author, version, about = "sessiond control tool", long_about = None, arg_required_else_help = true)]pub(crate) struct Cli { #[command(subcommand)] command: Command,}
#[derive(Clone, Copy)]enum SessionAction { Close, Kill, Lock, Unlock,}
impl SessionAction { const fn from_command(command: &SessionCommand) -> Option<Self> { match command { SessionCommand::List => None, SessionCommand::Close { .. } => Some(Self::Close), SessionCommand::Kill { .. } => Some(Self::Kill), SessionCommand::Lock { .. } => Some(Self::Lock), SessionCommand::Unlock { .. } => Some(Self::Unlock), } }
fn ids(command: &SessionCommand) -> Option<&[u32]> { match command { SessionCommand::List => None, SessionCommand::Close { ids, .. } | SessionCommand::Kill { ids, .. } | SessionCommand::Lock { ids } | SessionCommand::Unlock { ids } => Some(ids), } }
const fn targets_current(command: &SessionCommand) -> bool { match command { SessionCommand::List => false, SessionCommand::Close { current, .. } | SessionCommand::Kill { current, .. } => { *current } SessionCommand::Lock { ids } | SessionCommand::Unlock { ids } => ids.is_empty(), } }
fn send(self, object: &session_core_session::SessionCoreSession) { match self { Self::Close => object.send_terminate(), Self::Kill => object.send_kill(), Self::Lock => object.send_lock(), Self::Unlock => object.send_unlock(), } }}
#[allow(clippy::too_many_lines)]fn main() -> anyhow::Result<()> { let cli = Cli::parse();
if let Command::Completions { shell } = cli.command { let mut command = Cli::command(); shell.generate(&mut command); return Ok(()); }
let mut state = State { sessions: Vec::new(), done: false, error: None, };
let mut socket = client::Client::connect("/run/sessiond.sock")?; let event_queue = socket.new_event_queue();
socket.add_implementation::<session_core_v1::SessionCoreV1Impl>();
event_queue.wait_for_handshake(&mut state)?;
let _manager = socket.bind::<session_core_manager::SessionCoreManager, _>(&event_queue, &mut state, 1)?;
event_queue.roundtrip(&mut state)?;
while state.sessions.iter().any(|session| !session.done) { event_queue.dispatch_events(&mut state, true)?; }
if let Some(err) = state.error.take() { anyhow::bail!("{err}"); }
let Command::Session(command) = &cli.command else { unreachable!(); };
match command { SessionCommand::List => { if state.sessions.is_empty() { println!("No active sessions."); return Ok(()); }
let fmt_state = |s: &session_core_v1::ActiveState| -> &'static str { match s { session_core_v1::ActiveState::Online => "online", session_core_v1::ActiveState::Active => "active", session_core_v1::ActiveState::Closing => "closing", session_core_v1::ActiveState::Opening => "opening", } };
let fmt_ts = |ts: u32| -> String { use std::time::{Duration, UNIX_EPOCH}; let Some(t) = UNIX_EPOCH.checked_add(Duration::from_secs(u64::from(ts))) else { return String::from("?"); }; let Ok(elapsed) = t.elapsed() else { return String::from("?"); }; let s = elapsed.as_secs(); let h = s / 3600; let m = (s % 3600) / 60; let s = s % 60; if h > 0 { format!("{h}h {m}min ago") } else if m > 0 { format!("{m}min {s}s ago") } else { format!("{s}s ago") } };
let id_w = state .sessions .iter() .map(|s| s.id.to_string().len()) .max() .unwrap_or(0) .max(2); let uid_w = state .sessions .iter() .map(|s| s.uid.to_string().len()) .max() .unwrap_or(0) .max(3); let user_w = state .sessions .iter() .map(|s| s.username.len()) .max() .unwrap_or(0) .max(4); let vt_w = state .sessions .iter() .map(|s| format!("tty{}", s.vt).len()) .max() .unwrap_or(0) .max(3); let seat_w = state .sessions .iter() .map(|s| s.seat.as_deref().unwrap_or("-").len()) .max() .unwrap_or(0) .max(4); let leader_w = state .sessions .iter() .map(|s| s.leader.to_string().len()) .max() .unwrap_or(0) .max(6); let since_w = state .sessions .iter() .map(|s| fmt_ts(s.timestamp).len()) .max() .unwrap_or(0) .max(5); let state_w = state .sessions .iter() .map(|s| fmt_state(&s.active).len()) .max() .unwrap_or(0) .max(5);
println!( " {:<id_w$} {:<uid_w$} {:<user_w$} {:<vt_w$} {:<seat_w$} {:<leader_w$} {:<since_w$} {:<state_w$}", "ID", "UID", "USER", "TTY", "SEAT", "LEADER", "SINCE", "STATE", ); println!( " {}", "─".repeat( id_w + 2 + uid_w + 2 + user_w + 2 + vt_w + 2 + seat_w + 2 + leader_w + 2 + since_w + 2 + state_w ) ); for session in state.sessions { println!( " {:<id_w$} {:<uid_w$} {:<user_w$} {:<vt_w$} {:<seat_w$} {:<leader_w$} {:<since_w$} {:<state_w$}", session.id, session.uid, session.username, format!("tty{}", session.vt), session.seat.as_deref().unwrap_or("-"), session.leader, fmt_ts(session.timestamp), fmt_state(&session.active), ); } } command @ (SessionCommand::Close { .. } | SessionCommand::Kill { .. } | SessionCommand::Lock { .. } | SessionCommand::Unlock { .. }) => { let Some(action) = SessionAction::from_command(command) else { unreachable!(); }; let Some(ids) = SessionAction::ids(command) else { unreachable!(); };
if SessionAction::targets_current(command) { let uid = process::getuid();
let session = state .sessions .iter() .find(|session| session.uid == uid.as_raw()) .context(format!( "{} Session for uid {} not found, check if pam module is installed", "error:".red(), uid.as_raw() ))?; action.send(&session.object);
return Ok(()); }
let mut unmatched = Vec::new(); let mut matched = Vec::new();
for id in ids { if let Some(session) = state.sessions.iter().find(|session| session.id == *id) { matched.push(session.object.clone()); } else { unmatched.push(id); } }
if !unmatched.is_empty() { let ids = unmatched .iter() .map(std::string::ToString::to_string) .collect::<Vec<_>>() .join(", "); eprintln!("{} unknown session ids: {ids}", "error:".red()); return Ok(()); }
for obj in matched { action.send(&obj); }
while !state.done { event_queue.dispatch_events(&mut state, true)?; }
if let Some(err) = state.error { eprintln!("{} {err}", "error:".red()); } } }
Ok(())}