use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Mode {
Unknown = 0,
Present = 1,
Absent = 2,
Disabled = 3,
}
impl Mode {
#[inline]
fn from_u8(v: u8) -> Self {
match v {
1 => Self::Present,
2 => Self::Absent,
3 => Self::Disabled,
_ => Self::Unknown,
}
}
}
static STATE: AtomicU8 = AtomicU8::new(Mode::Unknown as u8);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigSetting {
Auto,
Off,
Require,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SkipConfigs {
Off = 0,
Auto = 1,
On = 2,
}
impl ConfigSetting {
fn parse(s: &str) -> Option<Self> {
match s {
"auto" | "" => Some(Self::Auto),
"off" | "false" | "no" | "0" => Some(Self::Off),
"require" | "on" | "true" | "yes" | "1" => Some(Self::Require),
_ => None,
}
}
}
impl SkipConfigs {
fn parse(s: &str) -> Option<Self> {
match s {
"off" | "false" | "no" | "0" | "" => Some(Self::Off),
"auto" => Some(Self::Auto),
"on" | "true" | "yes" | "1" => Some(Self::On),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Config {
pub daemon: ConfigSetting,
pub skip_configs: SkipConfigs,
}
pub fn read_config_full() -> Config {
let defaults = Config {
daemon: ConfigSetting::Auto,
skip_configs: SkipConfigs::Off,
};
let path = match config_file_path() {
Some(p) => p,
None => return defaults,
};
let body = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => return defaults,
};
let parsed = match body.parse::<toml::Table>() {
Ok(t) => t,
Err(e) => {
tracing::warn!(path = %path.display(), error = %e, "zshrs.toml: parse failed; using defaults");
return defaults;
}
};
let daemon = parsed
.get("daemon")
.and_then(|v| v.as_table())
.and_then(|t| t.get("enabled"))
.and_then(|v| v.as_str())
.map(|s| {
ConfigSetting::parse(s).unwrap_or_else(|| {
tracing::warn!(value = s, "zshrs.toml: [daemon].enabled invalid; using auto");
ConfigSetting::Auto
})
})
.unwrap_or(ConfigSetting::Auto);
let skip_configs = parsed
.get("shell")
.and_then(|v| v.as_table())
.and_then(|t| t.get("skip_configs"))
.and_then(|v| v.as_str())
.map(|s| {
SkipConfigs::parse(s).unwrap_or_else(|| {
tracing::warn!(value = s, "zshrs.toml: [shell].skip_configs invalid; using off");
SkipConfigs::Off
})
})
.unwrap_or(SkipConfigs::Off);
Config {
daemon,
skip_configs,
}
}
pub fn read_config() -> ConfigSetting {
read_config_full().daemon
}
pub fn read_log_directive() -> String {
const DEFAULT: &str = "info";
let path = match config_file_path() {
Some(p) => p,
None => return DEFAULT.into(),
};
let body = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => return DEFAULT.into(),
};
let parsed = match body.parse::<toml::Table>() {
Ok(t) => t,
Err(_) => return DEFAULT.into(),
};
parsed
.get("log")
.and_then(|v| v.as_table())
.and_then(|t| t.get("level"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
.unwrap_or_else(|| DEFAULT.into())
}
static SKIP_CONFIGS: AtomicU8 = AtomicU8::new(0);
static SHOULD_SKIP_CONFIGS: AtomicU8 = AtomicU8::new(0);
fn config_file_path() -> Option<std::path::PathBuf> {
let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
std::path::PathBuf::from(custom)
} else {
std::path::PathBuf::from(std::env::var_os("HOME")?).join(".zshrs")
};
Some(root.join("zshrs.toml"))
}
pub fn probe() -> Mode {
let cfg = read_config_full();
SKIP_CONFIGS.store(cfg.skip_configs as u8, Ordering::Relaxed);
match cfg.daemon {
ConfigSetting::Off => {
tracing::info!("daemon: disabled in config ([daemon] enabled = \"off\")");
STATE.store(Mode::Disabled as u8, Ordering::Relaxed);
SHOULD_SKIP_CONFIGS.store(0, Ordering::Relaxed);
return Mode::Disabled;
}
ConfigSetting::Auto | ConfigSetting::Require => {}
}
let alive = probe_socket();
let mode = if alive { Mode::Present } else { Mode::Absent };
STATE.store(mode as u8, Ordering::Relaxed);
if alive {
tracing::info!("daemon: present (socket reachable)");
} else {
match cfg.daemon {
ConfigSetting::Require => {
tracing::warn!(
"daemon: absent — config requires it but socket is not reachable. \
Start it via `zshrs-daemon`, `systemctl --user start zshrs-daemon`, \
`launchctl load ~/Library/LaunchAgents/com.menketechnologies.zshrs-daemon.plist`, \
or `brew services start zshrs`. Falling back to vanilla mode."
);
}
_ => {
tracing::info!(
"daemon: absent (socket not reachable) — running in vanilla zsh mode"
);
}
}
}
let should_skip = match cfg.skip_configs {
SkipConfigs::Off => false,
SkipConfigs::On => mode == Mode::Present,
SkipConfigs::Auto => mode == Mode::Present && daemon_has_zshrs_rows(),
};
SHOULD_SKIP_CONFIGS.store(if should_skip { 1 } else { 0 }, Ordering::Relaxed);
if should_skip {
tracing::info!(
"shell: skip_configs active — bypassing /etc/zshenv + ~/.{{zshenv,zprofile,zshrc,zlogin}} and \
applying canonical state from daemon"
);
} else if cfg.skip_configs != SkipConfigs::Off {
tracing::info!(
mode = ?cfg.skip_configs,
daemon = ?mode,
"shell: skip_configs configured but conditions not met — sourcing dotfiles normally"
);
}
mode
}
#[cfg(feature = "daemon")]
fn daemon_has_zshrs_rows() -> bool {
let paths = match crate::daemon::paths::CachePaths::resolve() {
Ok(p) => p,
Err(_) => return false,
};
let entries = match std::fs::read_dir(&paths.images) {
Ok(it) => it,
Err(_) => return false,
};
for entry in entries.flatten() {
if let Some(s) = entry.file_name().to_str() {
if s.ends_with("-recorder.rkyv") {
return true;
}
}
}
false
}
#[cfg(not(feature = "daemon"))]
fn daemon_has_zshrs_rows() -> bool {
false
}
#[inline]
pub fn should_skip_configs() -> bool {
SHOULD_SKIP_CONFIGS.load(Ordering::Relaxed) != 0
}
pub fn skip_configs_setting() -> SkipConfigs {
match SKIP_CONFIGS.load(Ordering::Relaxed) {
1 => SkipConfigs::Auto,
2 => SkipConfigs::On,
_ => SkipConfigs::Off,
}
}
#[cfg(feature = "daemon")]
fn probe_socket() -> bool {
match crate::daemon::paths::CachePaths::resolve() {
Ok(paths) => crate::daemon::client::Client::is_daemon_alive(&paths),
Err(_) => false,
}
}
#[cfg(not(feature = "daemon"))]
fn probe_socket() -> bool {
false
}
#[inline]
pub fn current() -> Mode {
Mode::from_u8(STATE.load(Ordering::Relaxed))
}
#[inline]
pub fn is_present() -> bool {
current() == Mode::Present
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_setting_parses_common_aliases() {
assert_eq!(ConfigSetting::parse("auto"), Some(ConfigSetting::Auto));
assert_eq!(ConfigSetting::parse(""), Some(ConfigSetting::Auto));
assert_eq!(ConfigSetting::parse("off"), Some(ConfigSetting::Off));
assert_eq!(ConfigSetting::parse("false"), Some(ConfigSetting::Off));
assert_eq!(ConfigSetting::parse("no"), Some(ConfigSetting::Off));
assert_eq!(ConfigSetting::parse("0"), Some(ConfigSetting::Off));
assert_eq!(ConfigSetting::parse("require"), Some(ConfigSetting::Require));
assert_eq!(ConfigSetting::parse("on"), Some(ConfigSetting::Require));
assert_eq!(ConfigSetting::parse("true"), Some(ConfigSetting::Require));
assert_eq!(ConfigSetting::parse("garbage"), None);
}
#[test]
fn mode_round_trips_through_atomic() {
for m in [Mode::Unknown, Mode::Present, Mode::Absent, Mode::Disabled] {
assert_eq!(Mode::from_u8(m as u8), m);
}
}
}