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 text: format!(
195 "{} {} {}",
196 &identity.id[..12],
197 identity.name,
198 identity.image
199 ),
200 payload: strop_picker::Payload::Container(identity.id.clone()),
201 })
202 .collect();
203 self.set_picker(super::picker::PickerGlue::diagnostics(
204 strop_picker::Picker::new(strop_picker::Kind::Containers, items, false),
205 ));
206 }
207 Outcome::Success(ContainerResult::Listing {
208 identity,
209 path,
210 entries,
211 }) if focused => {
212 let Ok(reference) = strop_containers::ContainerRef::of(&identity) else {
213 self.message = "container identity failed validation".into();
214 return;
215 };
216 self.workspaces.bind(
217 strop_workspace::Filesystem::Container(reference.id().clone()),
218 None,
219 );
220 self.containers
221 .attached
222 .insert(identity.id.clone(), identity.clone());
223 let short = &identity.id[..12];
224 let mut text = format!(
225 "container {} ({}:{}) — {} entry(s); enter opens, q closes\n",
226 identity.name,
227 short,
228 path,
229 entries.len()
230 );
231 for entry in &entries {
232 let marker = match entry.kind {
233 strop_containers::DirEntryKind::Dir => "d",
234 strop_containers::DirEntryKind::Symlink => "l",
235 _ => " ",
236 };
237 let suffix = if entry.kind == strop_containers::DirEntryKind::Dir {
238 "/"
239 } else {
240 ""
241 };
242 text.push_str(&format!("{} {}{}\n", marker, entry.name, suffix));
243 }
244 let mut buffer = Buffer::from_text(&text);
245 buffer.name = Some(format!("container:{short}:{path}"));
246 let id = self.docs.insert(Document::output(buffer));
247 self.drop_stale_scratch(id);
248 self.containers
249 .buffers
250 .insert(id, (identity.id.clone(), path));
251 self.containers.entries.insert(id, entries);
252 self.switch_to(id);
253 self.set_head(0);
254 }
255 Outcome::Success(ContainerResult::File {
256 identity,
257 path,
258 text,
259 }) if focused => {
260 let short = identity.id[..12].to_string();
261 let mut buffer = Buffer::from_text(&text);
262 buffer.name = Some(format!("container:{short}:{path}"));
263 let Ok(container) = strop_workspace::ContainerId::canonical(identity.id.clone())
264 else {
265 self.message = "container identity failed validation".into();
266 return;
267 };
268 let id = self.docs.insert(Document::container_file(
269 buffer,
270 container,
271 std::path::PathBuf::from(&path),
272 ));
273 self.drop_stale_scratch(id);
274 self.containers
275 .buffers
276 .insert(id, (identity.id.clone(), path));
277 self.switch_to(id);
278 self.set_head(0);
279 self.lsp_maybe_attach();
282 }
283 Outcome::Success(_) => {}
284 Outcome::Failed { failure, .. } if focused => {
285 self.message = failure.message;
286 }
287 _ => {}
288 }
289 }
290}
291
292fn run_container_job(
296 job: &ContainerJob,
297 token: &strop_core::worker::CancelToken,
298) -> Result<ContainerResult, strop_containers::ContainerError> {
299 let engine = strop_containers::engine(token)?;
300 match job {
301 ContainerJob::Discover => Ok(ContainerResult::Containers(strop_containers::list_running(
302 &engine, token,
303 )?)),
304 ContainerJob::Attach { id } => {
305 let identity = strop_containers::inspect(&engine, id, token)?;
306 let entries = strop_containers::list_dir(
307 &engine,
308 &strop_containers::ContainerRef::of(&identity)?,
309 "/",
310 token,
311 )?;
312 Ok(ContainerResult::Listing {
313 identity,
314 path: "/".into(),
315 entries,
316 })
317 }
318 ContainerJob::ListDir { id, path } => {
319 let identity = strop_containers::inspect(&engine, id, token)?;
320 let entries = strop_containers::list_dir(
321 &engine,
322 &strop_containers::ContainerRef::of(&identity)?,
323 path,
324 token,
325 )?;
326 Ok(ContainerResult::Listing {
327 identity,
328 path: path.clone(),
329 entries,
330 })
331 }
332 ContainerJob::ReadFile { id, path } => {
333 let identity = strop_containers::inspect(&engine, id, token)?;
334 let text = strop_containers::read_file(
335 &engine,
336 &strop_containers::ContainerRef::of(&identity)?,
337 path,
338 8 * 1024 * 1024,
339 token,
340 )?;
341 Ok(ContainerResult::File {
342 identity,
343 path: path.clone(),
344 text: String::from_utf8_lossy(&text).into_owned(),
345 })
346 }
347 }
348}