mkdev 3.5.0

Save your boilerplate instead of writing it.
// mkdev - Save your boilerplate instead of writing it
// Copyright (C) 2026  James C. Craven <4jamesccraven@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//! Data type that represents a programming language or file format.
//!
//! Interfaces with hyperpolyglot to store the name and colour of a programming language.
use std::fmt::Display;

use colored::Colorize;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Language {
    pub name: String,
    pub colour: Option<(u8, u8, u8)>,
}

impl Display for Language {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.colour {
            Some((r, g, b)) => write!(f, "{}", self.name.truecolor(r, g, b)),
            None => write!(f, "{}", self.name),
        }
    }
}

impl From<hyperpolyglot::Language> for Language {
    fn from(value: hyperpolyglot::Language) -> Self {
        let name = value.name.to_string();

        let colour = value.color.and_then(|s| {
            let s = s.strip_prefix('#')?;
            Some((
                u8::from_str_radix(&s[0..2], 16).ok()?,
                u8::from_str_radix(&s[2..4], 16).ok()?,
                u8::from_str_radix(&s[4..6], 16).ok()?,
            ))
        });

        Language { name, colour }
    }
}

/// Backwards compatibility for V1 Language Strings.
///
/// This `From` implementation attempts to parse out a colour if it can, or just uses the given
/// string as a name and sets colour to none if it can't.
impl From<&str> for Language {
    fn from(value: &str) -> Self {
        let mut name = value;
        let colour: Option<(u8, u8, u8)> = (|| {
            let s = value.strip_prefix("\x1b[38;2;")?;

            let (r, s) = s.split_once(';')?;
            let (g, s) = s.split_once(';')?;
            let (b, s) = s.split_once('m')?;

            #[rustfmt::skip]
            let col = (
                r.parse().ok()?,
                g.parse().ok()?,
                b.parse().ok()?
            );

            name = s.strip_suffix("\x1b[0m")?;

            Some(col)
        })();
        let name = name.to_string();

        Language { name, colour }
    }
}

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

    #[test]
    fn test_language_from_ansi_string() {
        let lang = Language::from("\x1b[38;2;255;100;0mRust\x1b[0m");
        assert_eq!(lang.name, "Rust");
        assert_eq!(lang.colour, Some((255, 100, 0)));
    }

    #[test]
    fn test_language_from_plain_string() {
        let lang = Language::from("Rust");
        assert_eq!(lang.name, "Rust");
        assert_eq!(lang.colour, None);
    }

    #[test]
    fn test_language_from_malformed_ansi_string() {
        let lang = Language::from("\x1b[38;2;999;0;0mRust\x1b[0m");
        assert_eq!(lang.name, "\x1b[38;2;999;0;0mRust\x1b[0m");
        assert_eq!(lang.colour, None);
    }
}