Skip to main content

qcode/ui/profiles/
status.rs

1//! What QCode knows about a profile on this machine: whether its image is built, whether it was
2//! built from the recipe this QCode would use, and whether a login is kept for it.
3//!
4//! Both answers come from the engine and neither is ever guessed. Until the engine has been
5//! asked, the answer is [`Readiness::Unknown`] and the screen says so rather than showing a
6//! profile as ready to run.
7
8use crate::profile::{Profile, SafeName};
9
10/// Whether something the engine holds is there.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Readiness {
13    /// The engine has not been asked yet, or could not answer.
14    Unknown,
15    /// The engine has it.
16    Present,
17    /// The engine was asked and does not have it.
18    Missing,
19}
20
21impl Readiness {
22    /// The answer to "is it there?", when there is one.
23    #[must_use]
24    pub fn of(present: bool) -> Self {
25        if present { Self::Present } else { Self::Missing }
26    }
27}
28
29/// Which recipe a profile's image was built from, as the label the build left on it says.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Revision {
32    /// There is no image to ask, or it has not been asked yet.
33    Unknown,
34    /// The image was built from the recipe this QCode writes for the profile today.
35    Current,
36    /// The image was built from another recipe: an earlier QCode's, or one from before images
37    /// carried the label at all.
38    Earlier,
39}
40
41/// What the engine answered about one profile, carried by name so an answer that arrives late
42/// still finds the profile it belongs to.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Status {
45    /// The profile the answer is about.
46    pub name: SafeName,
47    /// Whether the profile's image is built.
48    pub image: Readiness,
49    /// Which recipe the image was built from.
50    pub revision: Revision,
51    /// Whether a login is kept in the profile's credentials volume.
52    pub identity: Readiness,
53}
54
55/// One profile as the list shows it: the definition, and what the engine says about it.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Row {
58    /// The profile as its definition file describes it.
59    pub profile: Profile,
60    /// Whether the profile's image is built.
61    pub image: Readiness,
62    /// Which recipe the image was built from.
63    pub revision: Revision,
64    /// Whether a login is kept for the profile.
65    pub identity: Readiness,
66}
67
68impl Row {
69    /// A row nothing has been asked about yet.
70    #[must_use]
71    pub fn new(profile: Profile) -> Self {
72        Self { profile, image: Readiness::Unknown, revision: Revision::Unknown, identity: Readiness::Unknown }
73    }
74
75    /// Whether a workspace could open this profile right now: the image is there and so is a
76    /// login, unless the profile signs in to nothing. An unknown answer is not a yes.
77    #[must_use]
78    pub fn is_runnable(&self) -> bool {
79        self.image == Readiness::Present && (self.is_signed_in() || !self.profile.account.needs_login())
80    }
81
82    /// Whether a login is known to be kept for the profile.
83    #[must_use]
84    pub fn is_signed_in(&self) -> bool {
85        self.identity == Readiness::Present
86    }
87
88    /// Takes an answer that is about this profile; answers about another one are ignored.
89    pub fn apply(&mut self, status: &Status) {
90        if status.name == self.profile.name {
91            self.image = status.image;
92            self.revision = status.revision;
93            self.identity = status.identity;
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::profile::{AccountKind, HarnessKind, MountAccess, NetworkMode, Template};
102
103    fn profile(name: &str) -> Profile {
104        Profile {
105            name: SafeName::parse(name).expect("the name is safe"),
106            harness: HarnessKind::ClaudeCode,
107            template: Template::Recommended,
108            account: AccountKind::Subscription,
109            provider: None,
110            assets: MountAccess::ReadOnly,
111            network: NetworkMode::Full,
112            without: Vec::new(),
113            os: crate::base::Os::Debian,
114        }
115    }
116
117    #[test]
118    fn nothing_is_ready_before_the_engine_has_answered() {
119        let row = Row::new(profile("claude-sub"));
120        assert_eq!(row.image, Readiness::Unknown);
121        assert_eq!(row.identity, Readiness::Unknown);
122        assert!(!row.is_runnable(), "an unanswered profile is never treated as ready");
123    }
124
125    #[test]
126    fn a_profile_is_runnable_only_with_both_an_image_and_a_login() {
127        let mut row = Row::new(profile("claude-sub"));
128        row.apply(&Status {
129            name: SafeName::parse("claude-sub").expect("safe"),
130            image: Readiness::Present,
131            revision: Revision::Current,
132            identity: Readiness::Missing,
133        });
134        assert!(!row.is_runnable());
135        row.apply(&Status {
136            name: SafeName::parse("claude-sub").expect("safe"),
137            image: Readiness::Present,
138            revision: Revision::Current,
139            identity: Readiness::Present,
140        });
141        assert!(row.is_runnable());
142    }
143
144    #[test]
145    fn a_free_profile_is_runnable_with_its_image_alone() {
146        let mut free = profile("oc");
147        free.harness = HarnessKind::OpenCode;
148        free.account = AccountKind::Free;
149        let mut row = Row::new(free);
150        assert!(!row.is_runnable(), "an image nobody has checked is still not a yes");
151        row.apply(&Status {
152            name: SafeName::parse("oc").expect("safe"),
153            image: Readiness::Present,
154            revision: Revision::Current,
155            identity: Readiness::Missing,
156        });
157        assert!(row.is_runnable(), "no login is kept, and none is needed");
158    }
159
160    #[test]
161    fn an_answer_about_another_profile_is_left_alone() {
162        let mut row = Row::new(profile("claude-sub"));
163        row.apply(&Status {
164            name: SafeName::parse("codex-key").expect("safe"),
165            image: Readiness::Present,
166            revision: Revision::Current,
167            identity: Readiness::Present,
168        });
169        assert_eq!(row.image, Readiness::Unknown);
170        assert_eq!(row.identity, Readiness::Unknown);
171    }
172}