jay-compositor 1.15.0

The Jay compositor
use crate::compositor::LogLevel;
use indexmap::IndexMap;
use log::Level;
use regex::Captures;
use regex::Regex;
use regex::Replacer;
use std::env;
use std::fmt;
use std::fmt::Display;
use std::fs::read_to_string;
use std::ops::Deref;
use std::path::Path;
use std::sync::LazyLock;
use std::sync::OnceLock;

pub static CONFIG_DIR: OnceLock<Option<String>> = OnceLock::new();

pub fn config_dir() -> Option<&'static str> {
    macro_rules! declare_name {
        ($name:ident) => {
            const $name: &str = stringify!($name);
        };
    }
    declare_name!(JAY_CONFIG_DIR);
    declare_name!(HOME);
    declare_name!(XDG_CONFIG_HOME);

    CONFIG_DIR
        .get_or_init(|| {
            if let Ok(dir) = env::var(JAY_CONFIG_DIR) {
                Some(dir)
            } else if let Ok(xdg) = env::var(XDG_CONFIG_HOME) {
                Some(format!("{xdg}/jay"))
            } else if let Ok(home) = env::var(HOME) {
                Some(format!("{home}/.config/jay"))
            } else {
                eprintln!(
                    "None of {JAY_CONFIG_DIR}, {XDG_CONFIG_HOME}, {HOME} are set. Cannot determine config dir.",
                );
                None
            }
        })
        .as_deref()
}

pub static INITIAL_LOG_LEVEL: OnceLock<LogLevel> = OnceLock::new();

pub fn initial_log_level() -> LogLevel {
    *INITIAL_LOG_LEVEL.get_or_init(|| {
        JAY_LOG_LEVEL
            .map(|l| match l {
                "trace" => LogLevel::Trace,
                "debug" => LogLevel::Debug,
                "warn" => LogLevel::Warn,
                "error" => LogLevel::Error,
                "off" => LogLevel::Off,
                _ => LogLevel::Info,
            })
            .unwrap_or_default()
    })
}

fn parse_env(env: &str) -> IndexMap<String, String> {
    let def_regex = Regex::new(
        r#"(?xm)
                ^
                \s*
                (?:export\s+)?
                (?<key>[\w.-]+)
                (?:\s*=\s*?|:\s+?)
                (?<value>
                        \s*'(?:\\'|[^'])*'
                    |   \s*"(?:\\"|[^"])*"
                    |   \s*`(?:\\`|[^`])*`
                    |   [^\#\r\n]+
                )?
                \s*
                (?:\#.*)?
                $
            "#,
    )
    .unwrap();
    let string_regex = Regex::new(
        r#"(?x)
                ^
                (?:
                        '(.*)'
                    |   `(.*)`
                    |   "(.*)"
                )
                $
            "#,
    )
    .unwrap();
    let escape_regex = Regex::new(r#"\\(?<char>.)"#).unwrap();

    fn cod<'h>(c: &Captures<'h>, name: &str) -> &'h str {
        c.name(name).map(|v| v.as_str()).unwrap_or_default()
    }
    struct ValueReplacer<'a>(&'a Regex);
    impl Replacer for ValueReplacer<'_> {
        fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
            if let Some(v) = caps.get(1).or_else(|| caps.get(2)) {
                dst.push_str(v.as_str());
            } else if let Some(v) = caps.get(3) {
                let v = self.0.replace_all(v.as_str(), StringReplacer);
                dst.push_str(&v);
            }
        }
    }
    struct StringReplacer;
    impl Replacer for StringReplacer {
        fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
            let c = cod(caps, "char");
            let c = if c == "n" {
                "\n"
            } else if c == "r" {
                "\r"
            } else {
                c
            };
            dst.push_str(c);
        }
    }
    let mut res = IndexMap::default();
    for m in def_regex.captures_iter(env) {
        let key = cod(&m, "key");
        let value = cod(&m, "value");
        let value = string_regex.replace(value.trim(), ValueReplacer(&escape_regex));
        res.insert(key.to_string(), value.into_owned());
    }
    res
}

static JAY_ENV: LazyLock<IndexMap<String, String>> = LazyLock::new(|| {
    if let Some(dir) = config_dir()
        && let file = Path::new(dir).join("jay.env")
        && let Ok(v) = read_to_string(&file)
    {
        return parse_env(&v);
    }
    Default::default()
});

pub fn log_jay_env() {
    const LEVEL: Level = Level::Debug;
    if log::log_enabled!(LEVEL) {
        for (k, v) in &*JAY_ENV {
            log::log!(LEVEL, "jay.env: {k}={v}");
        }
    }
}

#[test]
fn test_parse_env() {
    let _ = parse_env("");
}

macro_rules! declare {
    ($name:ident: $ty:ty, $(@default = $default:expr,)? $map:expr $(,)?) => {
        #[allow(non_camel_case_types)]
        pub struct $name;

        const _: () = {
            const NAME: &str = stringify!($name);

            static ENV: LazyLock<Option<String>> = LazyLock::new(|| env::var(NAME).ok());

            static RAW: LazyLock<Option<&'static str>> = LazyLock::new(|| {
                ENV.as_deref()
                    .or_else(|| JAY_ENV.get(NAME).map(|v| v.as_str()))
            });

            impl $name {
                #[allow(dead_code)]
                pub fn name(self) -> &'static str {
                    NAME
                }

                #[allow(dead_code)]
                pub fn as_env(self) -> impl Display {
                    fmt::from_fn(|f| {
                        f.write_str(NAME)?;
                        f.write_str("=")?;
                        if let Some(v) = *RAW {
                            f.write_str(v)?;
                        }
                        Ok(())
                    })
                }
            }

            impl Deref for $name {
                type Target = $ty;

                fn deref(&self) -> &Self::Target {

                    static COOKED: LazyLock<$ty> = {
                        LazyLock::new(|| {
                            RAW.map($map)
                            $(
                                .unwrap_or($default)
                            )?
                        })
                    };
                    &*COOKED
                }
            }
        };
    };
}

macro_rules! declare_str {
    ($name:ident) => {
        declare!($name: Option<&'static str>, |v| v);
    };
}

macro_rules! declare_bool {
    ($name:ident) => {
        declare!($name: bool, @default = false, |v| {
            v == "1" ||
            v.eq_ignore_ascii_case("true") ||
            v.eq_ignore_ascii_case("on") ||
            v.eq_ignore_ascii_case("yes")
        });
    };
}

declare_bool!(JAY_MCM_AMD_ALLOW_CURSOR);
declare_bool!(JAY_MCM_AMD_USE_FIRST_LUT);
declare_bool!(JAY_MCM_NVIDIA_USE_LUTS);
declare_bool!(JAY_NO_CLIENT_PRIME);
declare_bool!(JAY_NO_DESCRIPTOR_HEAP);
declare_bool!(JAY_NO_REALTIME);
declare_bool!(JAY_VULKAN_VALIDATION);
declare_str!(DBUS_SESSION_BUS_ADDRESS);
declare_str!(DISPLAY);
declare_str!(HOME);
declare_str!(JAY_LOG_LEVEL);
declare_str!(JAY_PRIME_METHODS);
declare_str!(WAYLAND_DISPLAY);
declare_str!(XAUTHORITY);
declare_str!(XCURSOR_PATH);
declare_str!(XCURSOR_SIZE);
declare_str!(XCURSOR_THEME);
declare_str!(XDG_RUNTIME_DIR);
declare_str!(XDG_SESSION_ID);