onlyne_client/backend/
select.rs1use super::*;
2use onlyne_config::{Placement, validate_drive_placement};
3
4pub fn process_env() -> BTreeMap<String, String> {
5 std::env::vars().collect()
6}
7
8fn env_nonempty(env: &BTreeMap<String, String>, key: &str) -> bool {
9 env.get(key).is_some_and(|value| !value.is_empty())
10}
11
12fn orca_host_present(env: &BTreeMap<String, String>) -> bool {
13 env_nonempty(env, "ORCA_PANE_KEY")
14 || env_nonempty(env, "ORCA_TERMINAL_HANDLE")
15 || env_nonempty(env, "ORCA_WORKTREE_ID")
16}
17
18fn zellij_host_present(env: &BTreeMap<String, String>) -> bool {
19 env.contains_key("ZELLIJ")
20}
21
22pub(crate) const TERN_BINARY: &str = "/Applications/Tern.app/Contents/MacOS/tern";
28
29fn probed_placement(env: &BTreeMap<String, String>) -> Option<Placement> {
32 onlyne_config::PLACEMENT_PROBE_ORDER
33 .into_iter()
34 .find(|placement| host_present(*placement, env))
35}
36
37fn host_present(placement: Placement, env: &BTreeMap<String, String>) -> bool {
44 match placement {
45 Placement::Orca => orca_host_present(env),
46 Placement::Zellij => zellij_host_present(env),
47 Placement::Tern => {
48 env.get("TERM_PROGRAM")
49 .is_some_and(|program| program == "tern")
50 || env_nonempty(env, "TERN_PANE")
51 }
52 Placement::Headless | Placement::External => false,
53 }
54}
55
56fn host_binary(placement: Placement, env: &BTreeMap<String, String>) -> Option<String> {
59 let (override_key, fallback): (&str, &str) = match placement {
60 Placement::Orca => ("ORCA_CLI_COMMAND", "orca"),
61 Placement::Zellij => ("ZELLIJ_COMMAND", "zellij"),
62 Placement::Tern => ("TERN_COMMAND", TERN_BINARY),
63 Placement::Headless | Placement::External => return None,
66 };
67 Some(
68 env.get(override_key)
69 .filter(|value| !value.is_empty())
70 .cloned()
71 .unwrap_or_else(|| fallback.into()),
72 )
73}
74
75pub fn detect_placement(
88 env: &BTreeMap<String, String>,
89 declared: Option<SessionPlacement>,
90) -> Result<PlacementDetection> {
91 let explicit = env
92 .get("ONLYNE_BACKEND")
93 .map(|value| value.trim().to_string())
94 .filter(|value| !value.is_empty() && !value.eq_ignore_ascii_case("auto"));
95 if let Some(name) = &explicit {
96 let Some(placement) = SessionPlacement::parse(name) else {
97 return Err(UnknownPlacement(name.clone()).into());
98 };
99 return Ok(PlacementDetection {
100 placement,
101 source: SelectionSource::Explicit,
102 explicit: Some(name.clone()),
103 });
104 }
105 if let Some(placement) = declared {
106 return Ok(PlacementDetection {
107 placement,
108 source: SelectionSource::Declared,
109 explicit: None,
110 });
111 }
112 let probed = probed_placement(env);
113 Ok(PlacementDetection {
114 placement: SessionPlacement::Named(probed.unwrap_or(Placement::Headless)),
115 source: if probed.is_some() {
116 SelectionSource::Probe
117 } else {
118 SelectionSource::Fallback
119 },
120 explicit: None,
121 })
122}
123
124pub fn doctor_report(env: &BTreeMap<String, String>) -> Value {
128 let detected = detect_placement(env, None);
129 let found = detected.as_ref().ok();
130 let placement = found.map(|detected| detected.placement.as_str());
131 let selection = found.map(|detected| match detected.source {
132 SelectionSource::Explicit => "explicit",
133 SelectionSource::Declared => "declared",
134 SelectionSource::Probe => "probe",
135 SelectionSource::Fallback => "fallback",
136 });
137 let binary = found
138 .and_then(|detected| detected.placement.named())
139 .and_then(|named| host_binary(named, env));
140 let mut report = serde_json::json!({
141 "placement": placement,
142 "placement_selection": selection,
143 "binary": binary,
144 "explicit": found.and_then(|detected| detected.explicit.clone()),
145 });
146 if let Err(error) = &detected {
147 report["refusal"] = Value::String(error.to_string());
148 }
149 report
150}
151
152pub fn backend_for(
172 drive: onlyne_config::Drive,
173 placement: SessionPlacement,
174 runner: Arc<dyn Runner>,
175 policy: WorktreePolicy,
176 acp: &AcpOptions,
177) -> Result<Box<dyn SessionBackend>> {
178 if let Some(named) = placement.named() {
179 validate_drive_placement(drive, named).map_err(|message| anyhow::anyhow!("{message}"))?;
180 }
181 Ok(match placement {
182 SessionPlacement::Fake => Box::new(fake::FakeBackend::new()),
183 SessionPlacement::Named(Placement::Orca) => {
184 Box::new(orca::OrcaBackend::with_policy(runner, policy))
185 }
186 SessionPlacement::Named(Placement::Zellij) => Box::new(zellij::ZellijBackend::new(runner)),
187 SessionPlacement::Named(Placement::Tern) => Box::new(tern::TernBackend::new(runner)),
188 SessionPlacement::Named(Placement::Headless) => match drive {
189 onlyne_config::Drive::Acp => Box::new(acp::AcpBackend::new(acp.clone())),
190 onlyne_config::Drive::Plugin | onlyne_config::Drive::Exec => {
191 Box::new(exec::ExecBackend::new())
192 }
193 },
194 SessionPlacement::Named(Placement::External) => match drive {
195 onlyne_config::Drive::Plugin => Box::new(external::ExternalBackend::new()),
196 onlyne_config::Drive::Acp | onlyne_config::Drive::Exec => {
197 Box::new(exec::ExecBackend::new())
198 }
199 },
200 })
201}