use std::cmp::Ordering;
use std::fmt::Display;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::io;
use unicode_width::UnicodeWidthChar;
use super::dimension::Dimension;
use super::{Position, Styled};
use crate::ansi::{AnsiAware, AnsiAwareChar};
use crate::terminal::{Terminal, TerminalSize};
#[derive(Debug, Default)]
struct FrameRow {
content: Vec<Styled<String>>,
hash: u64,
}
impl FrameRow {
pub fn new(content: Vec<Styled<String>>, hash: u64) -> Self {
Self { content, hash }
}
pub fn get_content(&self) -> &[Styled<String>] {
&self.content
}
pub fn hash(&self) -> u64 {
self.hash
}
}
#[derive(Debug)]
struct FrameState {
pub terminal_size: TerminalSize,
pub frame_size: Dimension,
pub expected_cursor_position: Option<Position>,
pub finished_rows: Vec<FrameRow>,
pub current_styled: Styled<String>,
pub current_line: Vec<Styled<String>>,
pub current_line_width: u16,
pub current_line_hasher: DefaultHasher,
}
impl FrameState {
pub fn new(terminal_size: TerminalSize) -> Self {
Self {
terminal_size,
frame_size: Dimension::new(0, 0),
finished_rows: Vec::new(),
current_styled: Styled::default(),
current_line: Vec::new(),
current_line_hasher: DefaultHasher::default(),
current_line_width: 0,
expected_cursor_position: None,
}
}
pub fn write(&mut self, value: &Styled<impl AsRef<str> + Display>) {
self.current_styled.style = value.style;
for piece in value.content.ansi_aware_chars() {
piece.hash(&mut self.current_line_hasher);
value.style.hash(&mut self.current_line_hasher);
let current_char = match piece {
AnsiAwareChar::Char(c) => c,
AnsiAwareChar::AnsiEscapeSequence(seq) => {
self.current_styled.content.push_str(seq);
continue;
}
};
if current_char == '\n' {
self.finish_line();
continue;
}
let remaining_width_space = self
.terminal_size
.width()
.saturating_sub(self.current_line_width);
let character_length = UnicodeWidthChar::width(current_char).unwrap_or(0) as u16;
if character_length > remaining_width_space {
self.finish_line();
}
self.current_line_width = self.current_line_width.saturating_add(character_length);
self.current_styled.content.push(current_char);
}
if !self.current_styled.content.is_empty() {
self.current_line
.push(std::mem::take(&mut self.current_styled));
}
}
pub fn mark_cursor_position(&mut self, offset: isize) {
let row = self.finished_rows.len() as u16;
let col = self.current_line_width.saturating_add(offset as u16);
let row_offset = col / self.terminal_size.width();
let row = row + row_offset;
let col = col % self.terminal_size.width();
self.expected_cursor_position = Some(Position { row, col });
}
pub fn finish(&mut self) {
self.finish_line();
}
pub fn resize_if_needed(&mut self, new_size: TerminalSize) {
if new_size == self.terminal_size {
return;
}
let mut new_state = Self::new(new_size);
for row in &self.finished_rows {
for styled in row.get_content() {
new_state.write(styled);
}
new_state.finish_line();
}
for styled in &self.current_line {
new_state.write(styled);
}
new_state.finish_line();
*self = new_state;
}
fn finish_line(&mut self) {
let current_styled = std::mem::take(&mut self.current_styled);
self.current_styled.style = current_styled.style;
if !current_styled.content.is_empty() || !current_styled.style.is_empty() {
self.current_line.push(current_styled);
}
let hasher = std::mem::take(&mut self.current_line_hasher);
let content = std::mem::take(&mut self.current_line);
if content.is_empty() {
return;
}
self.finished_rows
.push(FrameRow::new(content, hasher.finish()));
self.frame_size = Dimension::new(
self.frame_size.width().max(self.current_line_width),
self.finished_rows.len() as u16,
);
if !self.current_styled.style.is_empty() {
self.current_styled
.style
.hash(&mut self.current_line_hasher);
}
self.current_line_width = 0;
}
}
#[derive(Debug, Default)]
enum RenderState {
#[default]
Initial,
ActiveRender {
last_rendered_frame: FrameState,
current_frame: FrameState,
},
Rendered(FrameState),
}
pub struct FrameRenderer<T>
where
T: Terminal,
{
terminal: T,
cursor_position: Position,
state: RenderState,
}
impl<T> FrameRenderer<T>
where
T: Terminal,
{
pub fn new(terminal: T) -> io::Result<Self> {
Ok(Self {
terminal,
cursor_position: Position::default(),
state: RenderState::Initial,
})
}
pub fn write(&mut self, value: impl Display) -> io::Result<()> {
self.write_styled(Styled::new(value))
}
pub fn write_styled(&mut self, value: Styled<impl Display>) -> io::Result<()> {
match &mut self.state {
RenderState::Rendered(_) | RenderState::Initial => {}
RenderState::ActiveRender { current_frame, .. } => {
let formatted = format!("{}", value.content);
let value = value.with_content(formatted);
current_frame.write(&value);
}
}
Ok(())
}
pub fn mark_cursor_position(&mut self, offset: isize) {
match &mut self.state {
RenderState::Rendered(_) | RenderState::Initial => {}
RenderState::ActiveRender { current_frame, .. } => {
current_frame.mark_cursor_position(offset);
}
}
}
pub fn start_frame(&mut self) -> io::Result<()> {
let terminal_size = self.refresh_terminal_size();
self.state = match std::mem::replace(&mut self.state, RenderState::Initial) {
RenderState::Initial => RenderState::ActiveRender {
last_rendered_frame: FrameState::new(terminal_size),
current_frame: FrameState::new(terminal_size),
},
RenderState::Rendered(last_rendered_frame) => RenderState::ActiveRender {
last_rendered_frame,
current_frame: FrameState::new(terminal_size),
},
RenderState::ActiveRender {
last_rendered_frame,
current_frame,
} => RenderState::ActiveRender {
last_rendered_frame,
current_frame,
},
};
Ok(())
}
pub fn finish_current_frame(&mut self, add_empty_line: bool) -> io::Result<()> {
let (last_rendered_frame, mut current_frame) = match std::mem::take(&mut self.state) {
RenderState::Rendered(_) | RenderState::Initial => {
return Ok(());
}
RenderState::ActiveRender {
last_rendered_frame,
current_frame,
} => (last_rendered_frame, current_frame),
};
current_frame.finish();
let rows_to_iterate = std::cmp::max(
last_rendered_frame.frame_size.height(),
current_frame.frame_size.height(),
);
self.terminal.cursor_hide()?;
self.move_cursor_to(Position { row: 0, col: 0 })?;
for i in 0..rows_to_iterate {
let last_row = last_rendered_frame.finished_rows.get(i as usize);
let current_row = current_frame.finished_rows.get(i as usize);
match (last_row, current_row) {
(Some(last_row), Some(current_row)) => {
if last_row.hash() != current_row.hash() {
for styled in current_row.get_content() {
self.terminal.write_styled(styled)?;
}
self.terminal.clear_until_new_line()?;
}
}
(Some(_), None) => {
self.terminal.clear_line()?;
}
(None, Some(current_row)) => {
for styled in current_row.get_content() {
self.terminal.write_styled(styled)?;
}
}
(None, None) => {
#[cfg(test)]
unreachable!(
"frame_size should never be larger then finished_rows for both frames"
)
}
}
self.terminal.write("\r")?;
self.cursor_position.col = 0;
if i + 1 < rows_to_iterate {
self.terminal.write("\n")?;
self.cursor_position.row += 1;
}
}
if add_empty_line {
self.terminal.write("\n")?;
self.cursor_position.row += 1;
}
if let Some(expected_cursor_position) = current_frame.expected_cursor_position {
self.move_cursor_to(expected_cursor_position)?;
}
self.terminal.cursor_show()?;
self.terminal.flush()?;
self.state = RenderState::Rendered(current_frame);
Ok(())
}
fn move_cursor_to_end_position(&mut self) -> io::Result<()> {
self.refresh_terminal_size();
let last_rendered = match &mut self.state {
RenderState::Initial => return Ok(()),
RenderState::ActiveRender {
last_rendered_frame,
..
}
| RenderState::Rendered(last_rendered_frame) => last_rendered_frame,
};
let end_position = Position {
col: 0,
row: last_rendered.frame_size.height(),
};
self.move_cursor_to(end_position)?;
Ok(())
}
fn move_cursor_to(&mut self, position: Position) -> io::Result<()> {
let current_cursor_position = self.cursor_position;
match current_cursor_position.row.cmp(&position.row) {
Ordering::Greater => {
self.terminal
.cursor_up(current_cursor_position.row - position.row)?;
}
Ordering::Less => {
self.terminal
.cursor_down(position.row - current_cursor_position.row)?;
}
Ordering::Equal => {}
}
match current_cursor_position.col.cmp(&position.col) {
Ordering::Greater => {
self.terminal
.cursor_left(current_cursor_position.col - position.col)?;
}
Ordering::Less => {
self.terminal
.cursor_right(position.col - current_cursor_position.col)?;
}
Ordering::Equal => {}
}
self.cursor_position = position;
Ok(())
}
fn refresh_terminal_size(&mut self) -> TerminalSize {
let terminal_size = self
.terminal
.get_size()
.unwrap_or_default()
.unwrap_or_default();
if terminal_size.width() < self.cursor_position.col {
let new_line_offset = self.cursor_position.col / terminal_size.width();
let new_col = self.cursor_position.col % terminal_size.width();
self.cursor_position = Position {
row: self.cursor_position.row + new_line_offset,
col: new_col,
};
}
match &mut self.state {
RenderState::Initial => {}
RenderState::ActiveRender {
current_frame,
last_rendered_frame,
} => {
last_rendered_frame.resize_if_needed(terminal_size);
current_frame.resize_if_needed(terminal_size);
}
RenderState::Rendered(last_rendered_frame) => {
last_rendered_frame.resize_if_needed(terminal_size);
}
};
terminal_size
}
}
impl<T> Drop for FrameRenderer<T>
where
T: Terminal,
{
fn drop(&mut self) {
let _unused = self.move_cursor_to_end_position();
let _unused = self.terminal.cursor_show();
let _unused = self.terminal.flush();
}
}
#[cfg(test)]
mod test {
use std::collections::VecDeque;
use crate::{
error::InquireResult,
terminal::{test::MockTerminal, TerminalSize},
};
use super::FrameRenderer;
#[test]
fn ensure_inline_ansi_codes_are_maintained() -> InquireResult<()> {
let mut output = VecDeque::new();
let terminal = MockTerminal::new(&mut output).with_size(TerminalSize::default());
let mut renderer = FrameRenderer::new(terminal)?;
renderer.start_frame()?;
renderer.write("Hello")?;
renderer.write("World")?;
renderer.write("\n")?;
renderer.write("\x1b[1;31mWhat\x1b[0m is your name?")?;
renderer.finish_current_frame(false)?;
let terminal = &mut renderer.terminal;
terminal.match_text("Hello");
terminal.match_text("World");
terminal.match_text("\r");
terminal.match_text("\n");
terminal.match_text("\x1b[1;31mWhat\x1b[0m is your name?");
Ok(())
}
#[test]
fn cursor_position_multiline_overflow() -> InquireResult<()> {
use super::RenderState;
let mut output = VecDeque::new();
let terminal = MockTerminal::new(&mut output).with_size(TerminalSize::new(10, 10).unwrap());
let mut renderer = FrameRenderer::new(terminal)?;
renderer.start_frame()?;
renderer.write("hello worl")?;
renderer.mark_cursor_position(15);
let expected_position = match &renderer.state {
RenderState::ActiveRender { current_frame, .. } => {
current_frame.expected_cursor_position
}
_ => None,
};
if let Some(pos) = expected_position {
assert_eq!(
pos.row, 2,
"Expected cursor row to be 2 when spanning multiple lines"
);
assert_eq!(pos.col, 5, "Expected cursor col to be 5 after wrapping");
} else {
panic!("Expected cursor position to be set");
}
renderer.mark_cursor_position(20);
let expected_position = match &renderer.state {
RenderState::ActiveRender { current_frame, .. } => {
current_frame.expected_cursor_position
}
_ => None,
};
if let Some(pos) = expected_position {
assert_eq!(pos.row, 3, "Expected cursor row to be 3 for exact multiple");
assert_eq!(pos.col, 0, "Expected cursor col to be 0 for exact multiple");
} else {
panic!("Expected cursor position to be set");
}
renderer.mark_cursor_position(99);
let expected_position = match &renderer.state {
RenderState::ActiveRender { current_frame, .. } => {
current_frame.expected_cursor_position
}
_ => None,
};
if let Some(pos) = expected_position {
assert_eq!(
pos.row, 10,
"Expected cursor row to be 3 for exact multiple"
);
assert_eq!(pos.col, 9, "Expected cursor col to be 0 for exact multiple");
} else {
panic!("Expected cursor position to be set");
}
Ok(())
}
}