feat: mirada standalone — compositor Wayland + WM sobre Llimphi (build magro)
Stack de display extraído del monorepo: compositor teselante (Cuerpo smithay + Cerebro WM agnóstico), greeter PAM, portal XDG, CLI de control. Llimphi se consume por git desde su repo publicado; las hojas compartidas (format, auth-core, rimay-localize, wawa-config, app-bus) y el widget menubar van vendorizados. Sin el asistente IA (pluma-llm) ni la barra web wasm — el compositor no los necesita. cargo check --workspace pasa (18 crates, 0 warn). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
//! El tiquet de sesión: lo que el greeter le entrega al compositor tras
|
||||
//! una autenticación exitosa.
|
||||
//!
|
||||
//! El greeter de carmen corre como proceso hijo del compositor. Cuando
|
||||
//! el login tiene éxito, imprime **una línea** de tiquet a su stdout; el
|
||||
//! compositor escanea las líneas del hijo buscando el prefijo
|
||||
//! [`TICKET_TAG`] y, al encontrarlo, hace el traspaso a modo sesión.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::UserInfo;
|
||||
|
||||
/// Etiqueta + versión de la línea de tiquet. El compositor sólo trata
|
||||
/// como tiquet las líneas que empiezan con esto — el resto del stdout
|
||||
/// del greeter (logs, ruido) se ignora.
|
||||
pub const TICKET_TAG: &str = "MIRADA-SESSION-TICKET-v1";
|
||||
|
||||
/// Resultado de un login: la identidad autenticada más, opcionalmente,
|
||||
/// el comando de sesión elegido. El greeter lo produce; el compositor lo
|
||||
/// consume para arrancar la sesión (setuid al usuario + spawn).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SessionTicket {
|
||||
/// Identidad del usuario autenticado.
|
||||
pub user: UserInfo,
|
||||
/// Comando de sesión a ejecutar como el usuario. Vacío = que el
|
||||
/// compositor decida (su autostart por defecto).
|
||||
pub session: String,
|
||||
/// `true` si la sesión es un compositor **ajeno** (sway, Plasma…): el
|
||||
/// servidor actual debe soltar el DRM y hacer `exec`, no correrla como
|
||||
/// cliente. `false` para sesiones nativas de mirada (pata, autostart),
|
||||
/// que sí corren como clientes del mismo compositor.
|
||||
pub foreign: bool,
|
||||
}
|
||||
|
||||
impl SessionTicket {
|
||||
/// Crea un tiquet sin comando de sesión explícito.
|
||||
pub fn new(user: UserInfo) -> Self {
|
||||
Self {
|
||||
user,
|
||||
session: String::new(),
|
||||
foreign: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fija el comando de sesión. Encadenable.
|
||||
pub fn with_session(mut self, session: impl Into<String>) -> Self {
|
||||
self.session = session.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Marca la sesión como compositor ajeno (handoff por `exec`).
|
||||
/// Encadenable.
|
||||
pub fn foreign(mut self, foreign: bool) -> Self {
|
||||
self.foreign = foreign;
|
||||
self
|
||||
}
|
||||
|
||||
/// Serializa el tiquet a una línea única, apta para stdout. Campos
|
||||
/// separados por tabulador: ni los nombres de usuario, ni los paths,
|
||||
/// ni los comandos de sesión suelen contener tabuladores.
|
||||
pub fn to_line(&self) -> String {
|
||||
format!(
|
||||
"{TICKET_TAG}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
|
||||
self.user.name,
|
||||
self.user.uid,
|
||||
self.user.gid,
|
||||
self.user.home.display(),
|
||||
self.user.shell.display(),
|
||||
self.session,
|
||||
if self.foreign { "1" } else { "0" },
|
||||
)
|
||||
}
|
||||
|
||||
/// Parsea una línea producida por [`to_line`]. `None` si la línea no
|
||||
/// es un tiquet (otra salida del greeter) o está malformada.
|
||||
pub fn from_line(line: &str) -> Option<SessionTicket> {
|
||||
let mut f = line.trim_end_matches(['\r', '\n']).split('\t');
|
||||
if f.next()? != TICKET_TAG {
|
||||
return None;
|
||||
}
|
||||
let name = f.next()?.to_string();
|
||||
let uid = f.next()?.parse().ok()?;
|
||||
let gid = f.next()?.parse().ok()?;
|
||||
let home = PathBuf::from(f.next()?);
|
||||
let shell = PathBuf::from(f.next()?);
|
||||
// El comando de sesión puede venir vacío.
|
||||
let session = f.next().unwrap_or("").to_string();
|
||||
// El flag `foreign` es opcional (tiquets viejos no lo traen).
|
||||
let foreign = matches!(f.next(), Some("1"));
|
||||
Some(SessionTicket {
|
||||
user: UserInfo {
|
||||
name,
|
||||
uid,
|
||||
gid,
|
||||
home,
|
||||
shell,
|
||||
},
|
||||
session,
|
||||
foreign,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample() -> UserInfo {
|
||||
UserInfo {
|
||||
name: "sergio".into(),
|
||||
uid: 1000,
|
||||
gid: 1000,
|
||||
home: PathBuf::from("/home/sergio"),
|
||||
shell: PathBuf::from("/usr/bin/bash"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_without_session() {
|
||||
let t = SessionTicket::new(sample());
|
||||
let back = SessionTicket::from_line(&t.to_line()).expect("parsea");
|
||||
assert_eq!(back, t);
|
||||
assert!(back.session.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_with_session() {
|
||||
let t = SessionTicket::new(sample()).with_session("shuma-shell --launcher");
|
||||
let back = SessionTicket::from_line(&t.to_line()).expect("parsea");
|
||||
assert_eq!(back, t);
|
||||
assert_eq!(back.session, "shuma-shell --launcher");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_with_foreign() {
|
||||
let t = SessionTicket::new(sample())
|
||||
.with_session("sway")
|
||||
.foreign(true);
|
||||
let back = SessionTicket::from_line(&t.to_line()).expect("parsea");
|
||||
assert_eq!(back, t);
|
||||
assert!(back.foreign);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreign_defaults_false_sin_campo() {
|
||||
// Una línea estilo v1 (sin el campo foreign) parsea con foreign=false.
|
||||
let line = format!("{TICKET_TAG}\tsergio\t1000\t1000\t/home/sergio\t/usr/bin/bash\tsway");
|
||||
let back = SessionTicket::from_line(&line).expect("parsea");
|
||||
assert!(!back.foreign);
|
||||
assert_eq!(back.session, "sway");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_line_ignores_non_ticket() {
|
||||
assert!(SessionTicket::from_line("[INFO] arrancando greeter").is_none());
|
||||
assert!(SessionTicket::from_line("").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_line_rejects_malformed() {
|
||||
// Prefijo correcto pero faltan campos.
|
||||
assert!(SessionTicket::from_line(&format!("{TICKET_TAG}\tsergio")).is_none());
|
||||
// uid no numérico.
|
||||
assert!(
|
||||
SessionTicket::from_line(&format!("{TICKET_TAG}\tsergio\tXX\t1000\t/h\t/sh\t"))
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tolerates_trailing_newline() {
|
||||
let line = format!("{}\n", SessionTicket::new(sample()).to_line());
|
||||
assert!(SessionTicket::from_line(&line).is_some());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user