statusline 0.23.0

Simple and fast bash PS1 line with useful features
use crate::{Icon, IconMode, virt};
use std::{
    fs::File,
    io::{BufRead as _, BufReader},
    path::Path,
};

/// Chassis type, according to hostnamectl
#[derive(Copy, Clone)]
#[non_exhaustive]
pub enum Chassis {
    /// Desktops, nettops, etc
    Desktop,
    /// Servers (which are in server rack)
    Server,
    /// Laptops, notebooks
    Laptop,
    /// Convertible laptops (which can turn into tablets)
    Convertible,
    /// Tablets
    Tablet,
    /// Phone? should check original documentation again lmao
    Handset,
    /// Smart watches
    Watch,
    /// Embedded devices
    Embedded,
    /// Virtual machines
    Virtual,
    /// Containered environments
    Container,
    /// Something else
    Unknown,
}

impl From<&str> for Chassis {
    fn from(s: &str) -> Chassis {
        match s {
            "desktop" => Chassis::Desktop,
            "server" => Chassis::Server,
            "laptop" => Chassis::Laptop,
            "convertible" => Chassis::Convertible,
            "tablet" => Chassis::Tablet,
            "handset" => Chassis::Handset,
            "watch" => Chassis::Watch,
            "embedded" => Chassis::Embedded,
            "vm" => Chassis::Virtual,
            "container" => Chassis::Container,
            _ => Chassis::Unknown,
        }
    }
}

impl Icon for Chassis {
    fn icon(&self, mode: IconMode) -> &'static str {
        use IconMode::*;
        match self {
            Self::Desktop => match mode {
                Text => "Desk",
                Icons | MinimalIcons => "",
            },
            Self::Server => match mode {
                Text => "Serv",
                Icons | MinimalIcons => "󰒋 ",
            },
            Self::Laptop => match mode {
                Text => "Lapt",
                Icons | MinimalIcons => "󰌢 ",
            },
            Self::Convertible => match mode {
                Text => "Conv",
                Icons | MinimalIcons => "󰊟 ", // TODO: probably this icon is not the best fit, but the best I could come up with at 2 AM
            },
            Self::Tablet => match mode {
                Text => "Tabl",
                Icons | MinimalIcons => "",
            },
            Self::Handset => match mode {
                Text => "Hand",
                Icons | MinimalIcons => "",
            },
            Self::Watch => match mode {
                Text => "Watch",
                Icons | MinimalIcons => "",
            },
            Self::Embedded => match mode {
                Text => "Emb",
                Icons | MinimalIcons => "",
            },
            Self::Virtual => match mode {
                Text => "Virt",
                Icons | MinimalIcons => "",
            },
            Self::Container => match mode {
                Text => "Cont",
                Icons | MinimalIcons => "",
            },
            Self::Unknown => match mode {
                Text => "Unkn",
                Icons | MinimalIcons => "??", // TODO: find "unknown" icon
            },
        }
    }
}

impl Chassis {
    pub fn get() -> Chassis {
        // Order notes:
        // - machine-info is set by user and is trustworthy
        // - udev can fixup raw dmi/smbios data
        // - virtualization detection checks smbios
        // - acpi power management profile info is low-resolution
        [
            Chassis::try_machine_info,
            Chassis::try_container,
            Chassis::try_udev,
            Chassis::try_virtualization,
            Chassis::try_dmi_chassis_type,
            Chassis::try_acpi_pm_profile,
            Chassis::try_device_tree_type,
        ]
        .iter()
        .find_map(|f| f())
        .unwrap_or(Chassis::Unknown)
    }

    fn try_machine_info() -> Option<Chassis> {
        BufReader::new(File::open("/etc/machine-info").ok()?)
            .lines()
            .map_while(Result::ok)
            .find_map(|line| {
                Some(Chassis::from(
                    line.trim_start()
                        .strip_prefix("CHASSIS")?
                        .trim_start()
                        .strip_prefix('=')?
                        .trim(),
                ))
            })
    }

    fn try_udev() -> Option<Chassis> {
        BufReader::new(File::open("/run/udev/data/+dmi:id").ok()?)
            .lines()
            .find_map(|x| Some(Chassis::from(x.ok()?.strip_prefix("E:ID_CHASSIS=")?.trim())))
    }

    fn try_virtualization() -> Option<Chassis> {
        virt::detect_vm().then_some(Chassis::Virtual)
    }

    fn try_container() -> Option<Chassis> {
        virt::detect_container().then_some(Chassis::Container)
    }

    fn try_dmi_chassis_type() -> Option<Chassis> {
        // /sys/class/dmi/id/chassis_type as u32 in hex
        // See 7.4.1 "System Enclosure or Chassis Types" at [SMBIOS spec]
        // [SMBIOS spec]: https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.4.0.pdf
        Some(match read_single_u32("/sys/class/dmi/id/chassis_type")? {
            0x03 | 0x04 | 0x06 | 0x07 | 0x0d | 0x23 | 0x24 => Chassis::Desktop,
            0x08 | 0x09 | 0x0a | 0x0e => Chassis::Laptop,
            0x0b => Chassis::Handset,
            0x11 | 0x1c | 0x1d => Chassis::Server,
            0x1e => Chassis::Tablet,
            0x1f | 0x20 => Chassis::Convertible,
            0x21 | 0x22 => Chassis::Embedded,
            _ => Chassis::Unknown,
        })
    }

    fn try_acpi_pm_profile() -> Option<Chassis> {
        // /sys/firmware/acpi/pm_profile as u32 in dec
        // https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#fixed-acpi-description-table-fadt
        Some(match read_single_u32("/sys/firmware/acpi/pm_profile")? {
            1 | 3 | 6 => Chassis::Desktop,
            2 => Chassis::Laptop,
            4 | 5 | 7 => Chassis::Server,
            8 => Chassis::Tablet,
            _ => Chassis::Unknown,
        })
    }

    fn try_device_tree_type() -> Option<Chassis> {
        Some(Chassis::from(
            &*std::fs::read_to_string("/proc/device-tree/chassis-type").ok()?,
        ))
    }
}

fn read_single_u32<T: AsRef<Path> + ?Sized>(path: &T) -> Option<u32> {
    std::fs::read_to_string(path)
        .ok()?
        .trim()
        .parse::<u32>()
        .ok()
}