use std::io::{self, Write};
use ratatui::backend::{Backend, ClearType, CrosstermBackend, WindowSize};
use ratatui::buffer::Cell;
use ratatui::layout::{Position, Rect, Size};
use ratatui::{Terminal, TerminalOptions, Viewport};
pub(crate) enum PanelOut {
Stdout(io::Stdout),
Tty(std::fs::File),
}
impl Write for PanelOut {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Stdout(w) => w.write(buf),
Self::Tty(w) => w.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Stdout(w) => w.flush(),
Self::Tty(w) => w.flush(),
}
}
}
pub(crate) type InlineTerm = Terminal<AnchoredBackend<PanelOut>>;
fn warn_once(err: &io::Error) {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
eprintln!(
"⚠ terminal did not report the cursor position ({err}) — inline \
panels will be anchored at the bottom of the screen"
);
});
}
fn anchor(size: Size) -> Position {
Position {
x: 0,
y: size.height.saturating_sub(1),
}
}
pub(crate) struct AnchoredBackend<W: Write> {
inner: CrosstermBackend<W>,
lease: Option<newt_core::tty::RegionLease>,
}
impl<W: Write> AnchoredBackend<W> {
#[cfg(unix)]
pub(crate) fn new(writer: W) -> Self {
Self::with_lease(writer, None)
}
fn with_lease(writer: W, lease: Option<newt_core::tty::RegionLease>) -> Self {
Self {
lease,
inner: CrosstermBackend::new(writer),
}
}
}
impl<W: Write> Backend for AnchoredBackend<W> {
fn get_cursor_position(&mut self) -> io::Result<Position> {
match crossterm::cursor::position().map(|(x, y)| Position { x, y }) {
Ok(position) => Ok(position),
Err(err) => {
warn_once(&err);
if let Some(newt_core::tty::Region::Rows { top, .. }) =
self.lease.as_ref().map(newt_core::tty::RegionLease::region)
{
return Ok(Position { x: 0, y: top });
}
Ok(anchor(self.inner.size().unwrap_or(Size {
width: 0,
height: 0,
})))
}
}
}
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
self.inner.draw(content)
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.inner.hide_cursor()
}
fn show_cursor(&mut self) -> io::Result<()> {
self.inner.show_cursor()
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
self.inner.set_cursor_position(position)
}
fn clear(&mut self) -> io::Result<()> {
self.inner.clear()
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
self.inner.clear_region(clear_type)
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
self.inner.append_lines(n)
}
fn size(&self) -> io::Result<Size> {
self.inner.size()
}
fn window_size(&mut self) -> io::Result<WindowSize> {
self.inner.window_size()
}
fn flush(&mut self) -> io::Result<()> {
Backend::flush(&mut self.inner)
}
}
impl<W: Write> Write for AnchoredBackend<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Write::flush(&mut self.inner)
}
}
pub(crate) fn inline_terminal(lease: newt_core::tty::RegionLease) -> io::Result<InlineTerm> {
let (top, height) = match lease.region() {
newt_core::tty::Region::Rows { top, height } => (top, height),
newt_core::tty::Region::WholeScreen => {
return Err(io::Error::other(
"an inline viewport cannot be built from a whole-screen lease",
))
}
};
let placed = lease.cursor_return().is_some();
let mut backend = AnchoredBackend::with_lease(PanelOut::Stdout(io::stdout()), Some(lease));
let viewport = if placed {
Viewport::Fixed(Rect::new(0, top, backend.size()?.width, height))
} else {
if top.saturating_add(height) < backend.size()?.height {
backend.set_cursor_position(Position { x: 0, y: top })?;
}
Viewport::Inline(height)
};
Terminal::with_options(backend, TerminalOptions { viewport })
}
pub(crate) fn cockpit_panel_terminal(
out: std::fs::File,
area: ratatui::layout::Rect,
) -> io::Result<InlineTerm> {
Terminal::with_options(
AnchoredBackend::with_lease(PanelOut::Tty(out), None),
TerminalOptions {
viewport: Viewport::Fixed(area),
},
)
}
pub(crate) fn lease_bottom_rows(
height: u16,
policy: newt_core::tty::OnCollision,
) -> io::Result<newt_core::tty::RegionLease> {
let screen =
ratatui::backend::Backend::size(&CrosstermBackend::new(io::stdout())).unwrap_or(Size {
width: 80,
height: 24,
});
let height = height.min(screen.height);
let top = screen.height.saturating_sub(height);
newt_core::tty::Terminal::lease_region(newt_core::tty::Region::Rows { top, height }, policy)
.ok_or_else(|| io::Error::other("another surface already owns these rows"))
}
pub(crate) fn lease_below_transcript(
height: u16,
policy: newt_core::tty::OnCollision,
) -> io::Result<newt_core::tty::RegionLease> {
match newt_core::tty::Terminal::lease_below_cursor(
height,
policy,
newt_core::tty::Sink::Stdout,
)? {
Some(lease) => Ok(lease),
None => lease_bottom_rows(height, policy),
}
}
#[cfg(unix)]
pub(crate) fn cursor_position_or_anchor() -> Position {
let mut backend = AnchoredBackend::new(PanelOut::Stdout(io::stdout()));
backend
.get_cursor_position()
.unwrap_or(Position { x: 0, y: 0 })
}
#[cfg(test)]
mod region_lease_door {
const DESTINATIONS: &[&str] = &["inline_viewport.rs"];
const DOOR: &str = "Viewport::Inline(";
const OPTIONS: &str = "Terminal::with_options(";
fn sources() -> Vec<(String, String)> {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut out = Vec::new();
let mut stack = vec![dir];
while let Some(d) = stack.pop() {
for entry in std::fs::read_dir(&d).expect("newt-tui/src is readable") {
let path = entry.expect("a readable entry").path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|e| e == "rs") {
let name = path
.file_name()
.expect("a file name")
.to_string_lossy()
.into_owned();
out.push((name, std::fs::read_to_string(&path).expect("readable")));
}
}
}
out
}
const UNLEASED_REGION_CLAIMS: &[(&str, usize)] = &[];
#[test]
fn the_inline_viewport_door_exists_in_exactly_one_declared_place() {
let files = sources();
assert!(
files.len() > 10,
"the scan found {} files, so it is not reading the crate",
files.len()
);
assert!(
files.iter().any(|(n, _)| n == "inline_viewport.rs"),
"the scan never reached the file that holds the door"
);
for (name, body) in &files {
if body.contains(DOOR) {
assert!(
DESTINATIONS.contains(&name.as_str()),
"`{DOOR}` appears in {name}, which is not a declared \
destination. An inline viewport is a claim on rows: mint a \
`RegionLease` and go through `inline_terminal`."
);
}
}
}
#[test]
fn every_region_claim_holds_a_lease() {
const HOLDS: &str = "RegionLease";
for (name, body) in &sources() {
if name == "inline_viewport.rs" {
continue; }
if !body.contains(OPTIONS) {
continue;
}
let allowed = UNLEASED_REGION_CLAIMS
.iter()
.find(|(f, _)| *f == name)
.map_or(0, |(_, n)| *n);
assert!(
body.contains(HOLDS) || allowed > 0,
"{name} builds a viewport but holds no `RegionLease`. Rows a \
surface claims without telling the arbiter are rows another \
surface can be handed (#1977)."
);
}
}
#[test]
fn every_alternate_screen_entry_holds_a_region_lease() {
const ENTER: &str = "EnterAlternateScreen)";
const HOLDS: &str = "_region: newt_core::tty::RegionLease";
let files = sources();
assert!(
files.len() > 10,
"the scan found {} files, so it is not reading the crate",
files.len()
);
let mut entrants = 0;
for (name, body) in &files {
if !body.contains(ENTER) {
continue;
}
entrants += 1;
assert!(
body.contains(HOLDS),
"{name} enters the alternate screen but holds no `RegionLease`. \
Taking every row without telling the arbiter is what let an \
inline surface open underneath the pager (#1980)."
);
}
assert_eq!(
entrants, 2,
"expected the pager and the splash to be the two alternate-screen \
entrants; found {entrants}. A new one must hold the screen, and a \
removed one must lower this number."
);
}
const CURSOR_RELATIVE_CLAIMS: &[(&str, &str)] = &[
(
"lean_input.rs",
"rows_above_cursor on the plain-scroller path",
),
(
"live_spill.rs",
"width-reflow cleanup rewinds and clears from the cursor down",
),
];
fn cursor_relative_claim(body: &str) -> bool {
body.contains("Clear(ClearType::FromCursorDown)")
&& (body.contains(concat!("Move", "Up("))
|| !(body.contains("RegionLease") || body.contains("lease_bottom_rows")))
}
#[test]
fn a_lease_does_not_hide_cursor_relative_reflow_cleanup() {
assert!(cursor_relative_claim(concat!(
"RegionLease; Move",
"Up(3); Clear(ClearType::FromCursorDown)"
)));
assert!(!cursor_relative_claim(
"RegionLease; MoveTo(0, top); Clear(ClearType::FromCursorDown)"
));
}
#[test]
fn cursor_relative_region_painters_are_the_declared_holdouts() {
let files = sources();
assert!(
files.len() > 10,
"the scan found {} files, so it is not reading the crate",
files.len()
);
let mut found: Vec<&str> = Vec::new();
for (name, body) in &files {
if !cursor_relative_claim(body) {
continue;
}
found.push(name.as_str());
}
found.sort_unstable();
let mut declared: Vec<&str> = CURSOR_RELATIVE_CLAIMS.iter().map(|(f, _)| *f).collect();
declared.sort_unstable();
assert_eq!(
found, declared,
"the cursor-relative holdouts changed. A new one must lease its \
rows or be declared here with a reason; a migrated one must be \
removed from the list."
);
}
#[test]
fn the_door_needle_catches_a_bare_construction() {
let smuggled = "let t = Terminal::with_options(b, TerminalOptions { \
viewport: Viewport::Inline(6) });";
assert!(smuggled.contains(DOOR), "the needle misses a real call");
assert!(
smuggled.contains(OPTIONS),
"the options needle misses a real call"
);
}
#[test]
fn the_door_needle_ignores_prose_about_the_rule() {
let prose = "//! opens this transient ratatui `Viewport::Inline` overlay: one surface";
assert!(
!prose.contains(DOOR),
"the needle matches prose, so the guard would flag documentation"
);
}
}
#[cfg(test)]
mod screen_lease_collision {
use newt_core::tty::{OnCollision, Region, Terminal};
#[serial_test::serial(tty_arbiter)]
#[test]
fn an_inline_surface_is_refused_while_the_whole_screen_is_held() {
let screen = Terminal::lease_region(Region::WholeScreen, OnCollision::SuspendHolder)
.expect("the alternate screen is always grantable");
assert!(
super::lease_bottom_rows(6, OnCollision::Refuse).is_err(),
"an inline surface was granted rows the pager owns"
);
assert!(
super::lease_bottom_rows(6, OnCollision::Shift).is_err(),
"a shift found free rows under a whole-screen holder"
);
drop(screen);
let rows = super::lease_bottom_rows(6, OnCollision::Refuse)
.expect("with the screen free, the inline surface opens");
assert!(matches!(rows.region(), Region::Rows { height: 6, .. }));
}
}