Skip to main content

rightkit_daemon/
mode.rs

1//! The one runtime decision about where an app keeps its data (M21).
2//!
3//! There is no test mode. Adrian, 2026-11-03: "dev mode, is mine. im the fucking owner, my data. no
4//! dummy data, no fucking made up shit. real data. snapshot and restore." Every build, release or
5//! dev, resolves the real per-user roots from the operating system's account database
6//! ([`Suite::per_user_default`]) and reads no environment variable, argument or marker to choose
7//! them. QA runs on that real data and protects it with the snapshot and restore of `rightkit-qa`
8//! (rightkit#1437), not with a throwaway root. The former `test-mode` feature, `Mode::Test`, the
9//! override selectors and the `RIGHTKIT-TEST-MODE-COMPILED` marker are gone (rightkit-daemon 0.5.3).
10
11use std::fmt;
12use std::io;
13use std::path::PathBuf;
14use std::sync::OnceLock;
15
16use crate::suite::{Suite, SuiteError};
17
18/// The app asking for a mode. `id` is the binding key (`"viewright"`); `dir_name` is the folder
19/// under `Orthic Labs` that holds the app's own data (`"ViewRight"`).
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct AppId {
22    id: &'static str,
23    dir_name: &'static str,
24}
25
26impl AppId {
27    /// An app whose data folder carries the same name as its id.
28    pub const fn new(id: &'static str) -> AppId {
29        AppId { id, dir_name: id }
30    }
31
32    /// An app whose data folder differs from its id.
33    pub const fn with_dir(id: &'static str, dir_name: &'static str) -> AppId {
34        AppId { id, dir_name }
35    }
36
37    pub const fn id(&self) -> &'static str {
38        self.id
39    }
40
41    pub const fn dir_name(&self) -> &'static str {
42        self.dir_name
43    }
44}
45
46/// Why [`decide`] refused to run. Messages never name an environment variable.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum Refusal {
49    /// The real per-user root could not be determined.
50    UserRootUnknown,
51    /// A filesystem error.
52    Io(String),
53}
54
55impl Refusal {
56    /// A stable kebab-case rule id.
57    pub fn rule(&self) -> &'static str {
58        match self {
59            Refusal::UserRootUnknown => "user-root-unknown",
60            Refusal::Io(_) => "io",
61        }
62    }
63}
64
65impl fmt::Display for Refusal {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Refusal::Io(e) => write!(f, "run refused ({}): {e}", self.rule()),
69            _ => write!(f, "run refused ({})", self.rule()),
70        }
71    }
72}
73
74impl std::error::Error for Refusal {}
75
76impl From<io::Error> for Refusal {
77    fn from(e: io::Error) -> Self {
78        Refusal::Io(e.to_string())
79    }
80}
81
82/// How a run was decided. The only value is `Release`: the real per-user roots.
83#[derive(Debug, Clone)]
84pub enum Mode {
85    Release,
86}
87
88impl Mode {
89    /// Always `false`: no build has a test mode.
90    pub fn is_test(&self) -> bool {
91        false
92    }
93}
94
95/// Where [`decide`] reads from. The default implementation of the provided methods is the real
96/// operating system. A test supplies a fake, including one that panics on any read.
97pub trait ModeInput {
98    /// An environment variable. [`decide`] and [`roots`] never call it.
99    fn env(&self, name: &str) -> Option<String>;
100
101    /// The real per-user suite root, from the account database.
102    fn per_user_default(&self) -> Result<Suite, SuiteError> {
103        Suite::per_user_default()
104    }
105}
106
107/// The real process environment.
108#[derive(Debug, Clone, Copy, Default)]
109pub struct ProcessInput;
110
111impl ModeInput for ProcessInput {
112    fn env(&self, name: &str) -> Option<String> {
113        std::env::var(name).ok()
114    }
115}
116
117/// Resolved roots a store must come through.
118#[derive(Debug, Clone)]
119pub struct AppRoots {
120    /// The suite root (`<Orthic Labs>/shared`).
121    pub suite: Suite,
122    /// The app's own data directory.
123    pub app_dir: PathBuf,
124}
125
126/// Decide once, before any store, log, credential or service starts. Returns [`Mode::Release`]
127/// and reads nothing from `input`.
128pub fn decide(app: &AppId, input: &dyn ModeInput) -> Result<Mode, Refusal> {
129    let _ = (app, input);
130    Ok(Mode::Release)
131}
132
133/// [`decide`] against the real process, once per process. Prints one line to stderr and exits with
134/// status 2 on a [`Refusal`]; hosts call it first in `main`.
135pub fn init_or_exit(app: &AppId) -> &'static Mode {
136    static MODE: OnceLock<Mode> = OnceLock::new();
137    if let Some(mode) = MODE.get() {
138        return mode;
139    }
140    match decide(app, &ProcessInput) {
141        Ok(mode) => MODE.get_or_init(|| mode),
142        Err(refusal) => {
143            eprintln!("{refusal}");
144            std::process::exit(2);
145        }
146    }
147}
148
149/// The roots every store must come through, against the real process.
150pub fn roots(app: &AppId, mode: &Mode) -> Result<AppRoots, SuiteError> {
151    roots_with(app, mode, &ProcessInput)
152}
153
154/// [`roots`] with an injectable source for the real per-user root.
155pub fn roots_with(app: &AppId, mode: &Mode, input: &dyn ModeInput) -> Result<AppRoots, SuiteError> {
156    match mode {
157        Mode::Release => {
158            let suite = input.per_user_default()?;
159            let orthic = suite
160                .root()
161                .parent()
162                .ok_or_else(|| SuiteError::NoRoot("the per-user suite root has no parent".into()))?
163                .to_path_buf();
164            Ok(AppRoots {
165                suite,
166                app_dir: orthic.join(app.dir_name),
167            })
168        }
169    }
170}