use std::io;
use std::sync::{Arc, Mutex};
use gpui_kit::base::async_util;
use crate::terminal::core::{Core, Effects};
use crate::terminal::engine::{InputRejected, TerminalSnapshot};
use crate::terminal::frame::{CellSelection, CursorShape, TerminalColors, Viewport};
use crate::terminal::input::TerminalInput;
use crate::terminal::options::EngineOptions;
#[derive(Clone)]
pub struct FrameSink {
latest: Arc<Mutex<Option<Arc<TerminalSnapshot>>>>,
wake: async_util::Sender<()>,
}
impl std::fmt::Debug for FrameSink {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FrameSink").finish_non_exhaustive()
}
}
impl FrameSink {
#[must_use]
pub fn new() -> (Self, async_util::Receiver<()>) {
let (wake, rx) = async_util::unbounded();
(
Self {
latest: Arc::new(Mutex::new(None)),
wake,
},
rx,
)
}
pub fn publish(&self, snapshot: Arc<TerminalSnapshot>) {
*self
.latest
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(snapshot);
let _ = self.wake.try_send(());
}
pub fn take(&self) -> Option<Arc<TerminalSnapshot>> {
self.latest
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct StartOptions {
pub(crate) viewport: Viewport,
pub(crate) colors: TerminalColors,
pub(crate) visible: bool,
pub(crate) cursor: Option<(CursorShape, bool)>,
}
impl Default for StartOptions {
fn default() -> Self {
Self {
viewport: Viewport::default(),
colors: TerminalColors::default(),
visible: true,
cursor: None,
}
}
}
impl StartOptions {
#[must_use]
pub fn with_viewport(mut self, viewport: Viewport) -> Self {
self.viewport = viewport;
self
}
#[must_use]
pub fn with_colors(mut self, colors: TerminalColors) -> Self {
self.colors = colors;
self
}
#[must_use]
pub fn with_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
#[must_use]
pub fn with_cursor(mut self, shape: CursorShape, blinking: bool) -> Self {
self.cursor = Some((shape, blinking));
self
}
pub fn viewport(&self) -> Viewport {
self.viewport
}
pub fn colors(&self) -> &TerminalColors {
&self.colors
}
pub fn visible(&self) -> bool {
self.visible
}
pub fn cursor(&self) -> Option<(CursorShape, bool)> {
self.cursor
}
}
pub trait FrameSource: 'static {
fn start(
self: Box<Self>,
sink: FrameSink,
options: StartOptions,
) -> io::Result<Box<dyn FrameHandle>>;
}
pub trait FrameHandle: 'static {
fn input(&self, input: TerminalInput) -> Result<(), InputRejected>;
fn input_backlog(&self) -> usize {
0
}
fn resize(&self, viewport: Viewport);
fn set_visible(&self, visible: bool);
fn request_frame(&self);
fn set_colors(&self, colors: TerminalColors) {
let _ = colors;
}
fn set_cursor(&self, shape: CursorShape, blinking: bool) {
let _ = (shape, blinking);
}
fn copy(&self, selection: CellSelection) -> Option<async_util::Receiver<io::Result<String>>> {
let _ = selection;
None
}
fn close(&self);
}
#[derive(Debug)]
pub struct FixtureSource {
bytes: Vec<u8>,
options: EngineOptions,
inputs: Arc<Mutex<Vec<TerminalInput>>>,
}
impl FixtureSource {
#[must_use]
pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
Self {
bytes: bytes.into(),
options: EngineOptions::default(),
inputs: Arc::new(Mutex::new(Vec::new())),
}
}
#[must_use]
pub fn with_options(mut self, options: EngineOptions) -> Self {
self.options = options;
self
}
#[must_use]
pub fn inputs(&self) -> Arc<Mutex<Vec<TerminalInput>>> {
self.inputs.clone()
}
}
struct FixtureHandle {
core: Mutex<Core>,
bells: u64,
inputs: Arc<Mutex<Vec<TerminalInput>>>,
sink: FrameSink,
}
impl FixtureHandle {
fn core(&self) -> std::sync::MutexGuard<'_, Core> {
self.core
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn publish(&self, core: &mut Core) -> io::Result<()> {
let frame = core
.frame(true)?
.ok_or_else(|| io::Error::other("fixture frame missing"))?;
let mut snapshot = TerminalSnapshot::for_frame(frame);
core.title().clone_into(&mut snapshot.title);
snapshot.cwd = core.cwd().cloned();
snapshot.bell_count = self.bells;
snapshot.at_prompt = core.at_prompt();
self.sink.publish(Arc::new(snapshot));
Ok(())
}
}
impl FrameSource for FixtureSource {
fn start(
self: Box<Self>,
sink: FrameSink,
options: StartOptions,
) -> io::Result<Box<dyn FrameHandle>> {
let mut core = Core::new(options.viewport, &self.options, options.colors)?;
if let Some((shape, blinking)) = options.cursor {
core.set_cursor(shape, blinking)?;
}
let mut effects = Effects::default();
core.write(&self.bytes, &mut effects);
let handle = FixtureHandle {
core: Mutex::new(core),
bells: effects.bells,
inputs: self.inputs,
sink,
};
handle.publish(&mut handle.core())?;
Ok(Box::new(handle))
}
}
impl FrameHandle for FixtureHandle {
fn input(&self, input: TerminalInput) -> Result<(), InputRejected> {
self.inputs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(input);
Ok(())
}
fn resize(&self, viewport: Viewport) {
let mut core = self.core();
let mut effects = Effects::default();
if core.resize(viewport, &mut effects).is_ok() {
let _ = self.publish(&mut core);
}
}
fn set_visible(&self, _visible: bool) {}
fn request_frame(&self) {}
fn set_colors(&self, colors: TerminalColors) {
let _ = self.core().set_colors(colors);
}
fn set_cursor(&self, shape: CursorShape, blinking: bool) {
let _ = self.core().set_cursor(shape, blinking);
}
fn close(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
fn rows(sink: &FrameSink) -> Vec<(String, u64)> {
let snapshot = sink.take().expect("a published frame");
snapshot
.frame
.rows
.iter()
.take(4)
.map(|row| (row.text.trim_end().to_owned(), row.revision))
.collect()
}
#[test]
fn a_fixture_resize_reflows_and_gives_changed_rows_new_revisions() {
let (sink, _wake) = FrameSink::new();
let handle = Box::new(FixtureSource::new("$ ls\r\naaaa bbbb\r\n$ "))
.start(
sink.clone(),
StartOptions {
viewport: Viewport::new(20, 4, 8, 16).unwrap(),
..StartOptions::default()
},
)
.unwrap();
let before = rows(&sink);
handle.resize(Viewport::new(5, 4, 8, 16).unwrap());
let after = rows(&sink);
let text = |rows: &[(String, u64)]| rows.iter().map(|r| r.0.clone()).collect::<Vec<_>>();
assert_eq!(text(&before), vec!["$ ls", "aaaa bbbb", "$", ""]);
assert_eq!(text(&after), vec!["$ ls", "aaaa", "bbbb", "$"]);
assert!(
after[2].1 > before[2].1,
"a changed row keeps its cache key: {before:?} -> {after:?}"
);
}
}