use std::collections::VecDeque;
use std::fs::File;
use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crossterm::cursor::MoveTo;
use crossterm::event::{self, Event, KeyEventKind};
use crossterm::style::{
Attribute, Color as CColor, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor,
};
use crossterm::terminal::{Clear, ClearType, EnableLineWrap};
use crossterm::{execute, queue};
use ratatui::backend::CrosstermBackend;
use ratatui::layout::Rect;
use ratatui::style::Modifier;
use ratatui::text::Line;
use ratatui::{Terminal, TerminalOptions, Viewport};
use super::ansi::{clip_to_width, visible_width, wrap_row, Row, TranscriptStream};
use super::pty::PtyCapture;
use crate::rich_input::{Chrome, EditorOutcome, MountedEditor, RichSurface, ScrollbackSink};
use crate::session_worker::{PanelMode, SurfaceRequest};
use crate::{InputSurface, ReadOutcome};
const IDLE_POLL: Duration = Duration::from_millis(20);
const CLOCK_TICK: Duration = Duration::from_millis(250);
const DRAIN_QUIET: Duration = Duration::from_millis(120);
struct WrapFence<W = File> {
file: W,
state: Arc<FenceState>,
}
#[derive(Default)]
struct FenceState {
wrap_off: AtomicBool,
held: AtomicBool,
}
const WRAP_OFF: &[u8] = b"\x1b[?7l";
const WRAP_ON: &[u8] = b"\x1b[?7h";
impl<W: Write> WrapFence<W> {
fn new(file: W) -> Self {
Self {
file,
state: Arc::default(),
}
}
fn turn_off(&mut self) -> io::Result<()> {
if !self.state.wrap_off.swap(true, Ordering::Relaxed) {
self.file.write_all(WRAP_OFF)?;
}
Ok(())
}
fn hold(&mut self) -> io::Result<()> {
self.state.held.store(true, Ordering::Relaxed);
self.turn_off()?;
self.file.flush()
}
fn release(&mut self) -> io::Result<()> {
self.state.held.store(false, Ordering::Relaxed);
self.flush()
}
}
impl WrapFence {
fn try_clone(&self) -> io::Result<File> {
self.file.try_clone()
}
fn fenced_clone(&self) -> io::Result<Self> {
Ok(Self {
file: self.file.try_clone()?,
state: Arc::clone(&self.state),
})
}
}
impl<W: Write> Write for WrapFence<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.turn_off()?;
self.file.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
if !self.state.held.load(Ordering::Relaxed)
&& self.state.wrap_off.swap(false, Ordering::Relaxed)
{
self.file.write_all(WRAP_ON)?;
}
self.file.flush()
}
}
impl std::os::fd::AsRawFd for WrapFence {
fn as_raw_fd(&self) -> std::os::fd::RawFd {
self.file.as_raw_fd()
}
}
struct Screen {
tty: WrapFence,
term: Terminal<CrosstermBackend<WrapFence>>,
cols: u16,
rows: u16,
top: u16,
block_h: u16,
status_rows: u16,
status: Row,
queued: usize,
region: newt_core::tty::RegionLease,
painted_widths: Vec<usize>,
}
fn block_region(top: u16, block_h: u16) -> newt_core::tty::Region {
newt_core::tty::Region::Rows {
top,
height: block_h,
}
}
#[derive(Debug, PartialEq, Eq)]
struct ModalReservation {
start: u16,
rows: u16,
chat_visible: bool,
}
fn plan_modal_reservation(_block_top: u16, screen_rows: u16, requested: u16) -> ModalReservation {
let rows = requested.min(screen_rows);
ModalReservation {
start: screen_rows.saturating_sub(rows),
rows,
chat_visible: false,
}
}
fn modal_cleanup_bytes(start: u16) -> io::Result<Vec<u8>> {
let mut buf = Vec::new();
queue!(buf, MoveTo(0, start), Clear(ClearType::FromCursorDown))?;
Ok(buf)
}
impl Screen {
fn terminal_size(&self) -> io::Result<(u16, u16)> {
use std::os::fd::AsRawFd;
let mut size: libc::winsize = unsafe { std::mem::zeroed() };
if unsafe { libc::ioctl(self.tty.as_raw_fd(), libc::TIOCGWINSZ, &mut size) } != 0 {
return Err(io::Error::last_os_error());
}
Ok((size.ws_col.max(1), size.ws_row.max(1)))
}
fn viewport_rect(&self) -> Rect {
Rect::new(
0,
self.top + self.status_rows,
self.cols,
self.block_h - self.status_rows,
)
}
fn rebuild_term(&mut self) -> io::Result<()> {
let backend = CrosstermBackend::new(self.tty.fenced_clone()?);
self.term = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fixed(self.viewport_rect()),
},
)?;
self.term.clear()
}
fn remeasure_modal_rows(
&mut self,
requested: u16,
erase_from: u16,
) -> io::Result<ModalReservation> {
let plan = plan_modal_reservation(self.top, self.rows, requested);
self.tty.hold()?;
let mut buf = Vec::new();
queue!(
buf,
crossterm::cursor::Hide,
MoveTo(0, erase_from.min(plan.start)),
Clear(ClearType::FromCursorDown)
)?;
self.tty.write_all(&buf)?;
self.tty.flush()?;
self.term.clear()?;
Ok(plan)
}
fn reserve_modal_rows(&mut self, requested: u16) -> io::Result<ModalReservation> {
self.reserve_modal_rows_from(self.top, requested)
}
fn reserve_modal_rows_from(
&mut self,
occupied_top: u16,
requested: u16,
) -> io::Result<ModalReservation> {
let plan = plan_modal_reservation(self.top, self.rows, requested);
self.tty.hold()?;
let mut buf = Vec::new();
queue!(
buf,
crossterm::cursor::Hide,
MoveTo(0, occupied_top),
Clear(ClearType::FromCursorDown)
)?;
let covered_transcript = occupied_top.saturating_sub(plan.start);
if covered_transcript > 0 {
queue!(buf, MoveTo(0, self.rows.saturating_sub(1)))?;
buf.extend(std::iter::repeat_n(b'\n', covered_transcript as usize));
}
queue!(buf, MoveTo(0, plan.start), Clear(ClearType::FromCursorDown))?;
self.tty.write_all(&buf)?;
self.tty.flush()?;
self.term.clear()?;
Ok(plan)
}
fn place_cursor(&mut self, row: u16) -> io::Result<()> {
execute!(self.tty, MoveTo(0, row))?;
self.tty.flush()
}
fn cleanup_modal(&mut self, reservation: &ModalReservation) -> io::Result<()> {
if reservation.chat_visible {
return Ok(());
}
self.tty
.write_all(&modal_cleanup_bytes(reservation.start)?)?;
self.tty.flush()
}
fn insert_rows(&mut self, rows: Vec<Row>) -> io::Result<()> {
let cols = self.cols as usize;
let phys: Vec<Row> = rows.iter().flat_map(|r| wrap_row(r, cols)).collect();
if phys.is_empty() || phys.len() > u16::MAX as usize {
return Ok(());
}
let (buf, plan) = render_insert(self.top, self.block_h, self.rows, &phys)?;
self.tty.write_all(&buf)?;
self.tty.flush()?;
let moved = plan.new_top != self.top;
self.top = plan.new_top;
self.region.relocate(
block_region(self.top, self.block_h),
newt_core::tty::OnCollision::SuspendHolder,
);
if moved {
self.rebuild_term()?;
} else {
self.term.clear()?;
}
Ok(())
}
fn relayout(&mut self, editor_rows: u16, status_rows: u16) -> io::Result<()> {
let new_h = (editor_rows + status_rows).clamp(1, self.rows.max(1));
if new_h == self.block_h && status_rows == self.status_rows {
return Ok(());
}
let mut buf = Vec::new();
queue!(buf, MoveTo(0, self.top), Clear(ClearType::FromCursorDown))?;
if self.top + new_h > self.rows {
let d = self.top + new_h - self.rows;
queue!(buf, MoveTo(0, self.rows.saturating_sub(1)))?;
buf.extend(std::iter::repeat_n(b'\n', d as usize));
self.top = self.rows - new_h;
}
self.tty.write_all(&buf)?;
self.tty.flush()?;
self.block_h = new_h;
self.region.relocate(
block_region(self.top, self.block_h),
newt_core::tty::OnCollision::SuspendHolder,
);
self.status_rows = status_rows;
self.rebuild_term()
}
fn resize(
&mut self,
cols: u16,
rows: u16,
editor_rows: u16,
status_rows: u16,
) -> io::Result<()> {
let old_top = self.top;
let lifted = if cols.max(1) < self.cols {
reflow_growth(&self.painted_widths, cols.max(1))
} else {
0
};
self.cols = cols.max(1);
self.rows = rows.max(1);
let new_h = (editor_rows + status_rows).clamp(1, self.rows);
self.block_h = new_h;
self.status_rows = status_rows;
self.top = resize_new_top(old_top, self.rows, new_h);
self.region.relocate(
block_region(self.top, self.block_h),
newt_core::tty::OnCollision::SuspendHolder,
);
let erase_from = resize_erase_from(old_top, self.top, self.rows).saturating_sub(lifted);
let mut buf = Vec::new();
queue!(buf, MoveTo(0, erase_from), Clear(ClearType::FromCursorDown))?;
self.tty.write_all(&buf)?;
self.tty.flush()?;
self.rebuild_term()
}
fn draw(
&mut self,
editor: &MountedEditor,
chrome: Chrome<'_>,
chat_inactive: bool,
) -> io::Result<()> {
let mut widths = Vec::with_capacity(self.block_h as usize);
if self.status_rows == 1 {
let cols = self.cols as usize;
let chip = if self.queued > 0 {
format!(" ⏎ queued: {}", self.queued)
} else {
String::new()
};
let chip_w = newt_core::tty::str_width(&chip);
let avail = cols.saturating_sub(chip_w + 1);
let status = clip_to_width(&self.status, avail);
widths.push(if chip.is_empty() {
visible_width(&status)
} else {
cols
});
let mut buf = Vec::new();
queue!(buf, crossterm::cursor::Hide, MoveTo(0, self.top))?;
buf.extend_from_slice(&status);
queue!(
buf,
SetAttribute(Attribute::Reset),
ResetColor,
Clear(ClearType::UntilNewLine)
)?;
if !chip.is_empty() {
let x = (cols - chip_w) as u16;
queue!(
buf,
MoveTo(x, self.top),
SetForegroundColor(CColor::DarkGrey),
crossterm::style::Print(chip),
ResetColor
)?;
}
self.tty.write_all(&buf)?;
self.tty.flush()?;
}
let frame = self.term.draw(|f| editor.draw(f, chrome, chat_inactive))?;
let area = frame.area;
widths.extend((area.top()..area.bottom()).map(|y| {
(area.left()..area.right())
.rev()
.find(|&x| frame.buffer[(x, y)].symbol() != " ")
.map_or(0, |x| usize::from(x - area.left()) + 1)
}));
self.painted_widths = widths;
Ok(())
}
fn shutdown(&mut self, trailing: &[u8]) -> io::Result<()> {
let mut buf = Vec::new();
queue!(buf, MoveTo(0, self.top), Clear(ClearType::FromCursorDown))?;
if !trailing.is_empty() {
buf.extend_from_slice(trailing);
buf.extend_from_slice(b"\x1b[0m\r\n");
}
queue!(
buf,
EnableLineWrap,
crossterm::event::DisableBracketedPaste,
crossterm::cursor::Show
)?;
self.tty.write_all(&buf)?;
self.tty.flush()?;
Ok(())
}
}
impl ScrollbackSink for Screen {
fn insert(&mut self, lines: Vec<Line<'static>>) -> io::Result<()> {
let rows = lines
.iter()
.map(line_to_ansi)
.collect::<io::Result<Vec<_>>>()?;
self.insert_rows(rows)
}
}
#[derive(Debug, PartialEq, Eq)]
struct InsertPlan {
extra_scroll: u16,
new_top: u16,
}
fn render_insert(
top: u16,
block_h: u16,
rows: u16,
phys: &[Row],
) -> io::Result<(Vec<u8>, InsertPlan)> {
let k = phys.len() as u16;
let plan = plan_insert(top, block_h, rows, k);
let mut buf = Vec::with_capacity(phys.iter().map(Vec::len).sum::<usize>() + 64);
queue!(buf, MoveTo(0, top), Clear(ClearType::FromCursorDown))?;
for (i, row) in phys.iter().enumerate() {
if i > 0 {
buf.extend_from_slice(b"\r\n");
}
buf.extend_from_slice(row);
buf.extend_from_slice(b"\x1b[0m");
}
if plan.extra_scroll > 0 {
queue!(buf, MoveTo(0, rows.saturating_sub(1)))?;
buf.extend(std::iter::repeat_n(b'\n', plan.extra_scroll as usize));
}
Ok((buf, plan))
}
fn write_mode_restores(w: &mut impl io::Write) -> io::Result<()> {
let mut buf = Vec::new();
queue!(
buf,
EnableLineWrap,
crossterm::event::DisableBracketedPaste,
crossterm::cursor::Show
)?;
w.write_all(&buf)
}
fn restore_terminal_modes() {
let _ = write_mode_restores(&mut io::stdout());
}
fn resize_new_top(old_top: u16, new_rows: u16, new_h: u16) -> u16 {
if old_top.saturating_add(new_h) <= new_rows {
old_top
} else {
new_rows.saturating_sub(new_h)
}
}
fn resize_erase_from(old_top: u16, new_top: u16, rows: u16) -> u16 {
old_top.min(new_top).min(rows.saturating_sub(1))
}
fn panel_erase_from(
old_start: u16,
old_rows: u16,
old_cols: u16,
cols: u16,
rows: u16,
requested: u16,
) -> u16 {
let new_start = plan_modal_reservation(0, rows, requested).start;
let lifted = if cols < old_cols {
reflow_growth(&vec![usize::from(old_cols); usize::from(old_rows)], cols)
} else {
0
};
resize_erase_from(old_start, new_start, rows).saturating_sub(lifted)
}
fn reflow_growth(widths: &[usize], cols: u16) -> u16 {
let cols = usize::from(cols.max(1));
let extra: usize = widths
.iter()
.map(|w| w.div_ceil(cols).saturating_sub(1))
.sum();
u16::try_from(extra).unwrap_or(u16::MAX)
}
fn plan_insert(top: u16, block_h: u16, rows: u16, k: u16) -> InsertPlan {
let last_row = (top as u32 + k as u32 - 1).min(rows.saturating_sub(1) as u32) as u16;
let floor = rows.saturating_sub(block_h);
let after = last_row + 1;
if after > floor {
InsertPlan {
extra_scroll: after - floor,
new_top: floor,
}
} else {
InsertPlan {
extra_scroll: 0,
new_top: after,
}
}
}
fn line_to_ansi(line: &Line<'_>) -> io::Result<Row> {
let mut out = Vec::new();
for span in &line.spans {
if let Some(fg) = span.style.fg {
queue!(out, SetForegroundColor(CColor::from(fg)))?;
}
if let Some(bg) = span.style.bg {
queue!(out, SetBackgroundColor(CColor::from(bg)))?;
}
if span.style.add_modifier.contains(Modifier::BOLD) {
queue!(out, SetAttribute(Attribute::Bold))?;
}
if span.style.add_modifier.contains(Modifier::DIM) {
queue!(out, SetAttribute(Attribute::Dim))?;
}
out.extend_from_slice(span.content.as_bytes());
queue!(out, SetAttribute(Attribute::Reset), ResetColor)?;
}
Ok(out)
}
struct Turn {
cancel: Arc<AtomicBool>,
}
pub(crate) struct Presenter {
surface: RichSurface,
editor: MountedEditor,
screen: Screen,
capture: PtyCapture,
stream: TranscriptStream,
pending_read: Option<SyncSender<anyhow::Result<ReadOutcome>>>,
#[cfg(feature = "live-spill")]
spills: Option<Arc<crate::completed_spill::CompletedSpillArchive>>,
queued: VecDeque<String>,
turn: Option<Turn>,
arbiter: newt_core::tty::EphemeralRegistration,
_arbiter_target: Arc<dyn newt_core::tty::Ephemeral>,
was_suspended: bool,
chat_inactive: bool,
dirty: bool,
last_draw: Instant,
meta: crate::prefix::Sequencer,
_restore: crate::RestoreOnDrop<fn()>,
_raw: newt_core::tty::raw_mode::RawModeGuard,
}
#[cfg(test)]
impl Presenter {
pub(crate) fn pump(&mut self) -> io::Result<()> {
self.drain_pty()?;
self.poll_keys()?;
self.draw()
}
pub(crate) fn claims(&self) -> precedence_ladder::ClaimSet {
self.editor.claim_set()
}
}
struct NoOpEphemeral;
impl newt_core::tty::Ephemeral for NoOpEphemeral {
fn erase(&self) {}
fn restore(&self) {}
}
impl Presenter {
pub(crate) fn open(surface: RichSurface) -> io::Result<Self> {
let (cols, rows) = crossterm::terminal::size()?;
let (cols, rows) = (cols.max(1), rows.max(1));
let cursor = crate::inline_viewport::cursor_position_or_anchor();
let (x, y) = (cursor.x, cursor.y);
let mut editor = MountedEditor::new(
surface.edit(),
surface.gutter(),
surface.history(),
crate::type_ahead::take().trim_end_matches('\n'),
);
let editor_rows = editor.wanted_rows(cols, rows, &surface.chrome());
let block_h = editor_rows.clamp(1, rows);
let mut stdout = io::stdout();
let mut y = y;
if x > 0 {
stdout.write_all(b"\r\n")?;
y = (y + 1).min(rows - 1);
}
let top = if y + block_h > rows {
let d = y + block_h - rows;
execute!(stdout, MoveTo(0, rows - 1))?;
for _ in 0..d {
stdout.write_all(b"\n")?;
}
stdout.flush()?;
rows - block_h
} else {
y
};
let raw = newt_core::tty::raw_mode::RawModeGuard::enter()?;
execute!(stdout, crossterm::event::EnableBracketedPaste)?;
let restore: crate::RestoreOnDrop<fn()> = crate::RestoreOnDrop {
restore: restore_terminal_modes,
};
let capture = PtyCapture::install(cols, rows)?;
let tty = WrapFence::new(capture.tty().try_clone()?);
let backend = CrosstermBackend::new(tty.fenced_clone()?);
let term = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fixed(Rect::new(0, top, cols, block_h)),
},
)?;
let region = newt_core::tty::Terminal::lease_region(
block_region(top, block_h),
newt_core::tty::OnCollision::Refuse,
)
.ok_or_else(|| io::Error::other("another surface already owns the cockpit's rows"))?;
let mut screen = Screen {
tty,
term,
cols,
rows,
top,
block_h,
status_rows: 0,
status: Vec::new(),
queued: 0,
region,
painted_widths: Vec::new(),
};
screen.term.clear()?;
let ephemeral: Arc<dyn newt_core::tty::Ephemeral> = Arc::new(NoOpEphemeral);
let arbiter = newt_core::tty::Terminal::register_ephemeral(&ephemeral);
Ok(Self {
surface,
editor,
screen,
capture,
stream: TranscriptStream::new(),
pending_read: None,
#[cfg(feature = "live-spill")]
spills: None,
queued: VecDeque::new(),
turn: None,
arbiter,
_arbiter_target: ephemeral,
was_suspended: false,
chat_inactive: false,
dirty: true,
meta: crate::prefix::Sequencer::new(crate::prefix::current()),
last_draw: Instant::now(),
_restore: restore,
_raw: raw,
})
}
pub(crate) fn run(mut self, requests: &Receiver<SurfaceRequest>) -> io::Result<()> {
loop {
loop {
match requests.try_recv() {
Ok(req) => self.handle_request(req)?,
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => return self.finish(),
}
}
self.drain_pty()?;
self.poll_keys()?;
self.sync_modal_edge();
if self.dirty || self.last_draw.elapsed() >= CLOCK_TICK {
self.draw()?;
}
}
}
fn finish(mut self) -> io::Result<()> {
let mut quiet_since = Instant::now();
while quiet_since.elapsed() < DRAIN_QUIET {
if self.drain_pty()? {
quiet_since = Instant::now();
} else {
std::thread::sleep(Duration::from_millis(5));
}
}
let trailing = self.stream.partial().to_vec();
self.screen.shutdown(&trailing)?;
Ok(())
}
pub(crate) fn handle_request(&mut self, req: SurfaceRequest) -> io::Result<()> {
match req {
SurfaceRequest::ReadLine { prompt: _, reply } => {
if self.surface.take_end_quit() {
let _ = reply.send(Ok(ReadOutcome::EndAndQuit));
} else if let Some(line) = self.queued.pop_front() {
self.screen.queued = self.queued.len();
let _ = reply.send(Ok(ReadOutcome::Line(line)));
} else {
self.pending_read = Some(reply);
}
self.dirty = true;
}
SurfaceRequest::PresentClarification {
batch,
hint,
prompt: _,
color: _,
verbose: _,
reply,
} => {
if self.surface.take_end_quit() {
let _ = reply.send(Ok(ReadOutcome::EndAndQuit));
self.dirty = true;
return Ok(());
}
let prompt_output = self.screen.tty.try_clone()?;
if self.dirty {
self.draw()?;
}
let requested_rows =
crate::clarification_modal::requested_rows(&batch, self.screen.cols);
let reservation = self.screen.reserve_modal_rows(requested_rows)?;
self.chat_inactive = true;
if reservation.chat_visible {
if let Err(error) = self.draw() {
self.chat_inactive = false;
return Err(error);
}
}
if let Err(error) = self.screen.place_cursor(reservation.start) {
self.chat_inactive = false;
let _ = self.finish_modal(Some(&reservation));
let _ = self.draw();
return Err(error);
}
let window = Self::suspend_terminal(prompt_output);
let result: io::Result<ReadOutcome> = self
.screen
.tty
.try_clone()
.and_then(|out| {
crate::inline_viewport::cockpit_panel_terminal(
out,
Rect::new(0, reservation.start, self.screen.cols, reservation.rows),
)
})
.and_then(|mut terminal| {
crate::clarification_modal::present_in(&mut terminal, &batch, &hint)
});
drop(window);
let modal_cleanup = self.finish_modal(Some(&reservation));
self.chat_inactive = false;
let repaint = (|| -> io::Result<()> {
self.screen.term.clear()?;
self.draw()
})();
let _ = reply.send(result.map_err(anyhow::Error::from));
modal_cleanup?;
repaint?;
}
SurfaceRequest::Reload { reply } => {
let result = self.surface.reload();
let draft = self.editor.draft();
let vi = self.editor.take_vi();
self.editor = MountedEditor::new(
self.surface.edit(),
self.surface.gutter(),
self.surface.history(),
&draft,
);
self.editor.adopt_vi(vi);
self.editor.set_turn_running(self.turn.is_some());
let _ = reply.send(result);
self.dirty = true;
}
SurfaceRequest::AddHistory(entry) => {
self.surface.add_history(&entry);
self.editor.set_history(self.surface.history());
}
SurfaceRequest::SaveHistory => self.surface.save_history(),
SurfaceRequest::SetRuntimeContext {
model,
endpoint,
gauge,
session,
} => {
self.surface
.set_runtime_context(&model, &endpoint, gauge, &session);
self.dirty = true;
}
SurfaceRequest::SetBackgroundJobs(jobs) => {
self.surface.set_background_jobs(jobs);
self.dirty = true;
}
#[cfg(feature = "live-spill")]
SurfaceRequest::SetSpillArchive(archive) => {
archive.enable_inspection();
self.spills = Some(archive);
self.screen.insert_rows(vec![
b"F4 inspect retained output (including /spill open IDs)".to_vec(),
])?;
}
SurfaceRequest::SetTabs(tabs) => {
self.surface.set_tabs(tabs);
self.dirty = true;
}
SurfaceRequest::TurnStarted { cancel } => {
self.turn = Some(Turn { cancel });
self.editor.set_turn_running(true);
self.dirty = true;
}
SurfaceRequest::RunBang {
command,
color,
verbose,
reply,
} => {
let result = self.run_bang_command(&command, color, verbose);
let _ = reply.send(result.map_err(Into::into));
}
SurfaceRequest::Interact { interaction, reply } => {
let prompt_output = self.screen.tty.try_clone()?;
if self.dirty {
self.draw()?;
}
let requested_rows =
crate::interaction_view::requested_rows(&interaction, self.screen.cols);
let reservation = self.screen.reserve_modal_rows(requested_rows)?;
self.chat_inactive = true;
if reservation.chat_visible {
if let Err(error) = self.draw() {
self.chat_inactive = false;
return Err(error);
}
}
if let Err(error) = self.screen.place_cursor(reservation.start) {
self.chat_inactive = false;
let _ = self.finish_modal(Some(&reservation));
let _ = self.draw();
return Err(error);
}
let window = Self::suspend_terminal(prompt_output);
let rich = self.screen.tty.try_clone().and_then(|out| {
let terminal = crate::inline_viewport::cockpit_panel_terminal(
out,
Rect::new(0, reservation.start, self.screen.cols, reservation.rows),
)?;
crate::interaction_view::present_in(terminal, &interaction)
});
let (outcome, prompt_notice) = match rich {
Ok((outcome, _)) => crate::permissions::apply_chat_prompt_policy(outcome),
Err(_) => crate::permissions::present_on_terminal_with_width(
&window,
&interaction,
usize::from(self.screen.cols),
),
};
drop(window);
let modal_cleanup = self.finish_modal(Some(&reservation));
self.chat_inactive = false;
let repaint = (|| {
self.screen.term.clear()?;
if let Some(notice) = prompt_notice {
self.screen.insert_rows(vec![notice.as_bytes().to_vec()])?;
}
self.draw()
})();
let _ = reply.send(outcome);
modal_cleanup?;
repaint?;
}
SurfaceRequest::Panel { mode, reply } => {
let panel_output = match self.screen.tty.try_clone() {
Ok(tty) => tty,
Err(_) => {
let _ = reply.send(None);
return Ok(());
}
};
if self.dirty {
self.draw()?;
}
let reservation = match mode {
PanelMode::Inline(rows) => Some(self.screen.reserve_modal_rows(rows)?),
PanelMode::AlternateScreen => None,
};
self.chat_inactive = true;
if reservation
.as_ref()
.is_some_and(|window| window.chat_visible)
{
if let Err(error) = self.draw() {
self.chat_inactive = false;
return Err(error);
}
}
let (release, released) = std::sync::mpsc::sync_channel(1);
let window = crate::session_worker::PanelWindow::new(
panel_output,
self.lent_area(reservation.as_ref()),
Some(release),
);
if reply.send(Some(window)).is_err() {
self.chat_inactive = false;
let cleanup = self.finish_modal(reservation.as_ref());
let _ = self.screen.term.clear();
let _ = self.draw();
return cleanup;
}
let mut reservation = reservation;
let mut mode = mode;
while let Ok(crate::session_worker::PanelSignal::Remeasure { rows, reply }) =
released.recv()
{
if let (Some(rows), PanelMode::Inline(_)) = (rows, mode) {
mode = PanelMode::Inline(rows);
}
let _ = self.remeasure_panel(mode, &mut reservation);
let _ = reply.send(self.lent_area(reservation.as_ref()));
}
let modal_cleanup = self.finish_modal(reservation.as_ref());
self.chat_inactive = false;
let repaint = (|| {
self.screen.term.clear()?;
self.draw()
})();
modal_cleanup?;
repaint?;
}
SurfaceRequest::TurnEnded => {
self.turn = None;
self.editor.set_turn_running(false);
newt_core::tty::set_interrupt_pending(false);
self.dirty = true;
}
}
Ok(())
}
fn drain_pty(&mut self) -> io::Result<bool> {
let mut any = false;
let mut buf = [0u8; 8192];
loop {
let mut pfd = libc::pollfd {
fd: self.capture.master_fd(),
events: libc::POLLIN,
revents: 0,
};
let ready = unsafe { libc::poll(&mut pfd, 1, 0) };
if ready <= 0 || pfd.revents & libc::POLLIN == 0 {
break;
}
let n = self.capture.read_available(&mut buf)?;
if n == 0 {
break;
}
any = true;
let drained = self.stream.feed(&buf[..n]);
if !drained.lines.is_empty() {
self.screen.insert_rows(drained.lines)?;
self.dirty = true;
}
if !drained.passthrough.is_empty() {
self.screen.tty.write_all(&drained.passthrough)?;
self.screen.tty.flush()?;
}
if drained.partial_changed {
self.screen.status = self.stream.partial().to_vec();
self.dirty = true;
}
}
Ok(any)
}
fn poll_keys(&mut self) -> io::Result<()> {
let Some(_stdin) = newt_core::tty::try_watch_stdin() else {
std::thread::sleep(IDLE_POLL);
return Ok(());
};
let mut fds = [
libc::pollfd {
fd: libc::STDIN_FILENO,
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: self.capture.master_fd(),
events: libc::POLLIN,
revents: 0,
},
];
let ready = unsafe { libc::poll(fds.as_mut_ptr(), 2, IDLE_POLL.as_millis() as i32) };
if fds[0].revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0 {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"terminal input closed",
));
}
if (ready <= 0 || fds[0].revents & libc::POLLIN == 0) && !event::poll(Duration::ZERO)? {
return Ok(());
}
while event::poll(Duration::from_millis(1))? {
let evt = event::read()?;
#[cfg(feature = "live-spill")]
if matches!(evt, Event::Key(key) if key.kind == KeyEventKind::Press && key.code == crossterm::event::KeyCode::F(4))
{
drop(_stdin);
return self.on_event(evt);
}
self.on_event(evt)?;
}
Ok(())
}
fn on_event(&mut self, evt: Event) -> io::Result<()> {
self.dirty = true;
match evt {
#[cfg(feature = "live-spill")]
Event::Key(key)
if key.kind == KeyEventKind::Press
&& key.code == crossterm::event::KeyCode::F(4) =>
{
if let Some(archive) = &self.spills {
let snapshot = archive.snapshot();
let output = self.screen.tty.try_clone()?;
let window = Self::suspend_terminal(self.screen.tty.try_clone()?);
let result = crate::transcript_pager::run_spill_picker(&snapshot, output);
drop(window);
let cleanup = self.finish_modal(None);
self.screen.term.clear()?;
self.draw()?;
cleanup?;
result?;
}
Ok(())
}
Event::Resize(cols, rows) => {
self.capture.resize(cols, rows);
let status_rows = self.status_rows();
let editor_rows = self.editor.wanted_rows(cols, rows, &self.surface.chrome());
self.screen.resize(cols, rows, editor_rows, status_rows)
}
Event::Key(key) if key.kind == KeyEventKind::Press && self.escapes(&key) => {
self.escape_during_turn();
Ok(())
}
Event::Key(key) if key.kind == KeyEventKind::Press && self.meta_key(&key)? => Ok(()),
other => {
let outcome = self.editor.on_event(other, &mut self.screen)?;
if let Some(outcome) = outcome {
self.on_outcome(outcome);
}
Ok(())
}
}
}
fn meta_key(&mut self, key: &crossterm::event::KeyEvent) -> io::Result<bool> {
use crate::prefix::{MetaAction, Sequencer, Step, BINDINGS};
let prefix = crate::prefix::current();
if self.meta != Sequencer::new(prefix) && !self.meta.armed() {
self.meta = Sequencer::new(prefix);
}
let ctrl = key
.modifiers
.contains(crossterm::event::KeyModifiers::CONTROL);
let note = match self
.meta
.feed(crate::panel::key_from_event(key.code, ctrl), &BINDINGS)
{
Step::Pass(_) => return Ok(false),
Step::Armed | Step::Cancelled => return Ok(true),
Step::Act(MetaAction::Redraw) => {
self.screen.term.clear()?;
self.draw()?;
return Ok(true);
}
Step::Act(MetaAction::Help) => format!(
"{}: {} (zoom and resize act on an open panel)",
crate::prefix::chord_label(prefix),
BINDINGS.describe()
),
Step::Act(action @ (MetaAction::Zoom | MetaAction::Resize)) => format!(
"{} acts on an open panel (e.g. /settings) — at the prompt: {} then ? for keys",
action.name(),
crate::prefix::chord_label(prefix)
),
};
self.screen.insert_rows(vec![note.into_bytes()])?;
Ok(true)
}
fn lent_area(&self, reservation: Option<&ModalReservation>) -> Rect {
let (top, rows) = reservation.map_or((0, self.screen.rows), |r| (r.start, r.rows));
Rect::new(0, top, self.screen.cols, rows)
}
fn remeasure_panel(
&mut self,
mode: PanelMode,
reservation: &mut Option<ModalReservation>,
) -> io::Result<()> {
let (cols, rows) = self.screen.terminal_size()?;
let resized = (cols, rows) != (self.screen.cols, self.screen.rows);
let old = reservation.as_ref().map(|r| (r.start, r.rows));
let old_cols = self.screen.cols;
if resized {
self.on_event(Event::Resize(cols, rows))?;
}
let (PanelMode::Inline(requested), Some((old_start, old_rows))) = (mode, old) else {
return Ok(());
};
if resized {
let erase_from = panel_erase_from(old_start, old_rows, old_cols, cols, rows, requested);
*reservation = Some(self.screen.remeasure_modal_rows(requested, erase_from)?);
} else if old_rows != requested.min(self.screen.rows) {
*reservation = Some(self.screen.reserve_modal_rows_from(old_start, requested)?);
}
Ok(())
}
fn finish_modal(&mut self, reservation: Option<&ModalReservation>) -> io::Result<()> {
let cleanup = self.finish_modal_rows(reservation);
let released = self.screen.tty.release();
cleanup.and(released)
}
fn finish_modal_rows(&mut self, reservation: Option<&ModalReservation>) -> io::Result<()> {
let (cols, rows) = self.screen.terminal_size()?;
if (cols, rows) != (self.screen.cols, self.screen.rows) {
if reservation.is_some() {
self.screen.tty.write_all(&modal_cleanup_bytes(0)?)?;
self.screen.tty.flush()?;
}
self.on_event(Event::Resize(cols, rows))
} else if let Some(reservation) = reservation {
self.screen.cleanup_modal(reservation)
} else {
Ok(())
}
}
fn suspend_terminal(output: File) -> newt_core::tty::PromptWindow {
newt_core::tty::Terminal::suspend_for_prompt_to(
output,
newt_core::tty::TerminalTaker::CockpitModal,
)
}
fn run_bang_command(
&mut self,
command: &crate::OperatorCommand,
color: bool,
verbose: bool,
) -> io::Result<bool> {
let (mut command, shell) = command.prepare()?;
self.capture.foreground_stdio(&mut command)?;
self.drain_pty()?;
let result = (|| {
let _window = Self::suspend_terminal(self.screen.tty.try_clone()?);
let _cooked = self._raw.suspend()?;
let mut modes_output = self.screen.tty.try_clone()?;
let _modes = crate::RestoreOnDrop {
restore: move || {
let _ = execute!(modes_output, crossterm::event::EnableBracketedPaste);
},
};
self.screen.shutdown(&[])?;
Ok(crate::run_bang_escape_unix(command, &shell, color, verbose))
})();
let resumed = (|| {
let (cols, rows) = self.screen.terminal_size()?;
let height = (self.editor.wanted_rows(cols, rows, &self.surface.chrome())
+ self.status_rows())
.clamp(1, rows);
execute!(self.screen.tty, MoveTo(0, rows - 1))?;
self.screen
.tty
.write_all(&vec![b'\n'; usize::from(height)])?;
self.screen.top = rows - height;
self.on_event(Event::Resize(cols, rows))?;
self.draw()
})();
result.and_then(|success| resumed.map(|()| success))
}
fn escapes(&self, key: &crossterm::event::KeyEvent) -> bool {
let Some(trigger) = crate::esc_ladder::trigger_name(key) else {
return false;
};
let claiming = self.editor.claim_set();
matches!(
crate::esc_ladder::ESC_LADDER.resolve(
trigger,
&precedence_ladder::Situation {
claiming: &claiming,
work_running: self.turn.is_some(),
},
),
precedence_ladder::Verdict::Escape { .. }
)
}
fn escape_during_turn(&mut self) {
let Some(turn) = self.turn.as_ref() else {
return;
};
turn.cancel.store(true, Ordering::SeqCst);
newt_core::tty::note_interrupt_press();
}
fn on_outcome(&mut self, outcome: EditorOutcome) {
match outcome {
EditorOutcome::Line(body) => self.submit(body),
EditorOutcome::LineThenQuit(body) => {
self.surface.arm_end_quit();
self.submit(body);
}
EditorOutcome::EndAndQuit => {
if let Some(reply) = self.pending_read.take() {
let _ = reply.send(Ok(ReadOutcome::EndAndQuit));
} else {
self.surface.arm_end_quit();
}
}
EditorOutcome::Tab(action) => {
if let Some(reply) = self.pending_read.take() {
let _ = reply.send(Ok(ReadOutcome::Tab(action)));
}
}
EditorOutcome::Eof => {
if let Some(reply) = self.pending_read.take() {
let _ = reply.send(Ok(ReadOutcome::Eof));
}
}
}
}
fn submit(&mut self, body: String) {
if let Some(reply) = self.pending_read.take() {
let _ = reply.send(Ok(ReadOutcome::Line(body)));
} else {
self.queued.push_back(body);
self.screen.queued = self.queued.len();
}
}
fn status_rows(&self) -> u16 {
u16::from(!self.screen.status.is_empty() || !self.queued.is_empty())
}
fn sync_modal_edge(&mut self) {
let suspended = self.arbiter.suspended();
if self.was_suspended != suspended {
let _ = self.screen.term.clear();
self.dirty = true;
}
self.was_suspended = suspended;
}
fn draw(&mut self) -> io::Result<()> {
let status_rows = self.status_rows();
let editor_rows =
self.editor
.wanted_rows(self.screen.cols, self.screen.rows, &self.surface.chrome());
self.screen.relayout(editor_rows, status_rows)?;
self.screen
.draw(&self.editor, self.surface.chrome(), self.chat_inactive)?;
self.dirty = false;
self.last_draw = Instant::now();
Ok(())
}
}
pub(crate) fn supported() -> bool {
use std::io::IsTerminal as _;
io::stdin().is_terminal()
&& io::stdout().is_terminal()
&& !newt_core::tty::protocol_mode()
&& std::env::var_os("NEWT_NO_COCKPIT").is_none()
}
#[cfg(test)]
mod tests {
#[test]
fn the_cockpit_takes_raw_mode_only_through_the_guard() {
let src = crate::production_source(include_str!("presenter.rs"));
for call in [
"enable_raw_mode()?",
"enable_raw_mode();",
"disable_raw_mode();",
] {
assert_eq!(
src.matches(call).count(),
0,
"`{call}` is a second raw-mode owner on crossterm's \
process-global; the cockpit takes raw through RawModeGuard"
);
}
assert!(
src.contains("_raw: newt_core::tty::raw_mode::RawModeGuard"),
"the session must HOLD a RawModeGuard"
);
}
#[test]
fn the_escape_restores_are_declared_before_raw_mode() {
let src = crate::production_source(include_str!("presenter.rs"));
let restore = src
.find(" _restore: crate::RestoreOnDrop<fn()>,")
.expect("the escape-sequence guard is a field");
let raw = src
.find(" _raw: newt_core::tty::raw_mode::RawModeGuard,")
.expect("raw mode is a field");
assert!(
restore < raw,
"_restore must be declared before _raw so the escape restores run \
first; swapping them inverts the teardown order silently"
);
}
use super::*;
#[test]
fn insert_plan_below_the_fold_moves_the_block_down_without_scrolling() {
assert_eq!(
plan_insert(10, 4, 24, 3),
InsertPlan {
extra_scroll: 0,
new_top: 13
}
);
}
#[test]
fn insert_plan_at_the_bottom_scrolls_by_exactly_the_rows_written() {
assert_eq!(
plan_insert(20, 4, 24, 3),
InsertPlan {
extra_scroll: 3,
new_top: 20
}
);
}
#[test]
fn insert_plan_overshooting_the_screen_scrolls_the_overshoot_plus_the_block() {
assert_eq!(
plan_insert(20, 4, 24, 10),
InsertPlan {
extra_scroll: 4,
new_top: 20
}
);
}
#[test]
fn insert_plan_crossing_the_fold_scrolls_only_what_does_not_fit() {
assert_eq!(
plan_insert(18, 4, 24, 4),
InsertPlan {
extra_scroll: 2,
new_top: 20
}
);
}
#[test]
fn modal_reservation_occludes_the_chat_block() {
assert_eq!(
plan_modal_reservation(20, 24, 5),
ModalReservation {
start: 19,
rows: 5,
chat_visible: false,
}
);
assert_eq!(
plan_modal_reservation(3, 7, 8),
ModalReservation {
start: 0,
rows: 7,
chat_visible: false,
},
"a short terminal gives the blocking modal the whole screen"
);
assert_eq!(
plan_modal_reservation(0, 4, 2),
ModalReservation {
start: 2,
rows: 2,
chat_visible: false,
}
);
}
#[test]
fn modal_cleanup_clears_outside_the_fixed_chat_viewport() {
let cleanup = modal_cleanup_bytes(6).expect("cleanup bytes");
let mut expected = Vec::new();
queue!(expected, MoveTo(0, 6), Clear(ClearType::FromCursorDown)).expect("expected bytes");
assert_eq!(cleanup, expected);
}
fn crlf_count(buf: &[u8]) -> usize {
buf.windows(2).filter(|w| *w == b"\r\n").count()
}
#[test]
fn insert_at_the_bottom_emits_no_trailing_line_feed() {
for k in [4usize, 5, 40] {
let phys: Vec<Row> = (0..k).map(|i| format!("row {i}").into_bytes()).collect();
let (buf, plan) = render_insert(20, 4, 24, &phys).unwrap();
assert_eq!(
crlf_count(&buf),
k - 1,
"k={k}: rows are separated by \\r\\n, never terminated by one"
);
assert!(
!buf.ends_with(b"\r\n"),
"k={k}: the buffer must not end on a line feed"
);
assert_eq!(plan.new_top, 20, "k={k}: the block stays bottom-anchored");
}
}
#[test]
fn insert_below_the_fold_ends_on_a_reset_not_a_line_feed() {
let phys: Vec<Row> = (0..3).map(|i| format!("r{i}").into_bytes()).collect();
let (buf, plan) = render_insert(5, 4, 24, &phys).unwrap();
assert_eq!(plan.extra_scroll, 0);
assert_eq!(plan.new_top, 8);
assert_eq!(crlf_count(&buf), 2);
assert!(buf.ends_with(b"\x1b[0m"), "last row ends on a reset, no LF");
}
#[test]
fn the_fence_turns_wrap_off_for_a_write_and_back_on_at_flush() {
let mut fence = WrapFence::new(Vec::new());
fence.write_all(b"a").unwrap();
fence.write_all(b"b").unwrap();
fence.flush().unwrap();
fence.flush().unwrap();
assert_eq!(fence.file, b"\x1b[?7lab\x1b[?7h");
}
#[test]
fn a_held_fence_keeps_wrap_off_across_flushes_until_released() {
let mut fence = WrapFence::new(Vec::new());
fence.hold().unwrap();
fence.write_all(b"modal").unwrap();
fence.flush().unwrap();
assert_eq!(fence.file, b"\x1b[?7lmodal");
fence.release().unwrap();
assert_eq!(fence.file, b"\x1b[?7lmodal\x1b[?7h");
}
#[test]
fn a_hold_binds_every_clone_of_the_fence() {
let mut screen = WrapFence::new(Vec::new());
let mut backend = WrapFence {
file: Vec::new(),
state: Arc::clone(&screen.state),
};
screen.hold().unwrap();
backend.write_all(b"clear").unwrap();
backend.flush().unwrap();
assert_eq!(backend.file, b"clear", "wrap is already off, and stays off");
screen.release().unwrap();
assert_eq!(screen.file, b"\x1b[?7l\x1b[?7h");
}
#[test]
fn the_mode_restores_re_enable_wrap_disable_paste_and_show_the_cursor() {
let mut buf = Vec::new();
write_mode_restores(&mut buf).unwrap();
let s = String::from_utf8_lossy(&buf);
assert!(s.contains("?7h"), "line wrap re-enabled: {s:?}");
assert!(s.contains("?2004l"), "bracketed paste disabled: {s:?}");
assert!(s.contains("?25h"), "cursor shown: {s:?}");
}
#[test]
fn a_narrower_terminal_lifts_the_old_block_by_its_wrapped_rows() {
assert_eq!(reflow_growth(&[23, 70, 80], 47), 2);
assert_eq!(
reflow_growth(&[23, 70, 80], 120),
0,
"a wider terminal wraps nothing"
);
assert_eq!(
reflow_growth(&[95], 47),
2,
"three rows for one 95-wide row"
);
assert_eq!(reflow_growth(&[47], 47), 0, "an exact fit does not wrap");
}
#[test]
fn a_panel_resize_erases_only_what_the_old_panel_could_occupy() {
assert_eq!(panel_erase_from(13, 18, 100, 118, 31, 18), 13);
assert_eq!(panel_erase_from(13, 18, 100, 100, 45, 18), 13);
assert_eq!(panel_erase_from(13, 18, 100, 100, 25, 18), 7);
assert_eq!(panel_erase_from(20, 8, 100, 60, 31, 8), 12);
assert_eq!(panel_erase_from(13, 18, 100, 60, 31, 18), 0);
assert!(panel_erase_from(20, 8, 100, 118, 31, 8) > 0);
}
#[test]
fn growing_the_terminal_keeps_the_block_under_existing_content_2441() {
assert_eq!(resize_new_top(27, 63, 3), 27);
}
#[test]
fn a_shrinking_terminal_still_bottom_anchors_the_block() {
assert_eq!(resize_new_top(20, 10, 3), 7);
}
#[test]
fn growth_too_small_for_the_block_to_fit_falls_back_to_the_bottom() {
assert_eq!(resize_new_top(27, 31, 5), 26);
}
#[test]
fn equal_rows_keeps_the_existing_bottom_anchor_behavior() {
assert_eq!(resize_new_top(20, 24, 4), 20);
}
#[test]
fn repeated_same_size_resizes_do_not_recreate_the_gap_2632() {
let top = resize_new_top(27, 63, 3);
assert_eq!(top, 27);
let top = resize_new_top(top, 63, 3);
assert_eq!(
top, 27,
"a same-size resize must not move a block that already fits"
);
}
#[test]
fn repeated_grows_keep_the_block_under_the_content_2632() {
let mut top = resize_new_top(27, 63, 3);
assert_eq!(top, 27);
top = resize_new_top(top, 90, 3);
assert_eq!(top, 27);
}
#[test]
fn a_partial_shrink_that_still_fits_keeps_the_old_position_2632() {
let top = resize_new_top(27, 63, 3);
assert_eq!(top, 27);
let top = resize_new_top(top, 40, 3);
assert_eq!(top, 27, "old position still fits; must not re-anchor to 37");
}
#[test]
fn resize_erases_from_the_higher_of_the_old_and_new_block_tops() {
assert_eq!(resize_erase_from(20, 26, 30), 20);
assert_eq!(resize_erase_from(20, 12, 24), 12);
assert_eq!(resize_erase_from(20, 6, 10), 6);
assert_eq!(resize_erase_from(20, 20, 24), 20);
}
#[test]
fn a_styled_line_round_trips_to_ansi_with_a_reset_per_span() {
use ratatui::style::{Color, Style};
use ratatui::text::Span;
let line = Line::from(vec![
Span::styled("[t]", Style::default().fg(Color::DarkGray)),
Span::styled(
" body",
Style::default().fg(Color::White).bg(Color::Rgb(82, 82, 82)),
),
]);
let bytes = line_to_ansi(&line).unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("[t]"), "{s:?}");
assert!(s.contains(" body"), "{s:?}");
let color_suppressed = std::env::var_os("NO_COLOR").is_some()
|| std::env::var("TERM").as_deref() == Ok("dumb");
assert_eq!(
s.contains("48;2;82;82;82"),
!color_suppressed,
"command background follows the runtime color policy: {s:?}"
);
assert!(s.contains("\x1b[0m"), "reset present: {s:?}");
assert_eq!(super::super::ansi::visible_width(&bytes), "[t] body".len());
}
}
#[cfg(test)]
#[path = "presenter_terminal_acceptance.rs"]
mod terminal_acceptance;
#[cfg(all(test, feature = "live-spill"))]
pub(crate) use terminal_acceptance::cockpit_pager_case;
#[cfg(all(test, feature = "rich-tui"))]
pub(crate) use terminal_acceptance::cockpit_psyche_case;
#[cfg(test)]
pub(crate) use terminal_acceptance::{
cockpit_acceptance_case, cockpit_bang_case, cockpit_buffered_input_case,
cockpit_clarification_input_case, cockpit_panel_loop_case, panel_live_resize_case,
panel_resize_case,
};
#[cfg(test)]
#[path = "presenter_migration_acceptance.rs"]
mod migration_acceptance;
#[cfg(test)]
pub(crate) use migration_acceptance::{
cockpit_migration_case, persona_migration_case, startup_migration_case,
};