qcode/ui/workspace/tab.rs
1//! One tab of the workspace screen: what it opens, where it stands, and the session it draws.
2
3use std::path::{Path, PathBuf};
4use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
5
6use qframe::runtime::TaskId;
7use qframe::widgets::{LogBuffer, LogLevel, LogLine, TerminalSession};
8
9use crate::base::apps::Quiet;
10use crate::bridge;
11use crate::desktop::callback::{Ending, Stop};
12use crate::ui::stalling;
13
14use super::bridge::{Letter, Pasted, Undelivered};
15use super::plan::LaunchFailure;
16
17/// How many lines of an image build a tab keeps: a build prints thousands, and the ones that
18/// matter when it fails are the last.
19const BUILD_LINES: usize = 2000;
20
21/// A tab's identity, which stays the same while tabs are closed and dragged around it.
22///
23/// Work started for a tab comes back as a message long after the tab may have moved, so the
24/// answer names the tab rather than its position.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct TabKey(pub u64);
27
28/// What a tab opens.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum TabKind {
31 /// A shell in the workspace's own container, with no harness in it.
32 Shell,
33 /// A harness in the container of the profile of this name.
34 Profile(String),
35 /// A blank tab asking what it should open. It has no container and never starts one; the
36 /// choice turns it into one of the others in place.
37 New,
38 /// A picture of the workspace, drawn by chafa in the workspace's own container. The text is the
39 /// file's key in the file tree: its path inside `Work/`, written with `/`.
40 Image(String),
41 /// A Markdown document of the workspace, read from the disk and shown by QCode itself, with no
42 /// container at all.
43 Markdown(String),
44 /// A file of the workspace, open in the chosen editor in the workspace's own container.
45 Editor(String),
46 /// A PDF of the workspace: its text, taken out in the workspace's own container and shown by
47 /// QCode, or one of its pages drawn there as a picture.
48 Pdf(String),
49 /// A word processor's document of the workspace: its text, taken out in the workspace's own
50 /// container and shown by QCode.
51 Office(String),
52 /// A sound of the workspace: played in a container of its own made for it, or, when it
53 /// cannot or should not play, described.
54 Sound(String),
55 /// `bash` as root in the container of the profile of this name, asked for by the person to
56 /// install what the system of that container lacks. What it changes stays in this workspace.
57 Admin(String),
58 /// A window of the desktop harness of the profile of this name, open on the person's own
59 /// screen from a container of its own. The tab has no terminal: it says where the window
60 /// stands and offers the two things that can be done to it.
61 Desktop(String),
62}
63
64impl TabKind {
65 /// The key of the workspace's file the tab opens, when it opens one.
66 #[must_use]
67 pub fn file(&self) -> Option<&str> {
68 match self {
69 Self::Image(file)
70 | Self::Markdown(file)
71 | Self::Editor(file)
72 | Self::Pdf(file)
73 | Self::Office(file)
74 | Self::Sound(file) => Some(file),
75 Self::Shell | Self::Profile(_) | Self::New | Self::Desktop(_) | Self::Admin(_) => None,
76 }
77 }
78
79 /// Whether an agent works in a tab of this kind: a harness in a terminal, or the window of a
80 /// desktop harness. Quitting and closing a tab ask about the same tabs through this.
81 #[must_use]
82 pub fn runs_agent(&self) -> bool {
83 matches!(self, Self::Profile(_) | Self::Desktop(_))
84 }
85
86 /// The profile the tab belongs to, when it belongs to one: a harness in a terminal or a
87 /// window on the screen.
88 #[must_use]
89 pub fn profile(&self) -> Option<&str> {
90 match self {
91 Self::Profile(name) | Self::Desktop(name) => Some(name),
92 Self::Shell
93 | Self::New
94 | Self::Admin(_)
95 | Self::Image(_)
96 | Self::Markdown(_)
97 | Self::Editor(_)
98 | Self::Pdf(_)
99 | Self::Office(_)
100 | Self::Sound(_) => None,
101 }
102 }
103}
104
105/// Where a PDF tab stands among its pages.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct Pages {
108 /// How many pages the document has, as its text counted them.
109 pub count: usize,
110 /// The page drawn, or the one that is drawn next; counted from 1.
111 pub page: usize,
112 /// Whether the tab draws the page rather than showing the text.
113 pub drawn: bool,
114}
115
116impl Default for Pages {
117 fn default() -> Self {
118 Self { count: 1, page: 1, drawn: false }
119 }
120}
121
122/// Where the page a window asked to have opened was shown.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum Shown {
125 /// In the person's own browser. Whether its sign-in is carried back is the tab's [`Back`].
126 InBrowser,
127 /// In the sign-in window inside the window's own container, which the person chose instead;
128 /// a sign-in comes back from there by itself, where Google lets it.
129 InWindow,
130 /// Not shown, because the sign-in could not come back from it: its port is taken. The tab's
131 /// [`Back`] says why.
132 Held,
133 /// The person asked for the sign-in window, and the profile's image has none.
134 NoWindow,
135 /// Nowhere: the person is given the address to read.
136 Nowhere,
137}
138
139/// Where the way back of a window's sign-in stands: QCode listening on this machine for the
140/// browser to be sent back, and carrying what arrives to the application.
141#[derive(Debug)]
142pub enum Back {
143 /// Listening; dropping the stop gives the port up.
144 Listening(Stop),
145 /// The application answered the request for the way back.
146 Returned,
147 /// Nothing came back within the wait.
148 TimedOut,
149 /// Another program of this machine holds the port.
150 Taken,
151 /// The system refused to listen on the port, in its own words.
152 Refused(String),
153}
154
155/// Where a tab stands.
156///
157/// Nothing here is a guess: a tab is `Running` only once a session is really attached to a
158/// container, and every way of failing carries what to do next.
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum TabState {
161 /// Not started yet. A tab brought back from the last session waits like a background tab of
162 /// a browser: it starts its container the first time it is shown, so bringing back a dozen
163 /// tabs does not start a dozen containers at once. A blank tab waits for its choice.
164 Waiting,
165 /// The container is being created or started; nothing is attached yet.
166 Starting,
167 /// A session is attached to the running container; for a Markdown tab, which has neither,
168 /// the document is read and shown.
169 Running,
170 /// The program inside the container ended, and the container is still up.
171 Ended {
172 /// Its exit code, or `None` when the engine did not report one.
173 code: Option<u32>,
174 },
175 /// The container is not running any more, so the tab lost its ground rather than its
176 /// program. This is the case the design asks to be named and offered a restart.
177 Stopped,
178 /// The engine refused, and said why.
179 Failed(LaunchFailure),
180 /// The engine does not have the image of the tab's profile, so nothing was made; the tab
181 /// offers to build it.
182 NoImage,
183 /// The profile's image is being built by this task, for the tab to open once it is there.
184 Building(TaskId),
185 /// The file the tab opens is not in the workspace any more.
186 Missing,
187 /// The file the tab opens could not be read, for this reason.
188 Unreadable(String),
189}
190
191impl TabState {
192 /// Whether the tab is waiting for its container.
193 #[must_use]
194 pub fn is_starting(&self) -> bool {
195 matches!(self, Self::Starting)
196 }
197
198 /// Whether the tab can be started again. A tab that is starting or running has nothing to
199 /// restart, so its restart is never offered.
200 #[must_use]
201 pub fn can_restart(&self) -> bool {
202 matches!(
203 self,
204 Self::Ended { .. } | Self::Stopped | Self::Failed(_) | Self::NoImage | Self::Missing | Self::Unreadable(_)
205 )
206 }
207}
208
209/// One tab: a container to enter, the state it is in and the session it draws while it runs.
210#[derive(Debug)]
211pub struct Tab {
212 key: TabKey,
213 /// The tab's id inside its workspace, the one agents know it by: 1 for the workspace's first
214 /// tab, then 2 and on. Given by the workspace when the tab joins it and never given again
215 /// while the workspace is open, so the id of a closed tab never lands on another. 0 until
216 /// then.
217 number: u32,
218 /// The name the person gave the tab, which the strip, the list agents read and the header of
219 /// every message the tab sends show in place of the automatic one.
220 name: Option<String>,
221 kind: TabKind,
222 state: TabState,
223 /// When the tab was opened, in seconds since the Unix epoch.
224 opened: u64,
225 /// The harness conversation the tab shows, when it is known.
226 conversation: Option<String>,
227 session: Option<TerminalSession>,
228 /// The text of a Markdown tab's file, as it was last read, or the text taken out of a PDF.
229 document: Option<String>,
230 /// Whether that text was cut short, being too long to show whole.
231 partial: bool,
232 /// Where a PDF tab stands among its pages.
233 pages: Pages,
234 /// Why a sound tab shows the sound's details rather than playing it, once it is known.
235 quiet: Option<Quiet>,
236 /// The sound server's socket on this machine, once a sound tab found one to play through.
237 socket: Option<PathBuf>,
238 /// Whether a sound tab waits to be asked before it plays: a tab brought back from the last
239 /// session does, because opening QCode again is not asking to hear the sound again.
240 held: bool,
241 /// The web address the window asked to have opened, and where it was shown — `None` while
242 /// the opening is still on its way. A window tab only.
243 sign_in: Option<(String, Option<Shown>)>,
244 /// The port that sign-in comes back to, and where its way back stands. A window tab only.
245 back: Option<(u16, Back)>,
246 /// Counts the sessions this tab has started, so the watch of a session that was replaced by
247 /// a restart is recognised and ignored.
248 run: u64,
249 /// What a harness tab's agent hands QCode's bridge to say which tab it is in.
250 token: String,
251 /// Messages other tabs' agents sent this tab that wait in it, oldest first.
252 letters: Vec<Letter>,
253 /// Whether the waiting letters are shown.
254 letters_shown: bool,
255 /// Why the oldest waiting message could not be typed into the harness, when an attempt was
256 /// made and failed. Cleared as soon as one is typed in.
257 undelivered: Option<Undelivered>,
258 /// A message written into this tab's prompt whose Return has not gone yet, and when it went
259 /// in. The tab owes that Return until it is written; nothing is owed to a tab that owes
260 /// nothing.
261 owed: Option<Pasted>,
262 /// The session's last input that was not the person's: when it started, or the Return QCode
263 /// wrote after the last message it typed in. Input later than this is the person typing.
264 own_input: Option<std::time::Instant>,
265 /// The other tab of an exchange of messages the loop limit ended, named as the person knows
266 /// it, until the person has read the line that says so.
267 stopped: Option<String>,
268 /// What the build of the profile's image said, when this tab built it.
269 build_log: LogBuffer,
270 /// When the build last said something, or when it started: what the silence the page warns
271 /// about is measured from.
272 build_said: Instant,
273 /// Whether that silence has lasted as long as the rule asks, which is what the page says
274 /// beside the build. Never true of a build that has ended.
275 build_stuck: bool,
276}
277
278impl Tab {
279 /// A tab of `kind` opened now, waiting for its container.
280 #[must_use]
281 pub fn new(key: TabKey, kind: TabKind) -> Self {
282 let now = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |since| since.as_secs());
283 Self {
284 key,
285 number: 0,
286 name: None,
287 kind,
288 state: TabState::Starting,
289 opened: now,
290 conversation: None,
291 session: None,
292 document: None,
293 partial: false,
294 pages: Pages::default(),
295 quiet: None,
296 sign_in: None,
297 back: None,
298 socket: None,
299 held: false,
300 run: 0,
301 token: bridge::token(),
302 letters: Vec::new(),
303 letters_shown: false,
304 undelivered: None,
305 owed: None,
306 own_input: None,
307 stopped: None,
308 build_log: LogBuffer::new(BUILD_LINES),
309 build_said: Instant::now(),
310 build_stuck: false,
311 }
312 }
313
314 /// A blank tab opened now, waiting for the person to choose what it opens.
315 #[must_use]
316 pub fn blank(key: TabKey) -> Self {
317 Self { state: TabState::Waiting, ..Self::new(key, TabKind::New) }
318 }
319
320 /// A tab of `kind` opened now that waits to be started, which the screen does as soon as it
321 /// is shown. A tab of one of the workspace's files is opened this way, so it starts exactly
322 /// the way a tab brought back from the last session does.
323 #[must_use]
324 pub fn waiting(key: TabKey, kind: TabKind) -> Self {
325 Self { state: TabState::Waiting, ..Self::new(key, kind) }
326 }
327
328 /// A tab of `kind` brought back from the last session, as it was recorded: opened at
329 /// `opened` and showing `conversation`. It waits to be shown before it starts anything.
330 #[must_use]
331 pub fn restored(key: TabKey, kind: TabKind, opened: u64, conversation: Option<String>) -> Self {
332 Self { opened, conversation, ..Self::waiting(key, kind) }
333 }
334
335 /// The tab's identity.
336 #[must_use]
337 pub fn key(&self) -> TabKey {
338 self.key
339 }
340
341 /// The tab's id inside its workspace, which agents send to; 0 before it joined one.
342 #[must_use]
343 pub fn number(&self) -> u32 {
344 self.number
345 }
346
347 /// Gives the tab its id inside its workspace. Only the workspace does this, as the tab joins it.
348 pub(super) fn numbered(&mut self, number: u32) {
349 self.number = number;
350 }
351
352 /// The name the person gave the tab, when they gave it one.
353 #[must_use]
354 pub fn name(&self) -> Option<&str> {
355 self.name.as_deref()
356 }
357
358 /// Gives the tab the person's `name`, or with `None` takes it away, after which the tab reads
359 /// its automatic name again.
360 pub fn rename(&mut self, name: Option<String>) {
361 self.name = name;
362 }
363
364 /// What the tab opens.
365 #[must_use]
366 pub fn kind(&self) -> &TabKind {
367 &self.kind
368 }
369
370 /// Where the tab stands.
371 #[must_use]
372 pub fn state(&self) -> &TabState {
373 &self.state
374 }
375
376 /// When the tab was opened, in seconds since the Unix epoch.
377 #[must_use]
378 pub fn opened(&self) -> u64 {
379 self.opened
380 }
381
382 /// The harness conversation the tab shows, when it is known.
383 #[must_use]
384 pub fn conversation(&self) -> Option<&str> {
385 self.conversation.as_deref()
386 }
387
388 /// The session drawn in the middle, while there is one.
389 #[must_use]
390 pub fn session(&self) -> Option<&TerminalSession> {
391 self.session.as_ref()
392 }
393
394 /// The text of a Markdown tab's file, once it has been read.
395 #[must_use]
396 pub fn document(&self) -> Option<&str> {
397 self.document.as_deref()
398 }
399
400 /// Whether the text the tab shows was cut short.
401 #[must_use]
402 pub fn is_partial(&self) -> bool {
403 self.partial
404 }
405
406 /// Where a PDF tab stands among its pages.
407 #[must_use]
408 pub fn pages(&self) -> Pages {
409 self.pages
410 }
411
412 /// Moves a PDF tab to `pages`.
413 pub fn turn(&mut self, pages: Pages) {
414 self.pages = pages;
415 }
416
417 /// Why a sound tab shows the sound's details rather than playing it.
418 #[must_use]
419 pub fn quiet(&self) -> Option<Quiet> {
420 self.quiet
421 }
422
423 /// Records why a sound tab shows the sound's details.
424 pub fn keep_quiet(&mut self, why: Quiet) {
425 self.quiet = Some(why);
426 }
427
428 /// The sound server's socket a sound tab plays through.
429 #[must_use]
430 pub fn socket(&self) -> Option<&Path> {
431 self.socket.as_deref()
432 }
433
434 /// Records the socket a sound tab plays through, and that it plays rather than describes.
435 pub fn play_through(&mut self, socket: PathBuf) {
436 self.socket = Some(socket);
437 self.quiet = None;
438 }
439
440 /// Whether a sound tab waits to be asked before it plays.
441 #[must_use]
442 pub fn is_held(&self) -> bool {
443 self.held
444 }
445
446 /// Makes a sound tab wait to be asked before it plays, or lets it play.
447 pub fn hold(&mut self, held: bool) {
448 self.held = held;
449 }
450
451 /// The web address the window asked to have opened, with where it was shown; that is `None`
452 /// until the opening answers.
453 #[must_use]
454 pub fn sign_in(&self) -> Option<(&str, Option<Shown>)> {
455 self.sign_in.as_ref().map(|(address, opened)| (address.as_str(), *opened))
456 }
457
458 /// Records the address the window asked to have opened. The address is shown at once, before
459 /// anything is known about the browser, because reading it is what the person needs most.
460 pub fn asked_to_open(&mut self, address: String) {
461 self.sign_in = Some((address, None));
462 }
463
464 /// Records how the opening of that address went, if it is still the address being opened.
465 pub fn opened_here(&mut self, address: &str, opened: Shown) {
466 if let Some((waiting, how)) = self.sign_in.as_mut()
467 && waiting == address
468 {
469 *how = Some(opened);
470 }
471 }
472
473 /// The port the window's sign-in comes back to, and where its way back stands.
474 #[must_use]
475 pub fn back(&self) -> Option<(u16, &Back)> {
476 self.back.as_ref().map(|(port, back)| (*port, back))
477 }
478
479 /// Whether the tab listens for a sign-in coming back to `port` right now.
480 #[must_use]
481 pub fn listens_on(&self, port: u16) -> bool {
482 matches!(self.back, Some((listening, Back::Listening(_))) if listening == port)
483 }
484
485 /// Records where the way back to `port` stands; a listening that was going on is stopped by
486 /// being replaced.
487 pub fn back_to(&mut self, port: u16, back: Back) {
488 self.back = Some((port, back));
489 }
490
491 /// Takes the end of the listening `id`, if it is still the tab's.
492 pub fn came_back(&mut self, id: u64, ending: Ending) {
493 let Some((port, Back::Listening(stop))) = &self.back else { return };
494 if stop.id() != id {
495 return;
496 }
497 let port = *port;
498 match ending {
499 Ending::Returned => self.back = Some((port, Back::Returned)),
500 Ending::TimedOut => self.back = Some((port, Back::TimedOut)),
501 // Only the tab stops a listening, and it has moved on already.
502 Ending::Stopped => {}
503 }
504 }
505
506 /// Stops listening for the way back, when the tab was.
507 pub fn stop_listening(&mut self) {
508 if matches!(self.back, Some((_, Back::Listening(_)))) {
509 self.back = None;
510 }
511 }
512
513 /// Which session of this tab is the current one.
514 #[must_use]
515 pub fn run(&self) -> u64 {
516 self.run
517 }
518
519 /// The token a harness tab is started with, by which the bridge knows it.
520 #[must_use]
521 pub fn token(&self) -> &str {
522 &self.token
523 }
524
525 /// The messages that wait in this tab, oldest first.
526 #[must_use]
527 pub fn letters(&self) -> &[Letter] {
528 &self.letters
529 }
530
531 /// Whether the waiting messages are shown.
532 #[must_use]
533 pub fn letters_shown(&self) -> bool {
534 self.letters_shown
535 }
536
537 /// Leaves `letter` waiting in the tab.
538 pub fn receive(&mut self, letter: Letter) {
539 self.letters.push(letter);
540 }
541
542 /// Why the waiting messages have not been typed into the harness, when something stops them.
543 #[must_use]
544 pub fn undelivered(&self) -> Option<Undelivered> {
545 self.undelivered
546 }
547
548 /// Records that the oldest waiting message could not be typed in, and why. The message stays
549 /// where it is: dropping it would let the sender believe its work was handed over.
550 pub fn not_delivered(&mut self, why: Undelivered) {
551 self.undelivered = Some(why);
552 }
553
554 /// Records that `pasted` went into this tab's prompt and that the tab owes its Return until
555 /// [`Tab::returned`] says it has gone. A tab takes no further message in the meantime: two
556 /// messages in one prompt line are one message to the harness.
557 pub fn pasted(&mut self, pasted: Pasted) {
558 self.owed = Some(pasted);
559 }
560
561 /// The message whose Return this tab still owes, when it owes one: the paste that went in, the
562 /// harness's last output before it, and when it went, which is what the Return's own wait is
563 /// counted from.
564 #[must_use]
565 pub fn owed(&self) -> Option<&Pasted> {
566 self.owed.as_ref()
567 }
568
569 /// Records that the Return this tab owed has been written, and that the newest thing in the
570 /// terminal is QCode's own: what the person types after it is theirs, as before.
571 pub fn returned(&mut self) {
572 if let Some(session) = &self.session {
573 self.own_input = Some(session.last_input());
574 }
575 self.owed = None;
576 }
577
578 /// Puts `letter` back at the head of the waiting messages, as the oldest of them, and says why
579 /// it is waiting again: its Return could not be written, so the harness that would have read
580 /// it is gone and the message waits for the tab's own next start rather than being lost.
581 pub fn put_back(&mut self, letter: Letter) {
582 self.letters.insert(0, letter);
583 self.undelivered = Some(Undelivered::NotRunning);
584 }
585
586 /// Whether the person has typed into the session since it started and since QCode last wrote
587 /// into it: a message pasted in is QCode's own, and so is the Return that sends it, so neither
588 /// counts as the person typing while the Return is still owed.
589 #[must_use]
590 pub fn person_typed(&self) -> bool {
591 self.session.as_ref().is_some_and(|session| self.own_input.is_none_or(|own| session.last_input() > own))
592 }
593
594 /// Takes the oldest waiting message away, once it has been typed into the harness. The
595 /// session's own last input is left to the Return that sends it, which is written later than
596 /// this paste.
597 pub fn delivered(&mut self) {
598 if !self.letters.is_empty() {
599 self.letters.remove(0);
600 }
601 self.undelivered = None;
602 if self.letters.is_empty() {
603 self.letters_shown = false;
604 }
605 }
606
607 /// Shows the waiting messages, or hides them.
608 pub fn show_letters(&mut self, shown: bool) {
609 self.letters_shown = shown;
610 }
611
612 /// Takes every waiting message out of the tab, oldest first, because its agent asked for them
613 /// from its inbox: they are delivered by being handed over, and none of them is typed in.
614 pub fn take_letters(&mut self) -> Vec<Letter> {
615 self.letters_shown = false;
616 self.undelivered = None;
617 std::mem::take(&mut self.letters)
618 }
619
620 /// Throws the waiting messages away.
621 pub fn discard_letters(&mut self) {
622 self.letters.clear();
623 self.letters_shown = false;
624 self.undelivered = None;
625 }
626
627 /// The other tab of the exchange the loop limit ended, while the tab still says so.
628 #[must_use]
629 pub fn stopped(&self) -> Option<&str> {
630 self.stopped.as_deref()
631 }
632
633 /// Records that the loop limit ended this tab's exchange with the tab named `other`.
634 pub fn stop(&mut self, other: String) {
635 self.stopped = Some(other);
636 }
637
638 /// Stops saying that an exchange was ended, once the person has read it.
639 pub fn unstop(&mut self) {
640 self.stopped = None;
641 }
642
643 /// Turns a blank tab into a tab of `kind` showing `conversation`, opened now and waiting for
644 /// its container. The key stays, so the tab keeps its place in the strip.
645 pub fn choose(&mut self, kind: TabKind, conversation: Option<String>) {
646 // The tab stays the same tab to the agents and the person: its id and its name go with it.
647 let (key, number, name) = (self.key, self.number, self.name.take());
648 *self = Self { conversation, number, name, ..Self::new(key, kind) };
649 }
650
651 /// Records that the tab shows `conversation`, once that is known, so the session file keeps
652 /// it and the tab opens it again next time.
653 pub fn show_conversation(&mut self, conversation: String) {
654 self.conversation = Some(conversation);
655 }
656
657 /// Puts the tab back to waiting for its container and gives up the session it had, which
658 /// ends the program still attached to it.
659 pub fn restarting(&mut self) {
660 self.close_session();
661 self.stop_listening();
662 self.state = TabState::Starting;
663 self.run += 1;
664 }
665
666 /// The tab builds its profile's image in `task`; the log starts empty and the silence starts
667 /// now, since a build that has just begun has said something.
668 pub fn building(&mut self, task: TaskId) {
669 self.build_log.clear();
670 self.state = TabState::Building(task);
671 self.spoke();
672 }
673
674 /// Adds a line the image build said to the tab's log, which is also what ends the silence: a
675 /// build that is saying things is not one to warn about.
676 pub fn build_line(&mut self, text: &str, failed: bool) {
677 let level = if failed { LogLevel::Error } else { LogLevel::Info };
678 self.build_log.push(LogLine::new(level, text));
679 self.spoke();
680 }
681
682 /// What the image build said so far.
683 #[must_use]
684 pub fn build_log(&self) -> &LogBuffer {
685 &self.build_log
686 }
687
688 /// Whether the tab is building its profile's image.
689 #[must_use]
690 pub fn is_building(&self) -> bool {
691 matches!(self.state, TabState::Building(_))
692 }
693
694 /// Whether the build has said nothing for longer than `quiet` as of `now`, which is what the
695 /// page says beside it. A tab that is not building says no whatever its silence is: there is
696 /// no build to be stuck.
697 pub fn build_looked(&mut self, now: Instant, quiet: Duration) {
698 self.build_stuck = self.is_building() && stalling::quiet_for(self.build_said, now, quiet);
699 }
700
701 /// Whether the build has been quiet long enough to be said to be stuck.
702 #[must_use]
703 pub fn is_stuck(&self) -> bool {
704 self.build_stuck
705 }
706
707 /// Notes that the build has said something, so that the silence is counted from this moment and
708 /// the warning beside it goes away.
709 fn spoke(&mut self) {
710 self.build_said = Instant::now();
711 self.build_stuck = false;
712 }
713
714 /// Starts a tab that was waiting to be shown: it now waits for its container instead.
715 pub fn wake(&mut self) {
716 self.state = TabState::Starting;
717 }
718
719 /// Attaches `session` and marks the tab as running.
720 pub fn attached(&mut self, session: TerminalSession) {
721 self.own_input = Some(session.last_input());
722 self.session = Some(session);
723 self.state = TabState::Running;
724 }
725
726 /// Takes the text of a Markdown tab's file, or of a PDF, and shows it; `partial` says it
727 /// was cut short.
728 pub fn read(&mut self, text: String, partial: bool) {
729 self.document = Some(text);
730 self.partial = partial;
731 self.state = TabState::Running;
732 }
733
734 /// Moves the tab to `state`, keeping the session so its last screen stays readable: a tab
735 /// that says the container stopped still shows what the harness printed before it did.
736 pub fn settled(&mut self, state: TabState) {
737 // A window that is not open has nobody to carry a sign-in to.
738 if state != TabState::Running {
739 self.stop_listening();
740 }
741 self.state = state;
742 }
743
744 /// Ends the tab's session, which is what closing a tab or restarting it does. The container
745 /// itself is left alone: other tabs and other workspaces may be using it.
746 ///
747 /// A Return owed to the program that is ending is given up with it: what is left in a prompt
748 /// nobody will read is not a message the tab still holds, so a message whose Return could not
749 /// be written is the only one that comes back, and it has come back by then.
750 pub fn close_session(&mut self) {
751 self.owed = None;
752 if let Some(session) = self.session.take() {
753 session.kill();
754 }
755 }
756}