blob: d54853f41006b922b60a35172599a9be99ea86ab [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Original bounded queue and canonical screen, independent of App and transport.
use super::*;
use crate::budget::{Kind, NativeBudget, Permit};
use maka_agent_facts::terminal::TerminalCursor;
/// Scheduling target for recoverable native batches, not an action admission limit.
const MAX_INPUT_ACTIONS: usize = 64;
use maka_agent_facts::terminal::input::encoded_actions_byte_len;
use std::collections::VecDeque;
const MAX_BATCH_BYTES: usize = 32 * 1024;
pub struct Display {
pub screen: String,
pub scrollback: String,
pub size: TerminalSize,
pub cursor: TerminalCursor,
}
pub struct State {
observation: TerminalViewObservation,
pub display: Option<Display>,
pub scroll: usize,
size: Option<TerminalSize>,
desired_size: Option<TerminalSize>,
next_sequence: u64,
queue: VecDeque<(InputAction, usize)>,
queued_bytes: usize,
pending_bytes: usize,
dirty: bool,
physical: bool,
permit: Option<Permit>,
}
impl State {
pub fn new(budget: &NativeBudget) -> Self {
// Retain native presentation custody. The input window separately packs
// complete actions and retains their actual bytes until the exact ACK.
// No worker or process lives in this owner.
let permit = budget.reserve(Kind::Encoded, 384 * 1024).ok();
Self {
observation: TerminalViewObservation {
owner: Uuid::new_v4(),
revision: 0,
source: String::new(),
controller: None,
captured: false,
batch: None,
error: permit
.is_none()
.then_some(TerminalViewError::BudgetExhausted),
},
display: None,
scroll: 0,
size: None,
desired_size: None,
next_sequence: 1,
queue: VecDeque::new(),
queued_bytes: 0,
pending_bytes: 0,
dirty: true,
physical: false,
permit,
}
}
fn changed(&mut self) {
self.observation.revision = self
.observation
.revision
.checked_add(1)
.expect("terminal revision");
self.dirty = true;
}
pub fn observation(&self) -> &TerminalViewObservation {
&self.observation
}
pub fn pending_observation(&self) -> Option<TerminalViewObservation> {
self.dirty.then(|| self.observation.clone())
}
pub fn published(&mut self, revision: u64) {
if self.observation.revision == revision {
self.dirty = false;
}
}
pub fn apply(&mut self, props: &TerminalViewProps) {
if self.observation.source != props.source
|| self.observation.controller != props.controller
{
self.observation.source.clone_from(&props.source);
self.observation.controller = props.controller;
self.observation.captured = false;
self.physical = false;
self.observation.batch = None;
self.observation.error = self
.permit
.is_none()
.then_some(TerminalViewError::BudgetExhausted);
self.queue.clear();
self.queued_bytes = 0;
self.pending_bytes = 0;
self.next_sequence = 1;
self.size = None;
self.desired_size = None;
self.scroll = 0;
self.changed();
}
self.display = if self.permit.is_some() {
match &props.output {
Some(ShellOutput::Pty {
screen,
scrollback,
cols,
rows,
cursor,
redacted: false,
..
}) => TerminalSize::new(*cols, *rows).ok().map(|size| Display {
screen: screen.clone(),
scrollback: scrollback.clone(),
size,
cursor: cursor.clone(),
}),
_ => None,
}
} else {
None
};
self.scroll = self.scroll.min(
self.display
.as_ref()
.map_or(0, |d| d.scrollback.lines().count()),
);
if self.size.is_none() {
self.size = self.display.as_ref().map(|d| d.size);
}
if !self.ready() {
self.capture(false);
}
if let Some(ack) = &props.acknowledgement
&& ack.owner == self.observation.owner
&& let Some(batch) = &self.observation.batch
&& ack.controller == batch.controller
&& ack.sequence == batch.sequence
{
if ack.accepted {
if let Some(size) = batch.size {
self.size = Some(size);
}
self.queued_bytes -= self.pending_bytes;
self.pending_bytes = 0;
self.observation.batch = None;
if self.observation.error == Some(TerminalViewError::InputFull) {
self.observation.error = None;
}
self.changed();
} else {
self.fail();
}
}
self.pump();
}
pub fn ready(&self) -> bool {
self.observation.controller.is_some()
&& self.display.is_some()
&& matches!(
self.observation.error,
None | Some(TerminalViewError::InputFull)
)
}
pub fn captured(&self) -> bool {
self.observation.captured
}
pub fn capture(&mut self, capture: bool) {
let capture = capture && self.physical && self.ready();
if self.observation.captured != capture {
self.observation.captured = capture;
self.changed();
}
}
pub fn presentation(&mut self, physical: bool) {
self.physical = physical;
if !physical {
self.capture(false);
}
}
pub fn resize(&mut self, cols: u16, rows: u16) {
self.desired_size = Some(TerminalSize::fit(cols, rows));
self.pump();
}
pub fn write(&mut self, action: InputAction) {
if !self.physical || !self.ready() || !self.captured() {
return;
}
let bytes = serde_json::to_vec(&action)
.expect("typed terminal input")
.len();
if encoded_actions_byte_len(std::slice::from_ref(&action)).is_err()
|| self.queued_bytes.checked_add(bytes).is_none()
|| (self.queued_bytes != 0 && self.queued_bytes.saturating_add(bytes) > 64 * 1024)
{
if self.observation.error != Some(TerminalViewError::InputFull) {
self.observation.error = Some(TerminalViewError::InputFull);
self.changed();
}
return;
}
self.queued_bytes += bytes;
self.queue.push_back((action, bytes));
self.pump();
}
pub fn paste(&mut self, text: &str) {
let text: String = text
.chars()
.filter(|c| !c.is_control() || matches!(c, '\n' | '\r' | '\t'))
.collect();
let text = text.replace("\r\n", "\n").replace('\r', "\n");
if !text.is_empty() {
self.write(InputAction::Paste(text));
}
}
fn fail(&mut self) {
self.observation.error = Some(TerminalViewError::InputFailed);
self.observation.batch = None;
self.observation.captured = false;
self.queue.clear();
self.queued_bytes = 0;
self.pending_bytes = 0;
self.changed();
}
fn pump(&mut self) {
if !self.physical || !self.ready() || self.observation.batch.is_some() {
return;
}
let size = self.desired_size.filter(|size| Some(*size) != self.size);
// A resize settles before any queued key or mouse action, as in the original View.
let actions = if size.is_some() {
vec![]
} else if !self.queue.is_empty() {
// Batch queued actions to a scheduling target, admitting a complete
// oversized first action. This original batch stays immutable until
// its exact receipt; a paste is never split into new actions.
let mut actions = Vec::new();
let mut encoded_bytes = 0;
while let Some((action, _)) = self.queue.front() {
let bytes = encoded_actions_byte_len(std::slice::from_ref(action))
.expect("validated queued input");
if actions.len() == MAX_INPUT_ACTIONS
|| (!actions.is_empty()
&& bytes > MAX_BATCH_BYTES.saturating_sub(encoded_bytes))
{
break;
}
let (action, retained_bytes) = self.queue.pop_front().unwrap();
encoded_bytes += bytes;
self.pending_bytes += retained_bytes;
actions.push(action);
}
actions
} else {
return;
};
if self.next_sequence > 9_007_199_254_740_991 {
self.fail();
return;
}
self.observation.batch = Some(TerminalInputBatch {
controller: self.observation.controller.unwrap(),
sequence: self.next_sequence,
actions,
size,
});
self.next_sequence += 1;
self.changed();
}
pub fn scroll_by(&mut self, lines: i32) {
let max = self
.display
.as_ref()
.map_or(0, |d| d.scrollback.lines().count());
self.scroll = self.scroll.saturating_add_signed(lines as isize).min(max);
}
pub fn text(&self, rows: usize) -> String {
let Some(screen) = &self.display else {
return String::new();
};
if self.scroll == 0 {
return screen.screen.clone();
}
let mut lines: Vec<_> = screen
.scrollback
.lines()
.chain(screen.screen.lines())
.collect();
let blanks = usize::from(screen.size.rows()).saturating_sub(screen.screen.lines().count());
lines.extend(std::iter::repeat_n("", blanks));
let end = lines.len().saturating_sub(self.scroll);
lines[end.saturating_sub(rows)..end].join("\n")
}
}
#[cfg(test)]
mod tests;