Skip to main content

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}