1use std::collections::HashMap;
9use std::sync::mpsc::{channel, Receiver, Sender};
10#[cfg(test)]
11mod tests;
12
13use strop_core::id::DocumentId;
14use strop_core::worker::{self, Completion, FailureKind, Outcome, Ticket, WorkerId};
15use strop_core::Buffer;
16
17use super::document::Document;
18use super::Editor;
19
20#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub enum ContainerJob {
23 Discover,
25 Attach { id: String },
28 ListDir { id: String, path: String },
30 ReadFile { id: String, path: String },
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35pub struct ContainerKey {
36 pub request: WorkerId,
37 pub focus: u64,
38 pub job: ContainerJob,
39}
40
41#[derive(serde::Serialize, serde::Deserialize)]
43pub enum ContainerResult {
44 Containers(Vec<strop_containers::ContainerIdentity>),
45 Listing {
46 identity: strop_containers::ContainerIdentity,
47 path: String,
48 entries: Vec<strop_containers::DirEntry>,
49 },
50 File {
51 identity: strop_containers::ContainerIdentity,
52 path: String,
53 text: String,
54 },
55}
56
57pub type ContainerEvent = Completion<ContainerKey, ContainerResult>;
58
59pub(crate) struct ContainerState {
60 pub tx: Sender<ContainerEvent>,
61 pub rx: Option<Receiver<ContainerEvent>>,
62 pub pending: Option<Ticket<ContainerKey>>,
63 pub attached: HashMap<String, strop_containers::ContainerIdentity>,
65 pub entries: HashMap<DocumentId, Vec<strop_containers::DirEntry>>,
67 pub buffers: HashMap<DocumentId, (String, String)>, }
70
71impl Default for ContainerState {
72 fn default() -> Self {
73 let (tx, rx) = channel();
74 Self {
75 tx,
76 rx: Some(rx),
77 pending: None,
78 attached: HashMap::new(),
79 entries: HashMap::new(),
80 buffers: HashMap::new(),
81 }
82 }
83}
84
85impl ContainerState {
86 pub fn take_rx(&mut self) -> Option<Receiver<ContainerEvent>> {
87 self.rx.take()
88 }
89}
90
91impl Editor {
92 pub(crate) fn request_containers(&mut self) {
95 self.start_container_job(ContainerJob::Discover);
96 }
97
98 fn start_container_job(&mut self, job: ContainerJob) {
99 if self.containers.pending.is_some() {
100 self.message = "a container request is already running".into();
101 return;
102 }
103 let request = match self.worker_ids.allocate() {
104 Ok(request) => request,
105 Err(error) => {
106 self.message = error.message;
107 return;
108 }
109 };
110 let key = ContainerKey {
111 request,
112 focus: self.focus_epoch,
113 job: job.clone(),
114 };
115 let pending = Ticket {
116 request,
117 key: key.clone(),
118 };
119 let tx = self.containers.tx.clone();
120 let handle = worker::spawn(
121 "container-job",
122 move |outcome| {
123 let _ = tx.send(Completion {
124 ticket: pending,
125 outcome,
126 });
127 },
128 move |token| match run_container_job(&job, &token) {
129 Ok(result) => Outcome::Success(result),
130 Err(strop_containers::ContainerError::Cancelled) => {
131 Outcome::Cancelled(worker::CancelReason::Dismissed)
132 }
133 Err(error) => Outcome::failed(FailureKind::Io, error.to_string()),
134 },
135 );
136 self.containers.pending = Some(Ticket { request, key });
137 self.worker_handles.insert(request, handle);
138 }
139
140 pub(crate) fn attach_container(&mut self, id: String) {
143 self.start_container_job(ContainerJob::Attach { id });
144 }
145
146 pub(crate) fn container_key(&mut self, key: super::Key) -> bool {
150 if key != super::Key::Enter {
151 return false;
152 }
153 let Some((id, base)) = self.containers.buffers.get(&self.current()).cloned() else {
154 return false;
155 };
156 let Some(entries) = self.containers.entries.get(&self.current()) else {
157 return false; };
159 let line = self.buf().line_of(self.head());
160 let Some(entry) = line.checked_sub(1).and_then(|row| entries.get(row)) else {
161 self.message = "no container entry at this position".into();
162 return true;
163 };
164 let path = format!("{}/{}", base.trim_end_matches('/'), entry.name);
165 let job = match entry.kind {
166 strop_containers::DirEntryKind::Dir => ContainerJob::ListDir { id, path },
167 _ => ContainerJob::ReadFile { id, path },
168 };
169 self.start_container_job(job);
170 true
171 }
172
173 pub(crate) fn handle_container_event(&mut self, event: ContainerEvent) {
174 let owned = self
175 .containers
176 .pending
177 .as_ref()
178 .is_some_and(|ticket| ticket.request == event.ticket.request);
179 if !owned {
180 return; }
182 self.containers.pending = None;
183 self.worker_handles.remove(&event.ticket.request);
184 let focused = event.ticket.key.focus == self.focus_epoch && !self.finishing;
185 match event.outcome {
186 Outcome::Success(ContainerResult::Containers(list)) if focused => {
187 if list.is_empty() {
188 self.message = "no running containers on the local engine".into();
189 return;
190 }
191 let items = list
192 .into_iter()
193 .map(|identity| strop_picker::Item {
194 badge: None,
195 text: format!(
196 "{} {} {}",
197 &identity.id[..12],
198 identity.name,
199 identity.image
200 ),
201 payload: strop_picker::Payload::Container(identity.id.clone()),
202 })
203 .collect();
204 self.set_picker(super::picker::PickerGlue::diagnostics(
205 strop_picker::Picker::new(strop_picker::Kind::Containers, items, false),
206 ));
207 }
208 Outcome::Success(ContainerResult::Listing {
209 identity,
210 path,
211 entries,
212 }) if focused => {
213 let Ok(reference) = strop_containers::ContainerRef::of(&identity) else {
214 self.message = "container identity failed validation".into();
215 return;
216 };
217 self.workspaces.bind(
218 strop_workspace::Filesystem::Container(reference.id().clone()),
219 None,
220 );
221 self.containers
222 .attached
223 .insert(identity.id.clone(), identity.clone());
224 let short = &identity.id[..12];
225 let mut text = format!(
226 "container {} ({}:{}) — {} entry(s); enter opens, q closes\n",
227 identity.name,
228 short,
229 path,
230 entries.len()
231 );
232 for entry in &entries {
233 let marker = match entry.kind {
234 strop_containers::DirEntryKind::Dir => "d",
235 strop_containers::DirEntryKind::Symlink => "l",
236 _ => " ",
237 };
238 let suffix = if entry.kind == strop_containers::DirEntryKind::Dir {
239 "/"
240 } else {
241 ""
242 };
243 text.push_str(&format!("{} {}{}\n", marker, entry.name, suffix));
244 }
245 let mut buffer = Buffer::from_text(&text);
246 buffer.name = Some(format!("container:{short}:{path}"));
247 let id = self.docs.insert(Document::output(buffer));
248 self.drop_stale_scratch(id);
249 self.containers
250 .buffers
251 .insert(id, (identity.id.clone(), path));
252 self.containers.entries.insert(id, entries);
253 self.switch_to(id);
254 self.set_head(0);
255 }
256 Outcome::Success(ContainerResult::File {
257 identity,
258 path,
259 text,
260 }) if focused => {
261 let short = identity.id[..12].to_string();
262 let mut buffer = Buffer::from_text(&text);
263 buffer.name = Some(format!("container:{short}:{path}"));
264 let Ok(container) = strop_workspace::ContainerId::canonical(identity.id.clone())
265 else {
266 self.message = "container identity failed validation".into();
267 return;
268 };
269 let id = self.docs.insert(Document::container_file(
270 buffer,
271 container,
272 std::path::PathBuf::from(&path),
273 ));
274 self.drop_stale_scratch(id);
275 self.containers
276 .buffers
277 .insert(id, (identity.id.clone(), path));
278 self.switch_to(id);
279 self.set_head(0);
280 self.lsp_maybe_attach();
283 }
284 Outcome::Success(_) => {}
285 Outcome::Failed { failure, .. } if focused => {
286 self.message = failure.message;
287 }
288 _ => {}
289 }
290 }
291}
292
293fn run_container_job(
297 job: &ContainerJob,
298 token: &strop_core::worker::CancelToken,
299) -> Result<ContainerResult, strop_containers::ContainerError> {
300 let engine = strop_containers::engine(token)?;
301 match job {
302 ContainerJob::Discover => Ok(ContainerResult::Containers(strop_containers::list_running(
303 &engine, token,
304 )?)),
305 ContainerJob::Attach { id } => {
306 let identity = strop_containers::inspect(&engine, id, token)?;
307 let entries = strop_containers::list_dir(
308 &engine,
309 &strop_containers::ContainerRef::of(&identity)?,
310 "/",
311 token,
312 )?;
313 Ok(ContainerResult::Listing {
314 identity,
315 path: "/".into(),
316 entries,
317 })
318 }
319 ContainerJob::ListDir { id, path } => {
320 let identity = strop_containers::inspect(&engine, id, token)?;
321 let entries = strop_containers::list_dir(
322 &engine,
323 &strop_containers::ContainerRef::of(&identity)?,
324 path,
325 token,
326 )?;
327 Ok(ContainerResult::Listing {
328 identity,
329 path: path.clone(),
330 entries,
331 })
332 }
333 ContainerJob::ReadFile { id, path } => {
334 let identity = strop_containers::inspect(&engine, id, token)?;
335 let text = strop_containers::read_file(
336 &engine,
337 &strop_containers::ContainerRef::of(&identity)?,
338 path,
339 8 * 1024 * 1024,
340 token,
341 )?;
342 Ok(ContainerResult::File {
343 identity,
344 path: path.clone(),
345 text: String::from_utf8_lossy(&text).into_owned(),
346 })
347 }
348 }
349}