feat: ship Rust login manager and GitLab release pipeline

Replace the legacy Go implementation as the production path, quarantine the reference tree, add deterministic release packaging and version metadata, and run verification through GitLab CI.
This commit is contained in:
2026-09-25 21:33:46 +05:30
parent 1723ab53b8
commit faf5b27328
49 changed files with 6795 additions and 747 deletions

View File

@@ -0,0 +1,267 @@
//! Low-cost terminal wallpaper and procedural ambient animation.
use std::{
fs,
path::Path,
time::{Duration, Instant},
};
use image::{ImageReader, RgbImage, imageops::FilterType};
use latchd_core::{
config::VisualConfig,
error::{Error, ResultExt},
};
use ratatui::{
Frame,
prelude::{Buffer, Color, Rect},
};
const MAX_IMAGE_SIDE: u32 = 480;
const MAX_SOURCE_SIDE: u32 = 8_192;
const MAX_SOURCE_PIXELS: u64 = 16_777_216;
const FRAME_INTERVAL: Duration = Duration::from_millis(120);
/// Background state owned by the TUI thread.
pub(crate) struct Background {
wallpaper: Option<RgbImage>,
automata: Automata,
enabled: bool,
animations: bool,
reduced_motion: bool,
last_tick: Instant,
}
impl Background {
/// Loads a bounded wallpaper image and prepares the procedural layer.
pub(crate) fn load(config: &VisualConfig) -> Result<Self, Error> {
let wallpaper = config
.wallpaper_path
.as_deref()
.map(|path| load_wallpaper(path, config.max_wallpaper_bytes))
.transpose()?;
Ok(Self {
wallpaper,
automata: Automata::default(),
enabled: config.background_enabled,
animations: config.animations,
reduced_motion: config.reduced_motion,
last_tick: Instant::now(),
})
}
pub(crate) fn tick_if_due(&mut self, now: Instant) -> bool {
if !self.enabled
|| !self.animations
|| self.reduced_motion
|| now.duration_since(self.last_tick) < FRAME_INTERVAL
{
return false;
}
self.automata.tick();
self.last_tick = now;
true
}
pub(crate) fn toggle_enabled(&mut self) -> bool {
self.enabled = !self.enabled;
self.enabled
}
pub(crate) fn toggle_motion(&mut self) -> bool {
self.reduced_motion = !self.reduced_motion;
self.reduced_motion
}
pub(crate) fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, ascii: bool, no_color: bool) {
if !self.enabled || area.width < 80 || area.width == 0 || area.height == 0 {
return;
}
self.automata
.resize(area.width as usize, area.height as usize);
let buffer = frame.buffer_mut();
draw_wallpaper(buffer, area, self.wallpaper.as_ref(), no_color);
draw_automata(buffer, area, &self.automata, ascii, no_color);
}
}
fn load_wallpaper(path: &Path, max_bytes: u64) -> Result<RgbImage, Error> {
let metadata =
fs::symlink_metadata(path).io_context(format!("inspect wallpaper {}", path.display()))?;
if !metadata.is_file() || metadata.uid() != 0 || metadata.mode() & 0o022 != 0 {
return Err(Error::Config(format!(
"wallpaper must be a root-owned, non-group-writable regular file: {}",
path.display()
)));
}
if metadata.len() > max_bytes {
return Err(Error::Config(format!(
"wallpaper exceeds visual.max_wallpaper_bytes: {}",
path.display()
)));
}
let reader = ImageReader::open(path)
.io_context(format!("open wallpaper {}", path.display()))?
.with_guessed_format()
.io_context(format!("inspect wallpaper format {}", path.display()))?;
let (width, height) = reader.into_dimensions().map_err(|error| {
Error::Config(format!(
"inspect wallpaper dimensions {}: {error}",
path.display()
))
})?;
if width == 0
|| height == 0
|| width > MAX_SOURCE_SIDE
|| height > MAX_SOURCE_SIDE
|| u64::from(width).saturating_mul(u64::from(height)) > MAX_SOURCE_PIXELS
{
return Err(Error::Config(format!(
"wallpaper dimensions exceed {MAX_SOURCE_SIDE}x{MAX_SOURCE_SIDE} or {MAX_SOURCE_PIXELS} pixels"
)));
}
let image = ImageReader::open(path)
.io_context(format!("open wallpaper {}", path.display()))?
.with_guessed_format()
.io_context(format!("inspect wallpaper format {}", path.display()))?
.decode()
.map_err(|error| Error::Config(format!("decode wallpaper {}: {error}", path.display())))?;
if image.width() == 0 || image.height() == 0 {
return Err(Error::Config(
"wallpaper dimensions must be non-zero".into(),
));
}
let image = image.resize(MAX_IMAGE_SIDE, MAX_IMAGE_SIDE, FilterType::Triangle);
Ok(image.to_rgb8())
}
fn draw_wallpaper(buffer: &mut Buffer, area: Rect, wallpaper: Option<&RgbImage>, no_color: bool) {
let Some(image) = wallpaper else { return };
if no_color {
return;
}
let width = image.width().max(1);
let height = image.height().max(1);
for y in 0..area.height {
for x in 0..area.width {
let source_x = (u32::from(x) * width / u32::from(area.width)).min(width - 1);
let source_y = (u32::from(y) * height / u32::from(area.height)).min(height - 1);
let pixel = image.get_pixel(source_x, source_y).0;
if let Some(cell) = buffer.cell_mut((area.x + x, area.y + y)) {
cell.set_symbol(" ");
cell.set_bg(Color::Rgb(pixel[0] / 3, pixel[1] / 3, pixel[2] / 3));
}
}
}
}
fn draw_automata(
buffer: &mut Buffer,
area: Rect,
automata: &Automata,
ascii: bool,
no_color: bool,
) {
for y in 0..area.height as usize {
for x in 0..area.width as usize {
if !automata.alive(x, y) {
continue;
}
let x = x as u16;
let y = y as u16;
if let Some(cell) = buffer.cell_mut((area.x + x, area.y + y)) {
cell.set_symbol(if ascii { "." } else { "·" });
if !no_color {
cell.set_fg(Color::Rgb(60, 100, 120));
}
}
}
}
}
#[derive(Default)]
struct Automata {
width: usize,
height: usize,
current: Vec<u8>,
next: Vec<u8>,
seed: u64,
}
impl Automata {
fn resize(&mut self, width: usize, height: usize) {
if self.width == width && self.height == height {
return;
}
self.width = width;
self.height = height;
self.current = vec![0; width.saturating_mul(height)];
self.next = vec![0; width.saturating_mul(height)];
self.seed = self.seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
for index in 0..self.current.len() {
self.current[index] = u8::from(self.random() % 11 == 0);
}
}
fn tick(&mut self) {
if self.width == 0 || self.height == 0 {
return;
}
for y in 0..self.height {
for x in 0..self.width {
let neighbors = self.neighbors(x, y);
let index = y * self.width + x;
self.next[index] =
u8::from(neighbors == 3 || (neighbors == 2 && self.current[index] == 1));
}
}
std::mem::swap(&mut self.current, &mut self.next);
}
fn neighbors(&self, x: usize, y: usize) -> u8 {
let mut count = 0;
for dy in [self.height.wrapping_sub(1), 0, 1] {
for dx in [self.width.wrapping_sub(1), 0, 1] {
if dx == 0 && dy == 0 {
continue;
}
let nx = (x + dx) % self.width;
let ny = (y + dy) % self.height;
count += self.current[ny * self.width + nx];
}
}
count
}
fn alive(&self, x: usize, y: usize) -> bool {
self.current
.get(y.saturating_mul(self.width).saturating_add(x))
.copied()
== Some(1)
}
fn random(&mut self) -> u64 {
let mut value = self.seed;
value ^= value << 13;
value ^= value >> 7;
value ^= value << 17;
self.seed = value;
value
}
}
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn automata_is_bounded_and_progresses() {
let mut automata = Automata::default();
automata.resize(8, 4);
assert_eq!(automata.current.len(), 32);
automata.tick();
assert_eq!(automata.current.len(), 32);
}
}