onlyne_client/backend/tern/
session.rs1use super::cli::default_command;
5use super::policy::{TernRef, session_label, split_word};
6use crate::backend::*;
7use std::collections::BTreeMap;
8use std::sync::Arc;
9
10pub struct TernBackend {
11 pub(super) runner: Arc<dyn Runner>,
12 pub(super) command: String,
13 pub(super) env: BTreeMap<String, String>,
14}
15
16impl TernBackend {
17 pub fn new(runner: Arc<dyn Runner>) -> Self {
18 Self::with_env(runner, process_env())
19 }
20
21 pub fn with_env(runner: Arc<dyn Runner>, env: BTreeMap<String, String>) -> Self {
22 let command = env
23 .get("TERN_COMMAND")
24 .cloned()
25 .filter(|value| !value.is_empty())
26 .unwrap_or_else(default_command);
27 Self {
28 runner,
29 command,
30 env,
31 }
32 }
33}
34
35impl SessionBackend for TernBackend {
36 fn name(&self) -> &'static str {
37 "tern"
38 }
39
40 fn capabilities(&self) -> Capabilities {
41 Capabilities {
42 spawn: true,
43 attach: true,
44 probe: true,
45 close: true,
46 focus: true,
47 rename: false,
53 }
54 }
55
56 fn available(&self) -> Result<bool> {
57 Ok(self.json(vec!["ls".into(), "--json".into()]).is_ok())
62 }
63
64 fn spawn(&self, spec: SpawnSpec) -> Result<SessionRef> {
65 if spec.command.is_empty() {
66 anyhow::bail!("tern spawn requires a command");
67 }
68 let label = session_label(&spec);
69 let session_id = self.find_or_create_session(&spec)?;
70 let tab = self.find_or_create_tab(&session_id, &spec)?;
71 let placement = spec
76 .placement
77 .unwrap_or_else(|| PanePlacement::from_pane_count(tab.pane_count));
78 tracing::info!(
79 pane_count = tab.pane_count,
80 direction = split_word(placement.direction),
81 "tern block split (tern takes no split ratio)"
82 );
83 let pane_id =
84 self.split_and_start(&tab.base_pane, &spec, placement, &session_id, &tab.tab_id)?;
85 Ok(SessionRef {
86 task_id: spec.task_id.clone(),
87 backend: self.name().into(),
88 backend_ref: TernRef {
89 session_id,
90 tab_id: tab.tab_id,
91 pane_id,
92 session_label: label,
93 base_pane: tab.base_pane,
94 split_direction: split_word(placement.direction).to_string(),
95 }
96 .to_value(),
97 generation: 1,
98 })
99 }
100
101 fn attach(&self, session: &SessionRef) -> Result<SessionRef> {
102 let probe = self.probe(session)?;
103 if !probe.alive {
104 anyhow::bail!("tern block is gone");
105 }
106 Ok(session.clone())
107 }
108
109 fn probe(&self, session: &SessionRef) -> Result<ResourceProbe> {
110 let reference = TernRef::from_session(session)?;
111 let listing = self.listing()?;
115 let Some((block, tab, host)) = listing.block(&reference.pane_id) else {
116 return Ok(ResourceProbe {
120 alive: false,
121 attached: false,
122 detail: Some(serde_json::json!({
123 "pane_id": reference.pane_id,
124 "error": "block is in no tab",
125 })),
126 });
127 };
128 let alive = block.exited.is_none() && block.live;
136 let mut detail = serde_json::json!({
137 "pane": {
138 "id": block.id,
139 "title": block.title,
140 "cwd": block.cwd,
141 "program": block.program,
142 "command": block.command,
143 "exited": block.exited,
144 "keep_open": block.keep_open,
145 "focused": block.focused,
146 "live": block.live,
147 },
148 "tab_id": tab.id,
149 "session_id": host.id,
150 "session_label": host.name,
151 });
152 if let Some(process) = self.process_of(&reference.pane_id) {
153 if let Some(map) = detail.as_object_mut() {
154 map.insert("process".into(), process);
155 }
156 }
157 Ok(ResourceProbe {
158 alive,
159 attached: block.live,
164 detail: Some(detail),
165 })
166 }
167
168 fn close(&self, session: &SessionRef, reason: CloseReason, force: bool) -> Result<()> {
169 tracing::debug!(task = %session.task_id, ?reason, force, "closing tern block");
172 let reference = TernRef::from_session(session)?;
173 match self.json(vec![
174 "close".into(),
175 reference.pane_id.clone(),
176 "--json".into(),
177 ]) {
178 Ok(_) => Ok(()),
179 Err(error) if self.is_gone(&error) => {
184 tracing::debug!(
185 task = %session.task_id,
186 pane = %reference.pane_id,
187 "tern block already closed"
188 );
189 Ok(())
190 }
191 Err(error) => Err(error),
192 }
193 }
194
195 fn rename(&self, _session: &SessionRef, _title: &str) -> Result<()> {
196 Err(unsupported(
202 self.name(),
203 "rename",
204 "tern renames a block's tab, so renaming one session's block would rename the \
205 whole role tab",
206 ))
207 }
208
209 fn focus(&self, session: &SessionRef) -> Result<()> {
210 let reference = TernRef::from_session(session)?;
211 let listing = self.listing()?;
212 if let Some(site) = self.focus_here(&listing) {
213 if site.pane_id == reference.pane_id
214 && site.tab_id == reference.tab_id
215 && site.session_id == reference.session_id
216 {
217 return Ok(());
218 }
219 }
220 self.focus_block(&reference.pane_id)?;
221 let after = self.listing()?;
227 match self.focus_here(&after) {
228 Some(site) if site.pane_id == reference.pane_id => Ok(()),
229 Some(site) => Err(anyhow::anyhow!(
230 "tern left block {} unfocused (focused block {})",
231 reference.pane_id,
232 site.pane_id
233 )),
234 None => Err(anyhow::anyhow!(
235 "tern left block {} unfocused (no block holds focus)",
236 reference.pane_id
237 )),
238 }
239 }
240}