1use std::collections::VecDeque;
13use std::io;
14use std::path::PathBuf;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{Arc, Condvar, Mutex};
17use std::time::{Duration, Instant};
18
19use gpui_kit::base::async_util;
20
21use crate::terminal::core::{Core, Effects};
22use crate::terminal::frame::{CellSelection, CursorShape, TerminalColors, TerminalFrame, Viewport};
23use crate::terminal::input::TerminalInput;
24use crate::terminal::options::EngineOptions;
25use crate::terminal::source::{FrameHandle, FrameSink, FrameSource, StartOptions};
26
27const INPUT_BYTES: usize = 1 << 20;
28const INPUT_COMMANDS: usize = 256;
29const WRITE_CHUNK: usize = 8192;
30const WRITE_BURST: usize = 64 * 1024;
33const WRITE_RETRY: Duration = Duration::from_millis(50);
36const READ_QUEUE_BYTES: usize = 1 << 20;
39const COMPRESSION_IDLE: Duration = Duration::from_millis(250);
40const COMPRESSION_STEP: Duration = Duration::from_millis(16);
41const FOREGROUND_CHECK: Duration = Duration::from_millis(50);
45
46#[derive(Clone, Debug, PartialEq, Eq)]
48#[non_exhaustive]
49pub struct ExitStatus {
50 pub(crate) code: Option<i32>,
51 pub(crate) signal: Option<String>,
52 pub(crate) runtime: Duration,
53 pub(crate) abnormal: bool,
54}
55
56impl ExitStatus {
57 pub fn new(code: Option<i32>, signal: Option<impl Into<String>>) -> Self {
60 Self {
61 code,
62 signal: signal.map(Into::into),
63 runtime: Duration::ZERO,
64 abnormal: false,
65 }
66 }
67
68 pub fn code(&self) -> Option<i32> {
70 self.code
71 }
72
73 pub fn signal(&self) -> Option<&str> {
75 self.signal.as_deref()
76 }
77
78 pub fn runtime(&self) -> Duration {
80 self.runtime
81 }
82
83 pub fn is_abnormal(&self) -> bool {
85 self.abnormal
86 }
87}
88
89#[derive(Clone, Debug, PartialEq, Eq)]
98#[non_exhaustive]
99pub struct ForegroundProcess {
100 pub(crate) pid: u32,
101 pub(crate) name: String,
102 pub(crate) argv: Vec<String>,
103 pub(crate) cwd: Option<PathBuf>,
104 pub(crate) shell: bool,
105}
106
107impl ForegroundProcess {
108 pub fn new(pid: u32, name: impl Into<String>) -> Self {
110 Self {
111 pid,
112 name: name.into(),
113 argv: Vec::new(),
114 cwd: None,
115 shell: false,
116 }
117 }
118
119 #[must_use]
121 pub fn with_argv(mut self, argv: impl IntoIterator<Item = impl Into<String>>) -> Self {
122 self.argv = argv.into_iter().map(Into::into).collect();
123 self
124 }
125
126 #[must_use]
128 pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
129 self.cwd = Some(cwd.into());
130 self
131 }
132
133 #[must_use]
136 pub fn with_shell(mut self, shell: bool) -> Self {
137 self.shell = shell;
138 self
139 }
140
141 pub fn pid(&self) -> u32 {
143 self.pid
144 }
145
146 pub fn name(&self) -> &str {
148 &self.name
149 }
150
151 pub fn argv(&self) -> &[String] {
154 &self.argv
155 }
156
157 pub fn cwd(&self) -> Option<&std::path::Path> {
159 self.cwd.as_deref()
160 }
161
162 pub fn is_shell(&self) -> bool {
166 self.shell
167 }
168}
169
170#[derive(Clone, Debug, Default, PartialEq, Eq)]
172#[non_exhaustive]
173pub enum TerminalStatus {
174 #[default]
176 Starting,
177 Live,
179 Exited(ExitStatus),
181 Failed(String),
183}
184
185#[derive(Clone, Debug, Default, PartialEq, Eq)]
187#[non_exhaustive]
188pub struct TerminalSnapshot {
189 pub frame: Arc<TerminalFrame>,
191 pub status: TerminalStatus,
193 pub title: String,
195 pub cwd: Option<PathBuf>,
197 pub bell_count: u64,
199 pub clipboard_sequence: u64,
201 pub clipboard: Option<Arc<str>>,
203 pub notification_sequence: u64,
205 pub notification: Option<(Arc<str>, Arc<str>)>,
207 pub at_prompt: bool,
209 pub foreground: Option<ForegroundProcess>,
211}
212
213impl TerminalSnapshot {
214 #[must_use]
216 pub fn for_frame(frame: Arc<TerminalFrame>) -> Self {
217 Self {
218 frame,
219 status: TerminalStatus::Live,
220 ..Self::default()
221 }
222 }
223
224 #[must_use]
226 pub fn with_status(mut self, status: TerminalStatus) -> Self {
227 self.status = status;
228 self
229 }
230
231 #[must_use]
233 pub fn with_title(mut self, title: impl Into<String>) -> Self {
234 self.title = title.into();
235 self
236 }
237
238 #[must_use]
240 pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
241 self.cwd = Some(cwd.into());
242 self
243 }
244
245 #[must_use]
247 pub fn with_bell_count(mut self, count: u64) -> Self {
248 self.bell_count = count;
249 self
250 }
251
252 #[must_use]
255 pub fn with_clipboard(mut self, sequence: u64, text: impl Into<Arc<str>>) -> Self {
256 self.clipboard_sequence = sequence;
257 self.clipboard = Some(text.into());
258 self
259 }
260
261 #[must_use]
263 pub fn with_notification(
264 mut self,
265 sequence: u64,
266 title: impl Into<Arc<str>>,
267 body: impl Into<Arc<str>>,
268 ) -> Self {
269 self.notification_sequence = sequence;
270 self.notification = Some((title.into(), body.into()));
271 self
272 }
273
274 #[must_use]
276 pub fn with_at_prompt(mut self, at_prompt: bool) -> Self {
277 self.at_prompt = at_prompt;
278 self
279 }
280
281 #[must_use]
283 pub fn with_foreground(mut self, foreground: ForegroundProcess) -> Self {
284 self.foreground = Some(foreground);
285 self
286 }
287}
288
289pub trait ByteSource: Send + 'static {
294 fn start(
297 self: Box<Self>,
298 sink: ByteSink,
299 viewport: Viewport,
300 ) -> io::Result<Box<dyn ByteHandle>>;
301}
302
303pub trait ByteHandle: Send {
305 fn write(&self, bytes: &[u8]) -> io::Result<usize>;
310 fn resize(&self, viewport: Viewport) -> io::Result<()>;
312 fn try_wait(&self) -> io::Result<Option<ExitStatus>>;
314 fn close(&self);
316 fn foreground(&self) -> Option<ForegroundProcess> {
320 None
321 }
322}
323
324#[derive(Clone, Debug)]
326pub struct ByteSink {
327 shared: Arc<Shared>,
328}
329
330impl ByteSink {
331 pub fn write(&self, bytes: &[u8]) -> bool {
336 let mut mail = self.shared.lock();
337 if self.shared.closing.load(Ordering::Acquire) {
338 return false;
339 }
340 mail.reads.push_back(bytes.to_vec());
341 mail.read_bytes += bytes.len();
342 mail.generation = mail.generation.wrapping_add(1);
343 self.shared.wake.notify_one();
344 true
345 }
346
347 pub fn wait_for_room(&self) -> bool {
352 let mut mail = self.shared.lock();
353 while mail.read_bytes >= READ_QUEUE_BYTES && !self.shared.closing.load(Ordering::Acquire) {
354 mail = self
355 .shared
356 .room
357 .wait(mail)
358 .unwrap_or_else(std::sync::PoisonError::into_inner);
359 }
360 !self.shared.closing.load(Ordering::Acquire)
361 }
362
363 pub fn writable(&self) {
366 let mut mail = self.shared.lock();
367 mail.writable = true;
368 self.shared.bump(&mut mail);
369 }
370
371 pub fn finished(&self) {
373 let mut mail = self.shared.lock();
374 mail.reader_exited = true;
375 mail.generation = mail.generation.wrapping_add(1);
376 self.shared.wake.notify_one();
377 }
378}
379
380#[derive(Clone, Copy, Debug, PartialEq, Eq)]
382pub enum InputRejected {
383 QueueFull,
385 Closed,
387}
388
389impl std::fmt::Display for InputRejected {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 match self {
392 Self::QueueFull => f.write_str("terminal input queue is full"),
393 Self::Closed => f.write_str("terminal is closed"),
394 }
395 }
396}
397
398impl std::error::Error for InputRejected {}
399
400#[derive(Default)]
401struct Mailbox {
402 commands: VecDeque<TerminalInput>,
403 bytes: usize,
404 reads: VecDeque<Vec<u8>>,
405 read_bytes: usize,
407 writable: bool,
409 viewport: Option<Viewport>,
410 visible: bool,
411 credit: bool,
412 painted: bool,
414 reader_exited: bool,
415 reveal: bool,
416 interrupt: Option<TerminalInput>,
417 copy: Option<(CellSelection, async_util::Sender<io::Result<String>>)>,
418 cursor: Option<(CursorShape, bool)>,
419 colors: Option<TerminalColors>,
420 generation: u64,
421}
422
423#[derive(Debug)]
424struct Shared {
425 mailbox: Mutex<Mailbox>,
426 wake: Condvar,
427 room: Condvar,
429 closing: AtomicBool,
430}
431
432impl std::fmt::Debug for Mailbox {
433 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434 f.debug_struct("Mailbox")
435 .field("commands", &self.commands.len())
436 .field("reads", &self.reads.len())
437 .finish_non_exhaustive()
438 }
439}
440
441impl Shared {
442 fn lock(&self) -> std::sync::MutexGuard<'_, Mailbox> {
443 self.mailbox
444 .lock()
445 .unwrap_or_else(std::sync::PoisonError::into_inner)
446 }
447
448 fn bump(&self, mail: &mut Mailbox) {
449 mail.generation = mail.generation.wrapping_add(1);
450 self.wake.notify_one();
451 }
452
453 fn set_closing(&self) {
455 self.closing.store(true, Ordering::Release);
456 self.room.notify_all();
457 }
458}
459
460#[derive(Clone, Debug)]
464pub(crate) struct EngineHandle {
465 shared: Arc<Shared>,
466}
467
468impl EngineHandle {
469 pub fn input(&self, input: TerminalInput) -> Result<(), InputRejected> {
471 let mut mail = self.shared.lock();
472 if self.shared.closing.load(Ordering::Acquire) {
473 return Err(InputRejected::Closed);
474 }
475 if input.is_interrupt() {
476 let discarded: usize = mail.commands.iter().map(TerminalInput::budget).sum();
477 mail.commands.clear();
478 mail.bytes = mail.bytes.saturating_sub(discarded);
479 mail.interrupt = Some(input);
480 self.shared.bump(&mut mail);
481 return Ok(());
482 }
483 let budget = input.budget();
484 if mail.commands.len() >= INPUT_COMMANDS || budget > INPUT_BYTES.saturating_sub(mail.bytes)
485 {
486 return Err(InputRejected::QueueFull);
487 }
488 mail.bytes += budget;
489 mail.commands.push_back(input);
490 self.shared.bump(&mut mail);
491 Ok(())
492 }
493
494 #[must_use]
496 pub fn input_backlog(&self) -> usize {
497 self.shared.lock().bytes
498 }
499
500 pub fn resize(&self, viewport: Viewport) {
502 let mut mail = self.shared.lock();
503 mail.viewport = Some(viewport);
504 self.shared.bump(&mut mail);
505 }
506
507 pub fn set_visible(&self, visible: bool) {
510 let mut mail = self.shared.lock();
511 if mail.visible != visible {
512 mail.visible = visible;
513 mail.credit = visible;
514 mail.reveal = visible;
515 self.shared.bump(&mut mail);
516 }
517 }
518
519 pub fn request_frame(&self) {
522 let mut mail = self.shared.lock();
523 if !mail.credit || !mail.painted {
524 mail.credit = true;
525 mail.painted = true;
526 self.shared.bump(&mut mail);
527 }
528 }
529
530 pub fn set_cursor(&self, shape: CursorShape, blinking: bool) {
532 let mut mail = self.shared.lock();
533 mail.cursor = Some((shape, blinking));
534 self.shared.bump(&mut mail);
535 }
536
537 pub fn set_colors(&self, colors: TerminalColors) {
539 let mut mail = self.shared.lock();
540 mail.colors = Some(colors);
541 self.shared.bump(&mut mail);
542 }
543
544 pub fn copy(&self, selection: CellSelection) -> async_util::Receiver<io::Result<String>> {
547 let (tx, rx) = async_util::unbounded();
548 let mut mail = self.shared.lock();
549 mail.copy = Some((selection, tx));
550 self.shared.bump(&mut mail);
551 rx
552 }
553
554 pub fn close(&self) {
556 let mut mail = self.shared.lock();
557 self.shared.set_closing();
558 self.shared.bump(&mut mail);
559 }
560}
561
562#[derive(Debug)]
564pub(crate) struct EngineConfig {
565 pub options: EngineOptions,
567 pub viewport: Viewport,
569 pub colors: TerminalColors,
571 pub visible: bool,
573 pub cursor: Option<(CursorShape, bool)>,
575 pub input: Vec<u8>,
577 pub abnormal_exit_runtime: Duration,
579}
580
581impl Default for EngineConfig {
582 fn default() -> Self {
583 Self {
584 options: EngineOptions::default(),
585 viewport: Viewport::default(),
586 colors: TerminalColors::default(),
587 visible: true,
588 cursor: None,
589 input: Vec::new(),
590 abnormal_exit_runtime: Duration::from_millis(250),
591 }
592 }
593}
594
595pub(crate) fn spawn(
599 source: Box<dyn ByteSource>,
600 config: EngineConfig,
601 sink: FrameSink,
602) -> io::Result<EngineHandle> {
603 let shared = Arc::new(Shared {
604 mailbox: Mutex::new(Mailbox {
607 visible: config.visible,
608 credit: false,
609 ..Mailbox::default()
610 }),
611 wake: Condvar::new(),
612 room: Condvar::new(),
613 closing: AtomicBool::new(false),
614 });
615 let handle = EngineHandle {
616 shared: shared.clone(),
617 };
618 let byte_sink = ByteSink {
619 shared: shared.clone(),
620 };
621 std::thread::Builder::new()
622 .name("gpui-cn-terminal".to_owned())
623 .spawn(move || {
624 let mut owner = match Owner::start(source, byte_sink, config, shared.clone(), &sink) {
625 Ok(owner) => owner,
626 Err(error) => {
627 shared.set_closing();
628 sink.publish(Arc::new(TerminalSnapshot {
629 status: TerminalStatus::Failed(error.to_string()),
630 ..TerminalSnapshot::default()
631 }));
632 return;
633 }
634 };
635 owner.run();
636 })?;
637 Ok(handle)
638}
639
640#[derive(Debug)]
642pub struct Engine<S> {
643 source: S,
644 options: EngineOptions,
645 abnormal_exit_runtime: Duration,
646 input: Vec<u8>,
647}
648
649impl<S: ByteSource> Engine<S> {
650 #[must_use]
652 pub fn new(source: S, options: EngineOptions) -> Self {
653 Self {
654 source,
655 options,
656 abnormal_exit_runtime: Duration::from_millis(250),
657 input: Vec::new(),
658 }
659 }
660
661 #[must_use]
663 pub fn with_abnormal_exit_runtime(mut self, runtime: Duration) -> Self {
664 self.abnormal_exit_runtime = runtime;
665 self
666 }
667
668 #[must_use]
670 pub fn with_input(mut self, input: impl Into<Vec<u8>>) -> Self {
671 self.input = input.into();
672 self
673 }
674}
675
676impl<S: ByteSource> FrameSource for Engine<S> {
677 fn start(
678 self: Box<Self>,
679 sink: FrameSink,
680 options: StartOptions,
681 ) -> io::Result<Box<dyn FrameHandle>> {
682 let handle = spawn(
683 Box::new(self.source),
684 EngineConfig {
685 options: self.options,
686 viewport: options.viewport,
687 colors: options.colors,
688 visible: options.visible,
689 cursor: options.cursor,
690 input: self.input,
691 abnormal_exit_runtime: self.abnormal_exit_runtime,
692 },
693 sink,
694 )?;
695 Ok(Box::new(handle))
696 }
697}
698
699impl FrameHandle for EngineHandle {
700 fn input(&self, input: TerminalInput) -> Result<(), InputRejected> {
701 Self::input(self, input)
702 }
703
704 fn input_backlog(&self) -> usize {
705 Self::input_backlog(self)
706 }
707
708 fn resize(&self, viewport: Viewport) {
709 Self::resize(self, viewport);
710 }
711
712 fn set_visible(&self, visible: bool) {
713 Self::set_visible(self, visible);
714 }
715
716 fn request_frame(&self) {
717 Self::request_frame(self);
718 }
719
720 fn set_colors(&self, colors: TerminalColors) {
721 Self::set_colors(self, colors);
722 }
723
724 fn set_cursor(&self, shape: CursorShape, blinking: bool) {
725 Self::set_cursor(self, shape, blinking);
726 }
727
728 fn copy(&self, selection: CellSelection) -> Option<async_util::Receiver<io::Result<String>>> {
729 Some(Self::copy(self, selection))
730 }
731
732 fn close(&self) {
733 Self::close(self);
734 }
735}
736
737struct PendingWrite {
738 bytes: Vec<u8>,
739 offset: usize,
740 budget: usize,
741 paste: bool,
742}
743
744struct CoreSlot {
746 live: Option<Core>,
747 parked: Option<crate::terminal::core::Parked>,
748}
749
750impl std::ops::Deref for CoreSlot {
751 type Target = Core;
752
753 fn deref(&self) -> &Core {
754 self.live
755 .as_ref()
756 .expect("the core is restored before it is used")
757 }
758}
759
760impl std::ops::DerefMut for CoreSlot {
761 fn deref_mut(&mut self) -> &mut Core {
762 self.live
763 .as_mut()
764 .expect("the core is restored before it is used")
765 }
766}
767
768struct Parking {
770 options: EngineOptions,
771 key: u64,
772 last_use: Instant,
774}
775
776struct Owner {
777 shared: Arc<Shared>,
778 core: CoreSlot,
779 parking: Parking,
780 handle: Box<dyn ByteHandle>,
781 sink: FrameSink,
782 snapshot: TerminalSnapshot,
783 urgent: Vec<u8>,
786 pending: Option<PendingWrite>,
787 write_blocked: bool,
790 exit: Option<ExitStatus>,
791 started: Instant,
792 abnormal_exit_runtime: Duration,
793 compression_deadline: Option<Instant>,
794 foreground_due: Option<Instant>,
796 dirty: bool,
797 force: bool,
798 effects: Effects,
799}
800
801impl Owner {
802 fn start(
803 source: Box<dyn ByteSource>,
804 byte_sink: ByteSink,
805 config: EngineConfig,
806 shared: Arc<Shared>,
807 sink: &FrameSink,
808 ) -> io::Result<Self> {
809 let mut core = Core::new(config.viewport, &config.options, config.colors)?;
810 if let Some((shape, blinking)) = config.cursor {
811 core.set_cursor(shape, blinking)?;
812 }
813 let frame = core
814 .frame(true)?
815 .ok_or_else(|| io::Error::other("initial terminal frame missing"))?;
816 let handle = source.start(byte_sink, config.viewport)?;
817 let snapshot = TerminalSnapshot {
818 frame,
819 status: TerminalStatus::Live,
820 ..TerminalSnapshot::default()
821 };
822 sink.publish(Arc::new(snapshot.clone()));
823 Ok(Self {
824 shared,
825 core: CoreSlot {
826 live: Some(core),
827 parked: None,
828 },
829 parking: Parking {
830 options: config.options.clone(),
831 key: crate::terminal::park::next_key(),
832 last_use: Instant::now(),
833 },
834 handle,
835 sink: sink.clone(),
836 snapshot,
837 urgent: config.input.clone(),
838 pending: None,
839 write_blocked: false,
840 exit: None,
841 started: Instant::now(),
842 abnormal_exit_runtime: config.abnormal_exit_runtime,
843 compression_deadline: None,
844 foreground_due: Some(Instant::now() + FOREGROUND_CHECK),
845 dirty: true,
846 force: false,
847 effects: Effects::default(),
848 })
849 }
850
851 fn run(&mut self) {
852 while !self.shared.closing.load(Ordering::Acquire) {
853 if let Err(error) = self.turn() {
854 self.snapshot.status = TerminalStatus::Failed(error.to_string());
855 self.publish();
856 break;
857 }
858 }
859 self.cleanup();
860 }
861
862 fn turn(&mut self) -> io::Result<()> {
863 let (
864 reads,
865 viewport,
866 command,
867 visible,
868 credit,
869 eof,
870 reveal,
871 interrupt,
872 copy,
873 cursor,
874 colors,
875 painted,
876 generation,
877 ) = {
878 let mut mail = self.shared.lock();
879 let interrupt = mail.interrupt.take();
880 let command = if interrupt.is_none() && self.pending.is_none() {
881 mail.commands.pop_front()
882 } else {
883 None
884 };
885 mail.read_bytes = 0;
886 self.shared.room.notify_all();
887 if std::mem::take(&mut mail.writable) {
888 self.write_blocked = false;
889 }
890 (
891 std::mem::take(&mut mail.reads),
892 mail.viewport.take(),
893 command,
894 mail.visible,
895 mail.credit,
896 mail.reader_exited,
897 std::mem::take(&mut mail.reveal),
898 interrupt,
899 mail.copy.take(),
900 mail.cursor.take(),
901 mail.colors.take(),
902 std::mem::take(&mut mail.painted),
903 mail.generation,
904 )
905 };
906 let had_work = !reads.is_empty()
907 || viewport.is_some()
908 || command.is_some()
909 || reveal
910 || interrupt.is_some()
911 || copy.is_some()
912 || cursor.is_some()
913 || colors.is_some();
914 {
915 if had_work || painted || (eof && self.exit.is_none()) {
918 self.parking.last_use = Instant::now();
919 self.unpark()?;
920 } else if self.core.live.is_none() {
921 self.wait(generation, None);
922 return Ok(());
923 }
924 }
925 if reveal {
926 self.dirty = true;
927 self.force = true;
928 }
929 if let Some((shape, blinking)) = cursor {
930 self.core.set_cursor(shape, blinking)?;
931 self.dirty = true;
932 }
933 if let Some(colors) = colors {
934 self.core.set_colors(colors)?;
935 self.dirty = true;
936 self.force = true;
937 }
938 if !reads.is_empty() {
939 for chunk in reads {
940 self.core.write(&chunk, &mut self.effects);
941 }
942 self.compression_deadline = Some(Instant::now() + COMPRESSION_IDLE);
943 self.foreground_due
944 .get_or_insert_with(|| Instant::now() + FOREGROUND_CHECK);
945 self.dirty = true;
946 self.apply_effects()?;
947 }
948 if let Some(interrupt) = interrupt {
949 let partial_paste = self
950 .pending
951 .as_ref()
952 .is_some_and(|pending| pending.paste && pending.offset > 0);
953 if let Some(pending) = self.pending.take() {
954 self.release(pending.budget);
955 }
956 let mut bytes = if partial_paste {
957 b"\x1b[201~".to_vec()
958 } else {
959 Vec::new()
960 };
961 bytes.extend(self.core.input(interrupt)?);
962 self.urgent.extend(bytes);
963 self.dirty = true;
964 }
965 if let Some(viewport) = viewport {
966 self.core.resize(viewport, &mut self.effects)?;
967 self.handle.resize(viewport)?;
968 self.apply_effects()?;
969 self.dirty = true;
970 self.force = true;
971 }
972 if let Some(input) = command {
973 let budget = input.budget();
974 if self.exit.is_some() && !matches!(input, TerminalInput::Scroll(_)) {
975 self.release(budget);
976 } else {
977 let paste = matches!(input, TerminalInput::Paste(_));
978 match self.core.input(input) {
979 Ok(bytes) if !bytes.is_empty() => {
980 self.pending = Some(PendingWrite {
981 bytes,
982 offset: 0,
983 budget,
984 paste,
985 });
986 }
987 Ok(_) => self.release(budget),
988 Err(error) => {
989 self.release(budget);
990 return Err(error);
991 }
992 }
993 self.dirty = true;
994 }
995 }
996 if let Some((selection, reply)) = copy {
997 let _ = reply.try_send(self.core.copy(selection));
998 }
999 self.flush()?;
1000 if eof
1001 && self.exit.is_none()
1002 && let Some(mut status) = self.handle.try_wait()?
1003 {
1004 status.runtime = self.started.elapsed();
1005 status.abnormal = status.runtime <= self.abnormal_exit_runtime;
1006 self.exit = Some(status.clone());
1007 self.snapshot.status = TerminalStatus::Exited(status);
1008 self.dirty = true;
1009 self.force = true;
1010 }
1011 if self.foreground_due.is_some_and(|due| Instant::now() >= due) {
1012 self.foreground_due = None;
1013 let foreground = if self.exit.is_some() {
1014 None
1015 } else {
1016 self.handle.foreground()
1017 };
1018 if foreground != self.snapshot.foreground {
1019 self.snapshot.foreground = foreground;
1020 self.publish();
1023 }
1024 }
1025 let exited = self.exit.is_some();
1026 let held = self.core.held();
1027 if self.dirty && ((visible && credit && !held) || (exited && self.force)) {
1028 if let Some(frame) = self.core.frame(self.force)? {
1029 self.snapshot.frame = frame;
1030 self.snapshot.at_prompt = self.core.at_prompt();
1031 self.shared.lock().credit = false;
1032 self.publish();
1033 }
1034 self.dirty = false;
1035 self.force = false;
1036 }
1037 if self
1038 .compression_deadline
1039 .is_some_and(|deadline| Instant::now() >= deadline)
1040 {
1041 let complete = self.core.compress();
1042 self.compression_deadline = (!complete).then(|| Instant::now() + COMPRESSION_STEP);
1043 }
1044 let unsent = self.pending.is_some() || !self.urgent.is_empty();
1045 if !had_work && !(unsent && !self.write_blocked) {
1046 let delay = if unsent {
1047 Some(WRITE_RETRY)
1048 } else if self.dirty && visible && credit {
1049 self.core
1050 .hold_remaining()
1051 .or(Some(Duration::from_millis(1)))
1052 } else if let Some(deadline) = self.compression_deadline {
1053 Some(deadline.saturating_duration_since(Instant::now()))
1054 } else if eof && self.exit.is_none() {
1055 Some(Duration::from_millis(20))
1056 } else {
1057 None
1058 };
1059 let delay = match self.foreground_due {
1060 Some(due) => {
1061 let until = due.saturating_duration_since(Instant::now());
1062 Some(delay.map_or(until, |delay| delay.min(until)))
1063 }
1064 None => delay,
1065 };
1066 let delay = match self.park_after() {
1067 Some(_) if self.park_if_idle() => None,
1068 Some(after) => Some(delay.map_or(after, |delay| delay.min(after))),
1069 None => delay,
1070 };
1071 self.wait(generation, delay);
1072 }
1073 Ok(())
1074 }
1075
1076 fn wait(&self, generation: u64, delay: Option<Duration>) {
1078 let mail = self.shared.lock();
1079 if mail.generation == generation && !self.shared.closing.load(Ordering::Acquire) {
1080 match delay {
1081 None => drop(
1082 self.shared
1083 .wake
1084 .wait(mail)
1085 .unwrap_or_else(std::sync::PoisonError::into_inner),
1086 ),
1087 Some(delay) => drop(
1088 self.shared
1089 .wake
1090 .wait_timeout(mail, delay.max(Duration::from_millis(1)))
1091 .unwrap_or_else(std::sync::PoisonError::into_inner),
1092 ),
1093 }
1094 }
1095 }
1096
1097 fn park_after(&mut self) -> Option<Duration> {
1100 let park = &self.parking.options.park;
1101 if !park.enabled
1102 || self.core.live.is_none()
1103 || self.pending.is_some()
1104 || self.exit.is_some()
1105 || self.core.held()
1106 {
1107 return None;
1108 }
1109 Some(park.idle.saturating_sub(self.parking.last_use.elapsed()))
1110 }
1111
1112 fn park_if_idle(&mut self) -> bool {
1115 if self.park_after() != Some(Duration::ZERO) {
1116 return false;
1117 }
1118 let store = self.parking.options.park.store.clone();
1119 let parked = self
1120 .core
1121 .park()
1122 .and_then(|(bytes, parked)| store.save(self.parking.key, &bytes).map(|()| parked));
1123 match parked {
1124 Ok(parked) => {
1125 self.core.live = None;
1126 self.core.parked = Some(parked);
1127 self.compression_deadline = None;
1128 true
1129 }
1130 Err(_) => {
1132 self.parking.last_use = Instant::now();
1133 false
1134 }
1135 }
1136 }
1137
1138 fn unpark(&mut self) -> io::Result<()> {
1140 let Some(parked) = self.core.parked.take() else {
1141 return Ok(());
1142 };
1143 let bytes = self.parking.options.park.store.take(self.parking.key)?;
1144 self.core.live = Some(Core::restore(&bytes, parked, &self.parking.options)?);
1145 Ok(())
1146 }
1147
1148 fn release(&self, budget: usize) {
1149 let mut mail = self.shared.lock();
1150 mail.bytes = mail.bytes.saturating_sub(budget);
1151 }
1152
1153 fn flush(&mut self) -> io::Result<()> {
1157 if self.write_blocked {
1158 return Ok(());
1159 }
1160 if !self.urgent.is_empty() {
1161 let written = self.handle.write(&self.urgent)?;
1162 self.urgent.drain(..written.min(self.urgent.len()));
1163 if !self.urgent.is_empty() {
1164 self.write_blocked = true;
1165 return Ok(());
1166 }
1167 }
1168 let mut burst = WRITE_BURST;
1169 while burst > 0 {
1170 let Some(pending) = &mut self.pending else {
1171 break;
1172 };
1173 let end = (pending.offset + WRITE_CHUNK.min(burst)).min(pending.bytes.len());
1174 let offered = end - pending.offset;
1175 let written = self
1176 .handle
1177 .write(&pending.bytes[pending.offset..end])?
1178 .min(offered);
1179 pending.offset += written;
1180 burst = burst.saturating_sub(written);
1181 if pending.offset >= pending.bytes.len() {
1182 let budget = pending.budget;
1183 self.pending = None;
1184 self.release(budget);
1185 break;
1186 }
1187 if written < offered {
1188 self.write_blocked = true;
1189 break;
1190 }
1191 }
1192 Ok(())
1193 }
1194
1195 fn apply_effects(&mut self) -> io::Result<()> {
1196 let effects = std::mem::take(&mut self.effects);
1197 self.urgent.extend_from_slice(&effects.replies);
1198 let mut changed = false;
1199 if effects.title_changed {
1200 self.snapshot.title = self.core.title().to_owned();
1201 changed = true;
1202 }
1203 if effects.cwd_changed {
1204 self.snapshot.cwd = self.core.cwd().cloned();
1205 changed = true;
1206 }
1207 if effects.bells > 0 {
1208 self.snapshot.bell_count += effects.bells;
1209 changed = true;
1210 }
1211 if let Some(text) = effects.clipboard.last() {
1212 self.snapshot.clipboard_sequence += effects.clipboard.len() as u64;
1213 self.snapshot.clipboard = Some(Arc::from(text.as_str()));
1214 changed = true;
1215 }
1216 if let Some((title, body)) = effects.notifications.last() {
1217 self.snapshot.notification_sequence += effects.notifications.len() as u64;
1218 self.snapshot.notification =
1219 Some((Arc::from(title.as_str()), Arc::from(body.as_str())));
1220 changed = true;
1221 }
1222 if changed {
1223 self.dirty = true;
1224 self.force = true;
1225 }
1226 Ok(())
1227 }
1228
1229 fn publish(&self) {
1230 self.sink.publish(Arc::new(self.snapshot.clone()));
1231 }
1232
1233 fn cleanup(&mut self) {
1234 self.shared.set_closing();
1235 {
1236 let mut mail = self.shared.lock();
1237 mail.commands.clear();
1238 mail.bytes = 0;
1239 mail.reads.clear();
1240 mail.read_bytes = 0;
1241 }
1242 self.pending = None;
1243 if self.core.parked.take().is_some() {
1244 self.parking.options.park.store.remove(self.parking.key);
1245 }
1246 self.handle.close();
1247 if self.exit.is_none() {
1248 let deadline = Instant::now() + Duration::from_secs(2);
1249 while Instant::now() < deadline {
1250 if let Ok(Some(mut status)) = self.handle.try_wait() {
1251 status.runtime = self.started.elapsed();
1252 status.abnormal = status.runtime <= self.abnormal_exit_runtime;
1253 self.snapshot.status = TerminalStatus::Exited(status.clone());
1254 self.exit = Some(status);
1255 break;
1256 }
1257 std::thread::sleep(Duration::from_millis(20));
1258 }
1259 }
1260 self.publish();
1261 }
1262}