Skip to main content

zsh/extensions/
daemon_presence.rs

1//! Daemon-presence detection + per-user config knob.
2//!
3//! **zshrs-original infrastructure — no C source counterpart.** C
4//! zsh has no daemon process. Shell state lives in the running
5//! interpreter and re-initializes on every launch. zshrs ships
6//! `zshrs-daemon` as a separate binary that holds canonical
7//! parameter/option/function state in shared memory; the shell
8//! probes its socket at startup and decides whether to use the
9//! cached state or fall back to vanilla full-init mode.
10//!
11//! Per the `zshrs ↔ zshrs-daemon = independent binaries` rule
12//! (docs/DAEMON.md "Daemon lifecycle"), the shell does NOT spawn the
13//! daemon. It probes once at startup and runs in one of three modes:
14//!
15//! | Mode             | Trigger                                       |
16//! |------------------|-----------------------------------------------|
17//! | DaemonPresent    | socket alive, daemon answered handshake       |
18//! | DaemonAbsent     | no socket / connect refused; degraded vanilla |
19//! | DaemonDisabled   | user set `[daemon] enabled = "off"` in config |
20//!
21//! Without the daemon: zshrs runs as a Rust-fast vanilla zsh — no
22//! cache, no canonical state, every config re-evaluated per shell
23//! launch ("rebuilding your house every morning"). With the daemon:
24//! zshrs uses the cached canonical state for fast cold-start.
25//!
26//! The probe is single-shot at startup. Builtins / hooks check
27//! `is_present()` before calling into the daemon client; on absent,
28//! they noop or fall back to source-interp behavior.
29//!
30//! Config (`$ZSHRS_HOME/zshrs.toml` or `~/.zshrs/zshrs.toml`, all optional):
31//!
32//! ```toml
33//! [daemon]
34//! # "auto" (default) = probe at startup, use if alive
35//! # "off"            = never probe; pure vanilla zsh mode
36//! # "require"        = probe and warn if absent (no spawn either way)
37//! enabled = "auto"
38//!
39//! [shell]
40//! # "off"  (default) = always source .zshenv/.zprofile/.zshrc/.zlogin
41//! # "auto"           = if daemon is present + has zshrs rows, skip
42//! #                    every dotfile and apply canonical state
43//! #                    from the daemon instead. ~10ms cold-start.
44//! # "on"             = always skip dotfiles when the daemon is up;
45//! #                    don't even check for zshrs rows. Strict mode.
46//! skip_configs = "off"
47//!
48//! # Optional: zsh script sourced once after dotfiles / canonical_apply,
49//! # before compsys + first prompt (omit key for default: none).
50//! # Respects `-f` / `--no-rcs` (not sourced).
51//! # startup_config = "/path/to/init.zsh"
52//! ```
53//!
54//! Lives in `~/.zshrs/` alongside everything else (rkyv shards,
55//! catalog.db, daemon.sock, zshrs-daemon.toml, log, …) — single
56//! directory rule for all zshrs files. Survives OS cache eviction
57//! (this is NOT cache-semantic state). `rm -rf ~/.zshrs/` is the
58//! one-verb total reset.
59
60use std::path::PathBuf;
61use std::sync::atomic::{AtomicU8, Ordering};
62use std::sync::Mutex;
63
64/// Daemon-presence probe result.
65/// zshrs-original — no C counterpart.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67#[repr(u8)]
68pub enum Mode {
69    /// Probe hasn't run yet.
70    Unknown = 0,
71    /// Socket connected; assume daemon is alive.
72    Present = 1,
73    /// Probe ran; daemon was not reachable. Shell runs in vanilla mode.
74    Absent = 2,
75    /// User opted out via `[daemon] enabled = "off"`. No probe attempted.
76    Disabled = 3,
77}
78
79impl Mode {
80    #[inline]
81    fn from_u8(v: u8) -> Self {
82        match v {
83            1 => Self::Present,
84            2 => Self::Absent,
85            3 => Self::Disabled,
86            _ => Self::Unknown,
87        }
88    }
89}
90
91static STATE: AtomicU8 = AtomicU8::new(Mode::Unknown as u8);
92
93/// What the user said in `[daemon].enabled` (or the auto default).
94/// zshrs-original — no C counterpart. C zsh has no daemon to enable.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum ConfigSetting {
97    /// Probe at startup; use the daemon if alive (default).
98    Auto,
99    /// Skip the probe entirely; never talk to the daemon.
100    Off,
101    /// Probe at startup; if the daemon isn't alive, log a warning
102    /// (still doesn't spawn — that's the user's responsibility).
103    Require,
104}
105
106/// What the user said in `[shell].skip_configs`.
107/// Explicit discriminants pin the AtomicU8 round-trip in
108/// `skip_configs_setting()`.
109/// zshrs-original — no C counterpart. C zsh always sources every
110/// startup file (Src/init.c `source_home_file()` chain); the
111/// canonical-state path that lets us skip those is unique to zshrs.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113#[repr(u8)]
114pub enum SkipConfigs {
115    /// Always source dotfiles (legacy / vanilla zsh behavior). Default.
116    Off = 0,
117    /// Skip dotfiles iff daemon is present AND has zshrs canonical
118    /// rows. Falls back to dotfile sourcing otherwise. The recommended
119    /// setting once the recorder has populated canonical state.
120    Auto = 1,
121    /// Always skip dotfiles when the daemon is up; don't bother
122    /// checking for zshrs rows. Strict mode for users who know their
123    /// daemon is fully populated.
124    On = 2,
125}
126
127impl ConfigSetting {
128    fn parse(s: &str) -> Option<Self> {
129        match s {
130            "auto" | "" => Some(Self::Auto),
131            "off" | "false" | "no" | "0" => Some(Self::Off),
132            "require" | "on" | "true" | "yes" | "1" => Some(Self::Require),
133            _ => None,
134        }
135    }
136}
137
138impl SkipConfigs {
139    fn parse(s: &str) -> Option<Self> {
140        match s {
141            "off" | "false" | "no" | "0" | "" => Some(Self::Off),
142            "auto" => Some(Self::Auto),
143            "on" | "true" | "yes" | "1" => Some(Self::On),
144            _ => None,
145        }
146    }
147}
148
149/// Knobs from `~/.zshrs/zshrs.toml`. Missing file / section
150/// / key returns the safe defaults (`daemon=auto`,
151/// `skip_configs=off`, no `startup_config`). Unrecognized values
152/// fall back with a log warning.
153/// zshrs-original — no C counterpart.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct Config {
156    /// `daemon` field.
157    pub daemon: ConfigSetting,
158    /// `skip_configs` field.
159    pub skip_configs: SkipConfigs,
160    /// Absolute or `~`-prefixed path to a zsh script sourced after
161    /// normal startup when set. `None` if the key is absent or empty.
162    pub startup_config: Option<PathBuf>,
163    /// `[builtins].coreutils_shadows` — whether to use in-process
164    /// coreutils shadow builtins (`cat`/`head`/`tail`/`sort`/`cut`/
165    /// `tr`/`find`/`cp`/...) for the fork-elimination speedup, or
166    /// fall through to the real `/bin/X` binaries.
167    ///
168    /// **Default `off`** — old scripts run against the canonical
169    /// system coreutils out of the box, no edge-case divergence
170    /// surprises. Opt IN to the speedup with the following stanza
171    /// in `~/.zshrs/zshrs.toml`, or via `ZSHRS_COREUTILS_SHADOWS=1`
172    /// env for one-off testing:
173    ///
174    /// ```toml
175    /// [builtins]
176    /// coreutils_shadows = "on"
177    /// ```
178    pub coreutils_shadows: bool,
179}
180
181/// Resolved `[shell].startup_config` from the last `probe()` (same
182/// process). `None` when unset, omitted, or probe has not run.
183static STARTUP_CONFIG_PATH: Mutex<Option<PathBuf>> = Mutex::new(None);
184
185fn resolve_startup_config_path(raw: &str) -> PathBuf {
186    let s = raw.trim();
187    if let Some(rest) = s.strip_prefix("~/") {
188        return dirs::home_dir()
189            .map(|h| h.join(rest))
190            .unwrap_or_else(|| PathBuf::from(s));
191    }
192    if s == "~" {
193        return dirs::home_dir().unwrap_or_else(|| PathBuf::from(s));
194    }
195    PathBuf::from(s)
196}
197/// `read_config_full` — see implementation.
198pub fn read_config_full() -> Config {
199    let defaults = Config {
200        daemon: ConfigSetting::Auto,
201        skip_configs: SkipConfigs::Off,
202        startup_config: None,
203        // OFF by default — old scripts run against system /bin/X
204        // out of the box. Opt-in to the in-process speedup via
205        // [builtins].coreutils_shadows = on.
206        coreutils_shadows: false,
207    };
208    let path = match config_file_path() {
209        Some(p) => p,
210        None => return defaults,
211    };
212    let body = match std::fs::read_to_string(&path) {
213        Ok(s) => s,
214        Err(_) => return defaults,
215    };
216    let parsed = match body.parse::<toml::Table>() {
217        Ok(t) => t,
218        Err(e) => {
219            tracing::warn!(path = %path.display(), error = %e, "zshrs.toml: parse failed; using defaults");
220            return defaults;
221        }
222    };
223    let daemon = parsed
224        .get("daemon")
225        .and_then(|v| v.as_table())
226        .and_then(|t| t.get("enabled"))
227        .and_then(|v| v.as_str())
228        .map(|s| {
229            ConfigSetting::parse(s).unwrap_or_else(|| {
230                tracing::warn!(
231                    value = s,
232                    "zshrs.toml: [daemon].enabled invalid; using auto"
233                );
234                ConfigSetting::Auto
235            })
236        })
237        .unwrap_or(ConfigSetting::Auto);
238    let skip_configs = parsed
239        .get("shell")
240        .and_then(|v| v.as_table())
241        .and_then(|t| t.get("skip_configs"))
242        .and_then(|v| v.as_str())
243        .map(|s| {
244            SkipConfigs::parse(s).unwrap_or_else(|| {
245                tracing::warn!(
246                    value = s,
247                    "zshrs.toml: [shell].skip_configs invalid; using off"
248                );
249                SkipConfigs::Off
250            })
251        })
252        .unwrap_or(SkipConfigs::Off);
253    let startup_config = parsed
254        .get("shell")
255        .and_then(|v| v.as_table())
256        .and_then(|t| t.get("startup_config"))
257        .and_then(|v| {
258            if let Some(s) = v.as_str() {
259                let t = s.trim();
260                if t.is_empty() {
261                    None
262                } else {
263                    Some(resolve_startup_config_path(t))
264                }
265            } else {
266                tracing::warn!("zshrs.toml: [shell].startup_config must be a string; ignoring");
267                None
268            }
269        });
270    // `[builtins].coreutils_shadows` — accept booleans (`true`/`false`)
271    // OR string aliases (`on`/`off`/`yes`/`no`). String-aliased
272    // matches how `[daemon].enabled` accepts `auto`/`off`/`require`
273    // so the file stays toml-string-uniform.
274    let coreutils_shadows = parsed
275        .get("builtins")
276        .and_then(|v| v.as_table())
277        .and_then(|t| t.get("coreutils_shadows"))
278        .map(|v| match v {
279            toml::Value::Boolean(b) => *b,
280            toml::Value::String(s) => match s.trim().to_ascii_lowercase().as_str() {
281                "off" | "false" | "no" | "0" => false,
282                "on" | "true" | "yes" | "1" => true,
283                other => {
284                    tracing::warn!(
285                        value = other,
286                        "zshrs.toml: [builtins].coreutils_shadows invalid; using off"
287                    );
288                    false
289                }
290            },
291            _ => {
292                tracing::warn!(
293                    "zshrs.toml: [builtins].coreutils_shadows must be bool or string; using off"
294                );
295                false
296            }
297        })
298        .unwrap_or(false);
299    Config {
300        daemon,
301        skip_configs,
302        startup_config,
303        coreutils_shadows,
304    }
305}
306
307/// Snapshot of `[builtins].coreutils_shadows` cached after the first
308/// `coreutils_shadows_enabled()` lookup. `OnceLock` lazy so the toml
309/// read is one-shot, then every subsequent dispatch through
310/// `reg_overridable!` becomes a single atomic load.
311static CORE_SHADOWS_CACHE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
312
313/// Hot-path query for whether to dispatch the in-process coreutils
314/// shadow (`cat`/`cp`/`find`/`sort`/etc) or fall through to the
315/// system binary. Read by the `reg_overridable!` macro in
316/// `fusevm_bridge.rs` on every shadowed-builtin invocation. O(1)
317/// after first call.
318pub fn coreutils_shadows_enabled() -> bool {
319    *CORE_SHADOWS_CACHE.get_or_init(|| {
320        // `--zsh` strict-parity mode NEVER shadows externals — `head`
321        // / `cat` / `sort` must be the system binaries with the
322        // system binaries' exact flag handling and diagnostics
323        // (`head -0` is an error on BSD head, the shadow accepted
324        // it). Unconditional: parity floor outranks the toml knob
325        // and the env override.
326        if crate::IS_ZSH_MODE.load(std::sync::atomic::Ordering::Relaxed) {
327            return false;
328        }
329        // Env override wins for one-off testing:
330        //   ZSHRS_COREUTILS_SHADOWS=1 — force ON
331        //   ZSHRS_COREUTILS_SHADOWS=0 — force OFF
332        // Absent → fall through to toml / default-off.
333        if let Ok(v) = std::env::var("ZSHRS_COREUTILS_SHADOWS") {
334            let v = v.trim().to_ascii_lowercase();
335            if matches!(v.as_str(), "1" | "on" | "true" | "yes") {
336                return true;
337            }
338            if matches!(v.as_str(), "0" | "off" | "false" | "no" | "") {
339                return false;
340            }
341        }
342        read_config_full().coreutils_shadows
343    })
344}
345
346/// Path from `[shell].startup_config` captured during `probe()`.
347#[inline]
348pub fn startup_config_path() -> Option<PathBuf> {
349    STARTUP_CONFIG_PATH.lock().ok().and_then(|g| g.clone())
350}
351
352/// Back-compat wrapper kept for callers that only need the daemon knob.
353pub fn read_config() -> ConfigSetting {
354    read_config_full().daemon
355}
356
357/// `[log] level` from `~/.zshrs/zshrs.toml` for the SHELL side
358/// (zsh::log + zshrs-recorder). Same precedence model the daemon uses
359/// for its own side: `$ZSHRS_LOG` env wins, then this directive, then
360/// `"info"`. Caller hands the returned string straight to
361/// `EnvFilter::try_new`. Errors / missing file / missing key all
362/// resolve to "info" silently — a malformed directive produces a
363/// useful "bad directive" message at the EnvFilter parse layer
364/// instead.
365pub fn read_log_directive() -> String {
366    const DEFAULT: &str = "info";
367    let path = match config_file_path() {
368        Some(p) => p,
369        None => return DEFAULT.into(),
370    };
371    let body = match std::fs::read_to_string(&path) {
372        Ok(s) => s,
373        Err(_) => return DEFAULT.into(),
374    };
375    let parsed = match body.parse::<toml::Table>() {
376        Ok(t) => t,
377        Err(_) => return DEFAULT.into(),
378    };
379    parsed
380        .get("log")
381        .and_then(|v| v.as_table())
382        .and_then(|t| t.get("level"))
383        .and_then(|v| v.as_str())
384        .filter(|s| !s.is_empty())
385        .map(str::to_string)
386        .unwrap_or_else(|| DEFAULT.into())
387}
388
389/// Cached `[shell].skip_configs` value. Set by `probe()`; read by the
390/// shell-init path before sourcing dotfiles.
391static SKIP_CONFIGS: AtomicU8 = AtomicU8::new(0);
392
393/// Have we confirmed the daemon has zshrs canonical rows? Set during
394/// the same probe pass so the `skip_configs` decision is one atomic
395/// load on the hot path.
396static SHOULD_SKIP_CONFIGS: AtomicU8 = AtomicU8::new(0);
397
398/// Resolve `$ZSHRS_HOME/zshrs.toml` or `~/.zshrs/zshrs.toml`.
399/// Single-directory rule: every zshrs file lives under one root.
400/// Returns None if neither $ZSHRS_HOME nor $HOME is set, which is
401/// rare enough to treat as "no config file".
402pub fn config_file_path() -> Option<std::path::PathBuf> {
403    let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
404        std::path::PathBuf::from(custom)
405    } else {
406        std::path::PathBuf::from(std::env::var_os("HOME")?).join(".zshrs")
407    };
408    Some(root.join("zshrs.toml"))
409}
410
411/// Cheap probe — connect-only, no handshake — does the daemon
412/// socket answer?
413/// zshrs-original — no C counterpart. Sets the global state so
414/// subsequent `is_present()` checks are O(1) atomic loads.
415///
416/// Honors `[daemon].enabled`:
417///   - `Off` → state = Disabled, no probe
418///   - `Auto` / `Require` → probe; state = Present or Absent
419///
420/// `Require` additionally logs a warning if the daemon isn't alive —
421/// signal to the user that they configured the shell to expect a
422/// daemon but didn't actually start one.
423pub fn probe() -> Mode {
424    let cfg = read_config_full();
425    if let Ok(mut slot) = STARTUP_CONFIG_PATH.lock() {
426        *slot = cfg.startup_config.clone();
427    }
428    SKIP_CONFIGS.store(cfg.skip_configs as u8, Ordering::Relaxed);
429
430    match cfg.daemon {
431        ConfigSetting::Off => {
432            tracing::info!("daemon: disabled in config ([daemon] enabled = \"off\")");
433            STATE.store(Mode::Disabled as u8, Ordering::Relaxed);
434            // Disabled daemon → no skip; dotfiles always source.
435            SHOULD_SKIP_CONFIGS.store(0, Ordering::Relaxed);
436            return Mode::Disabled;
437        }
438        ConfigSetting::Auto | ConfigSetting::Require => {}
439    }
440
441    let alive = probe_socket();
442    let mode = if alive { Mode::Present } else { Mode::Absent };
443    STATE.store(mode as u8, Ordering::Relaxed);
444
445    if alive {
446        tracing::info!("daemon: present (socket reachable)");
447    } else {
448        match cfg.daemon {
449            ConfigSetting::Require => {
450                tracing::warn!(
451                    "daemon: absent — config requires it but socket is not reachable. \
452                     Start it via `zshrs-daemon`, `systemctl --user start zshrs-daemon`, \
453                     `launchctl load ~/Library/LaunchAgents/com.menketechnologies.zshrs-daemon.plist`, \
454                     or `brew services start zshrs`. Falling back to vanilla mode."
455                );
456            }
457            _ => {
458                tracing::info!(
459                    "daemon: absent (socket not reachable) — running in vanilla zsh mode"
460                );
461            }
462        }
463    }
464
465    // Resolve [shell].skip_configs against daemon presence + zshrs-row
466    // availability. Three settings collapse to a yes/no decision:
467    //   Off  → never skip
468    //   On   → skip iff daemon Present (don't even check rows)
469    //   Auto → skip iff daemon Present AND has zshrs canonical rows
470    let should_skip = match cfg.skip_configs {
471        SkipConfigs::Off => false,
472        SkipConfigs::On => mode == Mode::Present,
473        SkipConfigs::Auto => mode == Mode::Present && daemon_has_zshrs_rows(),
474    };
475    SHOULD_SKIP_CONFIGS.store(if should_skip { 1 } else { 0 }, Ordering::Relaxed);
476    if should_skip {
477        tracing::info!(
478            "shell: skip_configs active — bypassing /etc/zshenv + ~/.{{zshenv,zprofile,zshrc,zlogin}} and \
479             applying canonical state from daemon"
480        );
481    } else if cfg.skip_configs != SkipConfigs::Off {
482        tracing::info!(
483            mode = ?cfg.skip_configs,
484            daemon = ?mode,
485            "shell: skip_configs configured but conditions not met — sourcing dotfiles normally"
486        );
487    }
488    mode
489}
490
491/// Cheap probe: does the daemon have a recorder shard on disk for
492/// shell_id "zshrs"? A `*-recorder.rkyv` file in `~/.zshrs/images/`
493/// means the recorder ran at least once and we have canonical state to
494/// apply. **No IPC** — this is a directory listing + filename match,
495/// because the shell cold-start path can afford zero IPC roundtrips
496/// (the architecture's whole speed thesis).
497///
498/// Returns false on any I/O error → caller falls through to vanilla
499/// `source_startup_files()`.
500/// Is the recorded environment STALE relative to the user's rc files?
501///
502/// "Configs are ignored after the recorder" is the speed thesis, but that
503/// makes an edited `.zshrc` invisible until the user re-runs the recorder.
504/// This is the cheap staleness oracle: compare the newest rc-file mtime
505/// against the newest `*-recorder.rkyv` shard mtime. Returns the path of the
506/// offending rc file when any rc is newer than the recording (i.e. the user
507/// edited config and hasn't re-recorded), else `None`.
508///
509/// NO IPC and NO re-sourcing — a directory listing + a handful of `stat`s,
510/// so it stays within the cold-start budget. The caller decides the channel;
511/// per the no-startup-chatter rule this is logged (never printed to the tty)
512/// and surfaced in `--doctor`.
513#[cfg(feature = "daemon")]
514pub fn recording_staleness() -> Option<String> {
515    use std::time::SystemTime;
516    let paths = crate::daemon::paths::CachePaths::resolve().ok()?;
517    // Newest recorder shard mtime.
518    let mut newest_shard: Option<SystemTime> = None;
519    for entry in std::fs::read_dir(&paths.images).ok()?.flatten() {
520        if entry
521            .file_name()
522            .to_str()
523            .map(|s| s.ends_with("-recorder.rkyv"))
524            .unwrap_or(false)
525        {
526            if let Ok(m) = entry.metadata().and_then(|md| md.modified()) {
527                if newest_shard.map(|n| m > n).unwrap_or(true) {
528                    newest_shard = Some(m);
529                }
530            }
531        }
532    }
533    let shard_mtime = newest_shard?; // no recording yet → not "stale", just absent
534    // Any rc file newer than the recording?
535    let zdotdir = std::env::var("ZDOTDIR")
536        .or_else(|_| std::env::var("HOME"))
537        .unwrap_or_default();
538    let rc_set = [
539        "/etc/zshenv".to_string(),
540        format!("{}/.zshenv", zdotdir),
541        "/etc/zprofile".to_string(),
542        format!("{}/.zprofile", zdotdir),
543        "/etc/zshrc".to_string(),
544        format!("{}/.zshrc", zdotdir),
545        "/etc/zlogin".to_string(),
546        format!("{}/.zlogin", zdotdir),
547    ];
548    let mut newest_rc: Option<(SystemTime, String)> = None;
549    for path in &rc_set {
550        if let Ok(m) = std::fs::metadata(path).and_then(|md| md.modified()) {
551            if newest_rc.as_ref().map(|(n, _)| m > *n).unwrap_or(true) {
552                newest_rc = Some((m, path.clone()));
553            }
554        }
555    }
556    match newest_rc {
557        Some((rc_mtime, rc_path)) if rc_mtime > shard_mtime => Some(rc_path),
558        _ => None,
559    }
560}
561
562/// Non-daemon builds have no recorder shard — nothing to be stale against.
563#[cfg(not(feature = "daemon"))]
564pub fn recording_staleness() -> Option<String> {
565    None
566}
567
568/// Does a recorder shard exist at all? Distinguishes "no recording" (this
569/// shell sources rc files normally) from "recording present and fresh" — so
570/// `--doctor` doesn't claim "up to date" when nothing was ever recorded.
571#[cfg(feature = "daemon")]
572pub fn recording_present() -> bool {
573    crate::daemon::paths::CachePaths::resolve()
574        .ok()
575        .and_then(|p| std::fs::read_dir(&p.images).ok())
576        .map(|it| {
577            it.flatten().any(|e| {
578                e.file_name()
579                    .to_str()
580                    .map(|s| s.ends_with("-recorder.rkyv"))
581                    .unwrap_or(false)
582            })
583        })
584        .unwrap_or(false)
585}
586
587#[cfg(not(feature = "daemon"))]
588pub fn recording_present() -> bool {
589    false
590}
591
592#[cfg(feature = "daemon")]
593fn daemon_has_zshrs_rows() -> bool {
594    let paths = match crate::daemon::paths::CachePaths::resolve() {
595        Ok(p) => p,
596        Err(_) => return false,
597    };
598    let entries = match std::fs::read_dir(&paths.images) {
599        Ok(it) => it,
600        Err(_) => return false,
601    };
602    for entry in entries.flatten() {
603        if let Some(s) = entry.file_name().to_str() {
604            if s.ends_with("-recorder.rkyv") {
605                return true;
606            }
607        }
608    }
609    false
610}
611
612#[cfg(not(feature = "daemon"))]
613fn daemon_has_zshrs_rows() -> bool {
614    false
615}
616
617/// Should the shell-init path skip every `/etc/zsh*` + `~/.zsh*`
618/// dotfile and apply canonical state from the daemon instead? O(1)
619/// atomic load — set by `probe()` at startup.
620/// zshrs-original — no C counterpart. C zsh's Src/init.c always
621/// sources every startup file unconditionally.
622#[inline]
623pub fn should_skip_configs() -> bool {
624    SHOULD_SKIP_CONFIGS.load(Ordering::Relaxed) != 0
625}
626
627/// Read the cached `[shell].skip_configs` setting (verbatim from
628/// config — independent of whether conditions to skip were met).
629/// Mostly useful for diagnostics / `zshrs --doctor`.
630pub fn skip_configs_setting() -> SkipConfigs {
631    match SKIP_CONFIGS.load(Ordering::Relaxed) {
632        1 => SkipConfigs::Auto,
633        2 => SkipConfigs::On,
634        _ => SkipConfigs::Off,
635    }
636}
637
638/// Run the probe via the daemon-client crate's cheap is-alive helper.
639/// Falls back to a manual socket check if the daemon feature is off
640/// (which is the workspace's stub-mode build path).
641#[cfg(feature = "daemon")]
642fn probe_socket() -> bool {
643    match crate::daemon::paths::CachePaths::resolve() {
644        Ok(paths) => crate::daemon::client::Client::is_daemon_alive(&paths),
645        Err(_) => false,
646    }
647}
648
649#[cfg(not(feature = "daemon"))]
650fn probe_socket() -> bool {
651    false
652}
653
654/// O(1) read of the cached probe result. Returns `Unknown` until
655/// `probe()` has run.
656/// zshrs-original — no C counterpart.
657#[inline]
658pub fn current() -> Mode {
659    Mode::from_u8(STATE.load(Ordering::Relaxed))
660}
661
662/// Did the probe see a live daemon? `false` for any other state
663/// (`Unknown` / `Absent` / `Disabled`).
664/// zshrs-original — no C counterpart.
665#[inline]
666pub fn is_present() -> bool {
667    current() == Mode::Present
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673    use std::path::PathBuf;
674
675    #[test]
676    fn config_setting_parses_common_aliases() {
677        let _g = crate::test_util::global_state_lock();
678        assert_eq!(ConfigSetting::parse("auto"), Some(ConfigSetting::Auto));
679        assert_eq!(ConfigSetting::parse(""), Some(ConfigSetting::Auto));
680        assert_eq!(ConfigSetting::parse("off"), Some(ConfigSetting::Off));
681        assert_eq!(ConfigSetting::parse("false"), Some(ConfigSetting::Off));
682        assert_eq!(ConfigSetting::parse("no"), Some(ConfigSetting::Off));
683        assert_eq!(ConfigSetting::parse("0"), Some(ConfigSetting::Off));
684        assert_eq!(
685            ConfigSetting::parse("require"),
686            Some(ConfigSetting::Require)
687        );
688        assert_eq!(ConfigSetting::parse("on"), Some(ConfigSetting::Require));
689        assert_eq!(ConfigSetting::parse("true"), Some(ConfigSetting::Require));
690        assert_eq!(ConfigSetting::parse("garbage"), None);
691    }
692
693    #[test]
694    fn mode_round_trips_through_atomic() {
695        let _g = crate::test_util::global_state_lock();
696        for m in [Mode::Unknown, Mode::Present, Mode::Absent, Mode::Disabled] {
697            assert_eq!(Mode::from_u8(m as u8), m);
698        }
699    }
700
701    #[test]
702    fn resolve_startup_config_path_absolute_trims() {
703        let _g = crate::test_util::global_state_lock();
704        assert_eq!(
705            super::resolve_startup_config_path("  /tmp/x.zsh  "),
706            PathBuf::from("/tmp/x.zsh")
707        );
708    }
709
710    #[test]
711    fn resolve_startup_config_path_tilde() {
712        let _g = crate::test_util::global_state_lock();
713        let home = dirs::home_dir().expect("HOME");
714        assert_eq!(
715            super::resolve_startup_config_path("~/init.zsh"),
716            home.join("init.zsh")
717        );
718    }
719
720    #[test]
721    fn read_config_full_startup_config_from_zshrs_home() {
722        let _g = crate::test_util::global_state_lock();
723        use std::sync::Mutex;
724        static ENV_LOCK: Mutex<()> = Mutex::new(());
725        let _lock = ENV_LOCK.lock().expect("env test lock");
726        let dir = tempfile::tempdir().expect("tempdir");
727        std::fs::write(
728            dir.path().join("zshrs.toml"),
729            "[shell]\nstartup_config = \"/tmp/zshrs-startup-test.zsh\"\n",
730        )
731        .expect("write zshrs.toml");
732        unsafe {
733            std::env::set_var("ZSHRS_HOME", dir.path());
734        }
735        let cfg = read_config_full();
736        unsafe {
737            std::env::remove_var("ZSHRS_HOME");
738        }
739        assert_eq!(
740            cfg.startup_config,
741            Some(PathBuf::from("/tmp/zshrs-startup-test.zsh"))
742        );
743    }
744}