use std::collections::VecDeque;
use std::io;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use gpui_kit::base::async_util;
use crate::terminal::core::{Core, Effects};
use crate::terminal::frame::{CellSelection, CursorShape, TerminalColors, TerminalFrame, Viewport};
use crate::terminal::input::TerminalInput;
use crate::terminal::options::EngineOptions;
use crate::terminal::source::{FrameHandle, FrameSink, FrameSource, StartOptions};
const INPUT_BYTES: usize = 1 << 20;
const INPUT_COMMANDS: usize = 256;
const WRITE_CHUNK: usize = 8192;
const WRITE_BURST: usize = 64 * 1024;
const WRITE_RETRY: Duration = Duration::from_millis(50);
const READ_QUEUE_BYTES: usize = 1 << 20;
const COMPRESSION_IDLE: Duration = Duration::from_millis(250);
const COMPRESSION_STEP: Duration = Duration::from_millis(16);
const FOREGROUND_CHECK: Duration = Duration::from_millis(50);
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ExitStatus {
pub(crate) code: Option<i32>,
pub(crate) signal: Option<String>,
pub(crate) runtime: Duration,
pub(crate) abnormal: bool,
}
impl ExitStatus {
pub fn new(code: Option<i32>, signal: Option<impl Into<String>>) -> Self {
Self {
code,
signal: signal.map(Into::into),
runtime: Duration::ZERO,
abnormal: false,
}
}
pub fn code(&self) -> Option<i32> {
self.code
}
pub fn signal(&self) -> Option<&str> {
self.signal.as_deref()
}
pub fn runtime(&self) -> Duration {
self.runtime
}
pub fn is_abnormal(&self) -> bool {
self.abnormal
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ForegroundProcess {
pub(crate) pid: u32,
pub(crate) name: String,
pub(crate) argv: Vec<String>,
pub(crate) cwd: Option<PathBuf>,
pub(crate) shell: bool,
}
impl ForegroundProcess {
pub fn new(pid: u32, name: impl Into<String>) -> Self {
Self {
pid,
name: name.into(),
argv: Vec::new(),
cwd: None,
shell: false,
}
}
#[must_use]
pub fn with_argv(mut self, argv: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.argv = argv.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
self.cwd = Some(cwd.into());
self
}
#[must_use]
pub fn with_shell(mut self, shell: bool) -> Self {
self.shell = shell;
self
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn name(&self) -> &str {
&self.name
}
pub fn argv(&self) -> &[String] {
&self.argv
}
pub fn cwd(&self) -> Option<&std::path::Path> {
self.cwd.as_deref()
}
pub fn is_shell(&self) -> bool {
self.shell
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum TerminalStatus {
#[default]
Starting,
Live,
Exited(ExitStatus),
Failed(String),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct TerminalSnapshot {
pub frame: Arc<TerminalFrame>,
pub status: TerminalStatus,
pub title: String,
pub cwd: Option<PathBuf>,
pub bell_count: u64,
pub clipboard_sequence: u64,
pub clipboard: Option<Arc<str>>,
pub notification_sequence: u64,
pub notification: Option<(Arc<str>, Arc<str>)>,
pub at_prompt: bool,
pub foreground: Option<ForegroundProcess>,
}
impl TerminalSnapshot {
#[must_use]
pub fn for_frame(frame: Arc<TerminalFrame>) -> Self {
Self {
frame,
status: TerminalStatus::Live,
..Self::default()
}
}
#[must_use]
pub fn with_status(mut self, status: TerminalStatus) -> Self {
self.status = status;
self
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
#[must_use]
pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
self.cwd = Some(cwd.into());
self
}
#[must_use]
pub fn with_bell_count(mut self, count: u64) -> Self {
self.bell_count = count;
self
}
#[must_use]
pub fn with_clipboard(mut self, sequence: u64, text: impl Into<Arc<str>>) -> Self {
self.clipboard_sequence = sequence;
self.clipboard = Some(text.into());
self
}
#[must_use]
pub fn with_notification(
mut self,
sequence: u64,
title: impl Into<Arc<str>>,
body: impl Into<Arc<str>>,
) -> Self {
self.notification_sequence = sequence;
self.notification = Some((title.into(), body.into()));
self
}
#[must_use]
pub fn with_at_prompt(mut self, at_prompt: bool) -> Self {
self.at_prompt = at_prompt;
self
}
#[must_use]
pub fn with_foreground(mut self, foreground: ForegroundProcess) -> Self {
self.foreground = Some(foreground);
self
}
}
pub trait ByteSource: Send + 'static {
fn start(
self: Box<Self>,
sink: ByteSink,
viewport: Viewport,
) -> io::Result<Box<dyn ByteHandle>>;
}
pub trait ByteHandle: Send {
fn write(&self, bytes: &[u8]) -> io::Result<usize>;
fn resize(&self, viewport: Viewport) -> io::Result<()>;
fn try_wait(&self) -> io::Result<Option<ExitStatus>>;
fn close(&self);
fn foreground(&self) -> Option<ForegroundProcess> {
None
}
}
#[derive(Clone, Debug)]
pub struct ByteSink {
shared: Arc<Shared>,
}
impl ByteSink {
pub fn write(&self, bytes: &[u8]) -> bool {
let mut mail = self.shared.lock();
if self.shared.closing.load(Ordering::Acquire) {
return false;
}
mail.reads.push_back(bytes.to_vec());
mail.read_bytes += bytes.len();
mail.generation = mail.generation.wrapping_add(1);
self.shared.wake.notify_one();
true
}
pub fn wait_for_room(&self) -> bool {
let mut mail = self.shared.lock();
while mail.read_bytes >= READ_QUEUE_BYTES && !self.shared.closing.load(Ordering::Acquire) {
mail = self
.shared
.room
.wait(mail)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
!self.shared.closing.load(Ordering::Acquire)
}
pub fn writable(&self) {
let mut mail = self.shared.lock();
mail.writable = true;
self.shared.bump(&mut mail);
}
pub fn finished(&self) {
let mut mail = self.shared.lock();
mail.reader_exited = true;
mail.generation = mail.generation.wrapping_add(1);
self.shared.wake.notify_one();
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputRejected {
QueueFull,
Closed,
}
impl std::fmt::Display for InputRejected {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::QueueFull => f.write_str("terminal input queue is full"),
Self::Closed => f.write_str("terminal is closed"),
}
}
}
impl std::error::Error for InputRejected {}
#[derive(Default)]
struct Mailbox {
commands: VecDeque<TerminalInput>,
bytes: usize,
reads: VecDeque<Vec<u8>>,
read_bytes: usize,
writable: bool,
viewport: Option<Viewport>,
visible: bool,
credit: bool,
painted: bool,
reader_exited: bool,
reveal: bool,
interrupt: Option<TerminalInput>,
copy: Option<(CellSelection, async_util::Sender<io::Result<String>>)>,
cursor: Option<(CursorShape, bool)>,
colors: Option<TerminalColors>,
generation: u64,
}
#[derive(Debug)]
struct Shared {
mailbox: Mutex<Mailbox>,
wake: Condvar,
room: Condvar,
closing: AtomicBool,
}
impl std::fmt::Debug for Mailbox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Mailbox")
.field("commands", &self.commands.len())
.field("reads", &self.reads.len())
.finish_non_exhaustive()
}
}
impl Shared {
fn lock(&self) -> std::sync::MutexGuard<'_, Mailbox> {
self.mailbox
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn bump(&self, mail: &mut Mailbox) {
mail.generation = mail.generation.wrapping_add(1);
self.wake.notify_one();
}
fn set_closing(&self) {
self.closing.store(true, Ordering::Release);
self.room.notify_all();
}
}
#[derive(Clone, Debug)]
pub(crate) struct EngineHandle {
shared: Arc<Shared>,
}
impl EngineHandle {
pub fn input(&self, input: TerminalInput) -> Result<(), InputRejected> {
let mut mail = self.shared.lock();
if self.shared.closing.load(Ordering::Acquire) {
return Err(InputRejected::Closed);
}
if input.is_interrupt() {
let discarded: usize = mail.commands.iter().map(TerminalInput::budget).sum();
mail.commands.clear();
mail.bytes = mail.bytes.saturating_sub(discarded);
mail.interrupt = Some(input);
self.shared.bump(&mut mail);
return Ok(());
}
let budget = input.budget();
if mail.commands.len() >= INPUT_COMMANDS || budget > INPUT_BYTES.saturating_sub(mail.bytes)
{
return Err(InputRejected::QueueFull);
}
mail.bytes += budget;
mail.commands.push_back(input);
self.shared.bump(&mut mail);
Ok(())
}
#[must_use]
pub fn input_backlog(&self) -> usize {
self.shared.lock().bytes
}
pub fn resize(&self, viewport: Viewport) {
let mut mail = self.shared.lock();
mail.viewport = Some(viewport);
self.shared.bump(&mut mail);
}
pub fn set_visible(&self, visible: bool) {
let mut mail = self.shared.lock();
if mail.visible != visible {
mail.visible = visible;
mail.credit = visible;
mail.reveal = visible;
self.shared.bump(&mut mail);
}
}
pub fn request_frame(&self) {
let mut mail = self.shared.lock();
if !mail.credit || !mail.painted {
mail.credit = true;
mail.painted = true;
self.shared.bump(&mut mail);
}
}
pub fn set_cursor(&self, shape: CursorShape, blinking: bool) {
let mut mail = self.shared.lock();
mail.cursor = Some((shape, blinking));
self.shared.bump(&mut mail);
}
pub fn set_colors(&self, colors: TerminalColors) {
let mut mail = self.shared.lock();
mail.colors = Some(colors);
self.shared.bump(&mut mail);
}
pub fn copy(&self, selection: CellSelection) -> async_util::Receiver<io::Result<String>> {
let (tx, rx) = async_util::unbounded();
let mut mail = self.shared.lock();
mail.copy = Some((selection, tx));
self.shared.bump(&mut mail);
rx
}
pub fn close(&self) {
let mut mail = self.shared.lock();
self.shared.set_closing();
self.shared.bump(&mut mail);
}
}
#[derive(Debug)]
pub(crate) struct EngineConfig {
pub options: EngineOptions,
pub viewport: Viewport,
pub colors: TerminalColors,
pub visible: bool,
pub cursor: Option<(CursorShape, bool)>,
pub input: Vec<u8>,
pub abnormal_exit_runtime: Duration,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
options: EngineOptions::default(),
viewport: Viewport::default(),
colors: TerminalColors::default(),
visible: true,
cursor: None,
input: Vec::new(),
abnormal_exit_runtime: Duration::from_millis(250),
}
}
}
pub(crate) fn spawn(
source: Box<dyn ByteSource>,
config: EngineConfig,
sink: FrameSink,
) -> io::Result<EngineHandle> {
let shared = Arc::new(Shared {
mailbox: Mutex::new(Mailbox {
visible: config.visible,
credit: false,
..Mailbox::default()
}),
wake: Condvar::new(),
room: Condvar::new(),
closing: AtomicBool::new(false),
});
let handle = EngineHandle {
shared: shared.clone(),
};
let byte_sink = ByteSink {
shared: shared.clone(),
};
std::thread::Builder::new()
.name("gpui-cn-terminal".to_owned())
.spawn(move || {
let mut owner = match Owner::start(source, byte_sink, config, shared.clone(), &sink) {
Ok(owner) => owner,
Err(error) => {
shared.set_closing();
sink.publish(Arc::new(TerminalSnapshot {
status: TerminalStatus::Failed(error.to_string()),
..TerminalSnapshot::default()
}));
return;
}
};
owner.run();
})?;
Ok(handle)
}
#[derive(Debug)]
pub struct Engine<S> {
source: S,
options: EngineOptions,
abnormal_exit_runtime: Duration,
input: Vec<u8>,
}
impl<S: ByteSource> Engine<S> {
#[must_use]
pub fn new(source: S, options: EngineOptions) -> Self {
Self {
source,
options,
abnormal_exit_runtime: Duration::from_millis(250),
input: Vec::new(),
}
}
#[must_use]
pub fn with_abnormal_exit_runtime(mut self, runtime: Duration) -> Self {
self.abnormal_exit_runtime = runtime;
self
}
#[must_use]
pub fn with_input(mut self, input: impl Into<Vec<u8>>) -> Self {
self.input = input.into();
self
}
}
impl<S: ByteSource> FrameSource for Engine<S> {
fn start(
self: Box<Self>,
sink: FrameSink,
options: StartOptions,
) -> io::Result<Box<dyn FrameHandle>> {
let handle = spawn(
Box::new(self.source),
EngineConfig {
options: self.options,
viewport: options.viewport,
colors: options.colors,
visible: options.visible,
cursor: options.cursor,
input: self.input,
abnormal_exit_runtime: self.abnormal_exit_runtime,
},
sink,
)?;
Ok(Box::new(handle))
}
}
impl FrameHandle for EngineHandle {
fn input(&self, input: TerminalInput) -> Result<(), InputRejected> {
Self::input(self, input)
}
fn input_backlog(&self) -> usize {
Self::input_backlog(self)
}
fn resize(&self, viewport: Viewport) {
Self::resize(self, viewport);
}
fn set_visible(&self, visible: bool) {
Self::set_visible(self, visible);
}
fn request_frame(&self) {
Self::request_frame(self);
}
fn set_colors(&self, colors: TerminalColors) {
Self::set_colors(self, colors);
}
fn set_cursor(&self, shape: CursorShape, blinking: bool) {
Self::set_cursor(self, shape, blinking);
}
fn copy(&self, selection: CellSelection) -> Option<async_util::Receiver<io::Result<String>>> {
Some(Self::copy(self, selection))
}
fn close(&self) {
Self::close(self);
}
}
struct PendingWrite {
bytes: Vec<u8>,
offset: usize,
budget: usize,
paste: bool,
}
struct CoreSlot {
live: Option<Core>,
parked: Option<crate::terminal::core::Parked>,
}
impl std::ops::Deref for CoreSlot {
type Target = Core;
fn deref(&self) -> &Core {
self.live
.as_ref()
.expect("the core is restored before it is used")
}
}
impl std::ops::DerefMut for CoreSlot {
fn deref_mut(&mut self) -> &mut Core {
self.live
.as_mut()
.expect("the core is restored before it is used")
}
}
struct Parking {
options: EngineOptions,
key: u64,
last_use: Instant,
}
struct Owner {
shared: Arc<Shared>,
core: CoreSlot,
parking: Parking,
handle: Box<dyn ByteHandle>,
sink: FrameSink,
snapshot: TerminalSnapshot,
urgent: Vec<u8>,
pending: Option<PendingWrite>,
write_blocked: bool,
exit: Option<ExitStatus>,
started: Instant,
abnormal_exit_runtime: Duration,
compression_deadline: Option<Instant>,
foreground_due: Option<Instant>,
dirty: bool,
force: bool,
effects: Effects,
}
impl Owner {
fn start(
source: Box<dyn ByteSource>,
byte_sink: ByteSink,
config: EngineConfig,
shared: Arc<Shared>,
sink: &FrameSink,
) -> io::Result<Self> {
let mut core = Core::new(config.viewport, &config.options, config.colors)?;
if let Some((shape, blinking)) = config.cursor {
core.set_cursor(shape, blinking)?;
}
let frame = core
.frame(true)?
.ok_or_else(|| io::Error::other("initial terminal frame missing"))?;
let handle = source.start(byte_sink, config.viewport)?;
let snapshot = TerminalSnapshot {
frame,
status: TerminalStatus::Live,
..TerminalSnapshot::default()
};
sink.publish(Arc::new(snapshot.clone()));
Ok(Self {
shared,
core: CoreSlot {
live: Some(core),
parked: None,
},
parking: Parking {
options: config.options.clone(),
key: crate::terminal::park::next_key(),
last_use: Instant::now(),
},
handle,
sink: sink.clone(),
snapshot,
urgent: config.input.clone(),
pending: None,
write_blocked: false,
exit: None,
started: Instant::now(),
abnormal_exit_runtime: config.abnormal_exit_runtime,
compression_deadline: None,
foreground_due: Some(Instant::now() + FOREGROUND_CHECK),
dirty: true,
force: false,
effects: Effects::default(),
})
}
fn run(&mut self) {
while !self.shared.closing.load(Ordering::Acquire) {
if let Err(error) = self.turn() {
self.snapshot.status = TerminalStatus::Failed(error.to_string());
self.publish();
break;
}
}
self.cleanup();
}
fn turn(&mut self) -> io::Result<()> {
let (
reads,
viewport,
command,
visible,
credit,
eof,
reveal,
interrupt,
copy,
cursor,
colors,
painted,
generation,
) = {
let mut mail = self.shared.lock();
let interrupt = mail.interrupt.take();
let command = if interrupt.is_none() && self.pending.is_none() {
mail.commands.pop_front()
} else {
None
};
mail.read_bytes = 0;
self.shared.room.notify_all();
if std::mem::take(&mut mail.writable) {
self.write_blocked = false;
}
(
std::mem::take(&mut mail.reads),
mail.viewport.take(),
command,
mail.visible,
mail.credit,
mail.reader_exited,
std::mem::take(&mut mail.reveal),
interrupt,
mail.copy.take(),
mail.cursor.take(),
mail.colors.take(),
std::mem::take(&mut mail.painted),
mail.generation,
)
};
let had_work = !reads.is_empty()
|| viewport.is_some()
|| command.is_some()
|| reveal
|| interrupt.is_some()
|| copy.is_some()
|| cursor.is_some()
|| colors.is_some();
{
if had_work || painted || (eof && self.exit.is_none()) {
self.parking.last_use = Instant::now();
self.unpark()?;
} else if self.core.live.is_none() {
self.wait(generation, None);
return Ok(());
}
}
if reveal {
self.dirty = true;
self.force = true;
}
if let Some((shape, blinking)) = cursor {
self.core.set_cursor(shape, blinking)?;
self.dirty = true;
}
if let Some(colors) = colors {
self.core.set_colors(colors)?;
self.dirty = true;
self.force = true;
}
if !reads.is_empty() {
for chunk in reads {
self.core.write(&chunk, &mut self.effects);
}
self.compression_deadline = Some(Instant::now() + COMPRESSION_IDLE);
self.foreground_due
.get_or_insert_with(|| Instant::now() + FOREGROUND_CHECK);
self.dirty = true;
self.apply_effects()?;
}
if let Some(interrupt) = interrupt {
let partial_paste = self
.pending
.as_ref()
.is_some_and(|pending| pending.paste && pending.offset > 0);
if let Some(pending) = self.pending.take() {
self.release(pending.budget);
}
let mut bytes = if partial_paste {
b"\x1b[201~".to_vec()
} else {
Vec::new()
};
bytes.extend(self.core.input(interrupt)?);
self.urgent.extend(bytes);
self.dirty = true;
}
if let Some(viewport) = viewport {
self.core.resize(viewport, &mut self.effects)?;
self.handle.resize(viewport)?;
self.apply_effects()?;
self.dirty = true;
self.force = true;
}
if let Some(input) = command {
let budget = input.budget();
if self.exit.is_some() && !matches!(input, TerminalInput::Scroll(_)) {
self.release(budget);
} else {
let paste = matches!(input, TerminalInput::Paste(_));
match self.core.input(input) {
Ok(bytes) if !bytes.is_empty() => {
self.pending = Some(PendingWrite {
bytes,
offset: 0,
budget,
paste,
});
}
Ok(_) => self.release(budget),
Err(error) => {
self.release(budget);
return Err(error);
}
}
self.dirty = true;
}
}
if let Some((selection, reply)) = copy {
let _ = reply.try_send(self.core.copy(selection));
}
self.flush()?;
if eof
&& self.exit.is_none()
&& let Some(mut status) = self.handle.try_wait()?
{
status.runtime = self.started.elapsed();
status.abnormal = status.runtime <= self.abnormal_exit_runtime;
self.exit = Some(status.clone());
self.snapshot.status = TerminalStatus::Exited(status);
self.dirty = true;
self.force = true;
}
if self.foreground_due.is_some_and(|due| Instant::now() >= due) {
self.foreground_due = None;
let foreground = if self.exit.is_some() {
None
} else {
self.handle.foreground()
};
if foreground != self.snapshot.foreground {
self.snapshot.foreground = foreground;
self.publish();
}
}
let exited = self.exit.is_some();
let held = self.core.held();
if self.dirty && ((visible && credit && !held) || (exited && self.force)) {
if let Some(frame) = self.core.frame(self.force)? {
self.snapshot.frame = frame;
self.snapshot.at_prompt = self.core.at_prompt();
self.shared.lock().credit = false;
self.publish();
}
self.dirty = false;
self.force = false;
}
if self
.compression_deadline
.is_some_and(|deadline| Instant::now() >= deadline)
{
let complete = self.core.compress();
self.compression_deadline = (!complete).then(|| Instant::now() + COMPRESSION_STEP);
}
let unsent = self.pending.is_some() || !self.urgent.is_empty();
if !had_work && !(unsent && !self.write_blocked) {
let delay = if unsent {
Some(WRITE_RETRY)
} else if self.dirty && visible && credit {
self.core
.hold_remaining()
.or(Some(Duration::from_millis(1)))
} else if let Some(deadline) = self.compression_deadline {
Some(deadline.saturating_duration_since(Instant::now()))
} else if eof && self.exit.is_none() {
Some(Duration::from_millis(20))
} else {
None
};
let delay = match self.foreground_due {
Some(due) => {
let until = due.saturating_duration_since(Instant::now());
Some(delay.map_or(until, |delay| delay.min(until)))
}
None => delay,
};
let delay = match self.park_after() {
Some(_) if self.park_if_idle() => None,
Some(after) => Some(delay.map_or(after, |delay| delay.min(after))),
None => delay,
};
self.wait(generation, delay);
}
Ok(())
}
fn wait(&self, generation: u64, delay: Option<Duration>) {
let mail = self.shared.lock();
if mail.generation == generation && !self.shared.closing.load(Ordering::Acquire) {
match delay {
None => drop(
self.shared
.wake
.wait(mail)
.unwrap_or_else(std::sync::PoisonError::into_inner),
),
Some(delay) => drop(
self.shared
.wake
.wait_timeout(mail, delay.max(Duration::from_millis(1)))
.unwrap_or_else(std::sync::PoisonError::into_inner),
),
}
}
}
fn park_after(&mut self) -> Option<Duration> {
let park = &self.parking.options.park;
if !park.enabled
|| self.core.live.is_none()
|| self.pending.is_some()
|| self.exit.is_some()
|| self.core.held()
{
return None;
}
Some(park.idle.saturating_sub(self.parking.last_use.elapsed()))
}
fn park_if_idle(&mut self) -> bool {
if self.park_after() != Some(Duration::ZERO) {
return false;
}
let store = self.parking.options.park.store.clone();
let parked = self
.core
.park()
.and_then(|(bytes, parked)| store.save(self.parking.key, &bytes).map(|()| parked));
match parked {
Ok(parked) => {
self.core.live = None;
self.core.parked = Some(parked);
self.compression_deadline = None;
true
}
Err(_) => {
self.parking.last_use = Instant::now();
false
}
}
}
fn unpark(&mut self) -> io::Result<()> {
let Some(parked) = self.core.parked.take() else {
return Ok(());
};
let bytes = self.parking.options.park.store.take(self.parking.key)?;
self.core.live = Some(Core::restore(&bytes, parked, &self.parking.options)?);
Ok(())
}
fn release(&self, budget: usize) {
let mut mail = self.shared.lock();
mail.bytes = mail.bytes.saturating_sub(budget);
}
fn flush(&mut self) -> io::Result<()> {
if self.write_blocked {
return Ok(());
}
if !self.urgent.is_empty() {
let written = self.handle.write(&self.urgent)?;
self.urgent.drain(..written.min(self.urgent.len()));
if !self.urgent.is_empty() {
self.write_blocked = true;
return Ok(());
}
}
let mut burst = WRITE_BURST;
while burst > 0 {
let Some(pending) = &mut self.pending else {
break;
};
let end = (pending.offset + WRITE_CHUNK.min(burst)).min(pending.bytes.len());
let offered = end - pending.offset;
let written = self
.handle
.write(&pending.bytes[pending.offset..end])?
.min(offered);
pending.offset += written;
burst = burst.saturating_sub(written);
if pending.offset >= pending.bytes.len() {
let budget = pending.budget;
self.pending = None;
self.release(budget);
break;
}
if written < offered {
self.write_blocked = true;
break;
}
}
Ok(())
}
fn apply_effects(&mut self) -> io::Result<()> {
let effects = std::mem::take(&mut self.effects);
self.urgent.extend_from_slice(&effects.replies);
let mut changed = false;
if effects.title_changed {
self.snapshot.title = self.core.title().to_owned();
changed = true;
}
if effects.cwd_changed {
self.snapshot.cwd = self.core.cwd().cloned();
changed = true;
}
if effects.bells > 0 {
self.snapshot.bell_count += effects.bells;
changed = true;
}
if let Some(text) = effects.clipboard.last() {
self.snapshot.clipboard_sequence += effects.clipboard.len() as u64;
self.snapshot.clipboard = Some(Arc::from(text.as_str()));
changed = true;
}
if let Some((title, body)) = effects.notifications.last() {
self.snapshot.notification_sequence += effects.notifications.len() as u64;
self.snapshot.notification =
Some((Arc::from(title.as_str()), Arc::from(body.as_str())));
changed = true;
}
if changed {
self.dirty = true;
self.force = true;
}
Ok(())
}
fn publish(&self) {
self.sink.publish(Arc::new(self.snapshot.clone()));
}
fn cleanup(&mut self) {
self.shared.set_closing();
{
let mut mail = self.shared.lock();
mail.commands.clear();
mail.bytes = 0;
mail.reads.clear();
mail.read_bytes = 0;
}
self.pending = None;
if self.core.parked.take().is_some() {
self.parking.options.park.store.remove(self.parking.key);
}
self.handle.close();
if self.exit.is_none() {
let deadline = Instant::now() + Duration::from_secs(2);
while Instant::now() < deadline {
if let Ok(Some(mut status)) = self.handle.try_wait() {
status.runtime = self.started.elapsed();
status.abnormal = status.runtime <= self.abnormal_exit_runtime;
self.snapshot.status = TerminalStatus::Exited(status.clone());
self.exit = Some(status);
break;
}
std::thread::sleep(Duration::from_millis(20));
}
}
self.publish();
}
}