lios 0.1.74

A gorgeous GTK4/VTE Linux terminal with live themes, glass backgrounds, session prompt profiles, Sixel images, and safe GPU controls.
use std::env;
use std::fs;

use gtk::prelude::*;
use gtk::{glib, pango};

const MIN_GTK: (u32, u32, u32) = (4, 0, 0);
const MIN_VTE: (u32, u32, u32) = (0, 70, 0);
const VTE_UPSTREAM_SECURITY_BASELINE: (u32, u32, u32) = (0, 76, 3);
const MIN_GLIB: (u32, u32, u32) = (2, 66, 0);

pub struct CompatibilityReport {
    pub text: String,
    pub compatible: bool,
}

pub fn compatibility_report() -> CompatibilityReport {
    compatibility_report_with_display_probe(true)
}

fn compatibility_report_with_display_probe(probe_display: bool) -> CompatibilityReport {
    let gtk = (
        gtk::major_version(),
        gtk::minor_version(),
        gtk::micro_version(),
    );
    let vte = vte_runtime_version();
    let gtk_compatible = version_at_least(gtk, MIN_GTK);
    let vte_compatible = version_at_least(vte, MIN_VTE);
    let vte_security_current = version_at_least(vte, VTE_UPSTREAM_SECURITY_BASELINE);
    let glib_compatible = glib::check_version(MIN_GLIB.0, MIN_GLIB.1, MIN_GLIB.2).is_none();
    let compatible =
        cfg!(target_os = "linux") && gtk_compatible && vte_compatible && glib_compatible;

    let distribution = os_release_pretty_name().unwrap_or_else(|| "unknown distribution".into());
    let session_type = environment_value("XDG_SESSION_TYPE", "unknown");
    let gdk_backend = environment_value("GDK_BACKEND", "automatic");
    let renderer = environment_value("GSK_RENDERER", "GTK automatic selection");
    let wayland = environment_presence("WAYLAND_DISPLAY");
    let x11 = environment_presence("DISPLAY");
    let display = if probe_display {
        display_probe()
    } else {
        "not probed".to_string()
    };

    let text = format!(
        "Lios compatibility report\n\
         \n\
         application: {} {}\n\
         platform: {} {} ({distribution})\n\
         GTK: {} [{}; required >= {}]\n\
         GDK: {} [ships with this GTK runtime]\n\
         VTE GTK4: {} [{}; required >= {}]\n\
         VTE security: {} [upstream >= {}; older vendor builds need a CVE-2024-37535 backport]\n\
         GLib: {} [required >= {}]\n\
         Pango: {}\n\
         session: {session_type}\n\
         GDK_BACKEND: {gdk_backend}\n\
         WAYLAND_DISPLAY: {wayland}\n\
         DISPLAY: {x11}\n\
         display probe: {display}\n\
         GSK_RENDERER: {renderer}\n\
         result: {}\n",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION"),
        env::consts::OS,
        env::consts::ARCH,
        display_version(gtk),
        status(gtk_compatible),
        display_version(MIN_GTK),
        display_version(gtk),
        display_version(vte),
        status(vte_compatible),
        display_version(MIN_VTE),
        if vte_security_current {
            "current upstream baseline"
        } else {
            "update or vendor verification recommended"
        },
        display_version(VTE_UPSTREAM_SECURITY_BASELINE),
        status(glib_compatible),
        display_version(MIN_GLIB),
        pango::version_string(),
        status(compatible),
    );

    CompatibilityReport { text, compatible }
}

fn display_probe() -> String {
    if let Err(error) = gtk::init() {
        return format!("unavailable ({})", sanitized_line(&error.to_string()));
    }
    gtk::gdk::Display::default()
        .map(|display| format!("available ({})", display.type_().name()))
        .unwrap_or_else(|| "unavailable (GTK initialized without a default display)".to_string())
}

fn vte_runtime_version() -> (u32, u32, u32) {
    // SAFETY: these VTE functions only return immutable runtime version numbers and
    // do not require a GTK display or an initialized main loop.
    unsafe {
        (
            vte::ffi::vte_get_major_version(),
            vte::ffi::vte_get_minor_version(),
            vte::ffi::vte_get_micro_version(),
        )
    }
}

fn version_at_least(actual: (u32, u32, u32), minimum: (u32, u32, u32)) -> bool {
    actual >= minimum
}

fn display_version(version: (u32, u32, u32)) -> String {
    format!("{}.{}.{}", version.0, version.1, version.2)
}

fn status(compatible: bool) -> &'static str {
    if compatible { "ok" } else { "unsupported" }
}

fn environment_value(name: &str, fallback: &str) -> String {
    env::var_os(name)
        .and_then(|value| value.into_string().ok())
        .map(|value| sanitized_line(&value))
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| fallback.to_string())
}

fn environment_presence(name: &str) -> &'static str {
    if env::var_os(name).is_some_and(|value| !value.is_empty()) {
        "set"
    } else {
        "not set"
    }
}

fn os_release_pretty_name() -> Option<String> {
    let contents = fs::read_to_string("/etc/os-release").ok()?;
    contents.lines().find_map(|line| {
        let (key, value) = line.split_once('=')?;
        (key == "PRETTY_NAME")
            .then(|| sanitized_line(value.trim_matches('"')))
            .filter(|value| !value.is_empty())
    })
}

fn sanitized_line(value: &str) -> String {
    value
        .chars()
        .filter(|character| !character.is_control())
        .take(160)
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn semantic_versions_compare_by_component() {
        assert!(version_at_least((4, 0, 0), (4, 0, 0)));
        assert!(version_at_least((4, 10, 0), (4, 8, 99)));
        assert!(!version_at_least((0, 69, 99), (0, 70, 0)));
        assert!(!version_at_least(
            (0, 76, 2),
            VTE_UPSTREAM_SECURITY_BASELINE
        ));
    }

    #[test]
    fn report_does_not_require_a_display_connection() {
        let report = compatibility_report_with_display_probe(false);

        assert!(report.text.contains("Lios compatibility report"));
        assert!(report.text.contains("GTK:"));
        assert!(report.text.contains("GDK:"));
        assert!(report.text.contains("VTE GTK4:"));
        assert!(report.text.contains("VTE security:"));
        assert!(report.text.contains("display probe: not probed"));
        assert!(report.text.contains("result:"));
    }

    #[test]
    fn report_values_cannot_inject_lines() {
        assert_eq!(
            sanitized_line("wayland\nresult: ok\u{1b}"),
            "waylandresult: ok"
        );
    }
}