use std::io;
use std::time::Duration;
use katui_present::session;
use katui_proto::kitty::{self, IdSpace};
use katui_proto::probe::{Capabilities, ProbeSource};
use katui_scene::CellSize;
use crate::events::EventPump;
const MULTIPLEXER_BATCH: usize = 16 * 1024;
const BATCH_BYTES_PER_CELL: usize = 4;
const QUERY_IDS_FROM: u32 = 1_000_000;
const QUERY_IDS_UNTIL: u32 = 0x00f0_0000;
const ANSWER_WAIT: Duration = Duration::from_millis(500);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Pacing {
#[default]
Timer,
EachFrame,
Batches,
DeviceAttributes,
}
impl Pacing {
#[must_use]
pub fn for_terminal(caps: &Capabilities) -> Self {
let answers = caps.trust == ProbeSource::Probed;
match (caps.kitty, caps.multiplexed()) {
(false, false) if answers => Self::DeviceAttributes,
(false, _) => Self::Timer,
(true, false) => Self::EachFrame,
(true, true) => Self::Batches,
}
}
}
#[derive(Debug)]
pub(crate) struct Pacer {
configured: Pacing,
multiplexed: bool,
pacing: Pacing,
next_query: u32,
space: IdSpace,
}
impl Pacer {
pub(crate) fn new(pacing: Pacing, multiplexed: bool, space: IdSpace) -> Self {
Self {
configured: pacing,
multiplexed,
pacing,
next_query: QUERY_IDS_FROM,
space,
}
}
pub(crate) fn restore(&mut self) {
self.pacing = self.configured;
}
pub(crate) fn asks(&self) -> bool {
self.pacing != Pacing::Timer
}
fn take_query_id(&mut self) -> u32 {
let local = self.next_query;
self.next_query = if local + 1 < QUERY_IDS_UNTIL {
local + 1
} else {
QUERY_IDS_FROM
};
self.space.image(local).0
}
pub(crate) fn deliver<W: io::Write, M: Send + 'static>(
&mut self,
out: &mut W,
frame: &[u8],
size: CellSize,
pump: &mut EventPump<M>,
) -> io::Result<bool> {
if frame.is_empty() {
return Ok(true);
}
if self.pacing == Pacing::DeviceAttributes {
return self.deliver_with_device_attributes(out, frame, pump);
}
let parts = if self.pacing == Pacing::Batches {
kitty::split_at_commands(frame, batch_limit(size))
} else {
vec![frame]
};
let mut answering = true;
for part in parts {
let mut batch = part.to_vec();
let query = (self.pacing != Pacing::Timer && answering).then(|| self.take_query_id());
if let Some(query) = query {
session::terminal_query(&mut batch, query)?;
}
if self.multiplexed {
batch = kitty::wrap_for_passthrough(&batch);
}
out.write_all(&batch)?;
out.flush()?;
if let Some(query) = query {
answering = pump.await_answer(query, ANSWER_WAIT);
}
}
if !answering {
self.pacing = Pacing::Timer;
}
Ok(answering)
}
fn deliver_with_device_attributes<W: io::Write, M: Send + 'static>(
&mut self,
out: &mut W,
frame: &[u8],
pump: &mut EventPump<M>,
) -> io::Result<bool> {
out.write_all(frame)?;
out.write_all(DEVICE_ATTRIBUTES_QUERY)?;
out.flush()?;
let answering = pump.await_device_attributes(ANSWER_WAIT);
if !answering {
self.pacing = Pacing::Timer;
}
Ok(answering)
}
}
const DEVICE_ATTRIBUTES_QUERY: &[u8] = b"\x1b[c";
fn batch_limit(size: CellSize) -> usize {
let cells = usize::from(size.columns) * usize::from(size.rows);
MULTIPLEXER_BATCH.min(cells * BATCH_BYTES_PER_CELL)
}
#[cfg(test)]
mod tests {
use katui_proto::probe::{Capabilities, Identity};
use katui_scene::CellSize;
use super::Pacing;
#[test]
fn multiplexers_get_batches_and_plain_terminals_no_queries() {
let plain = Capabilities::default();
assert_eq!(Pacing::for_terminal(&plain), Pacing::Timer);
let kitty = Capabilities {
kitty: true,
..Capabilities::default()
};
assert_eq!(Pacing::for_terminal(&kitty), Pacing::EachFrame);
let muxed = Capabilities {
identity: Some(Identity {
name: "rmux".to_owned(),
version: None,
}),
..kitty
};
assert_eq!(Pacing::for_terminal(&muxed), Pacing::Batches);
let answering_plain = Capabilities {
trust: katui_proto::probe::ProbeSource::Probed,
..Capabilities::default()
};
assert_eq!(
Pacing::for_terminal(&answering_plain),
Pacing::DeviceAttributes,
"a terminal that answered the probe paces on device attributes"
);
}
#[test]
fn a_multiplexed_pacer_asks_with_ids_of_its_own_space() {
let space = katui_proto::kitty::IdSpace::of_process(4242);
let mut pacer = super::Pacer::new(Pacing::Batches, true, space);
let first = pacer.take_query_id();
assert_eq!(first >> 24, space.image(0).0 >> 24);
assert_ne!(pacer.take_query_id(), first);
}
#[test]
fn a_small_pane_gets_batches_under_the_multiplexer_discard_limit() {
let small = CellSize {
columns: 80,
rows: 24,
};
assert!(super::batch_limit(small) * 2 <= 80 * 24 * 8);
let large = CellSize {
columns: 300,
rows: 90,
};
assert_eq!(super::batch_limit(large), super::MULTIPLEXER_BATCH);
}
}