use std::cmp::Ordering;
use std::io::{self, Read, Write};
use std::os::unix::io::{AsRawFd, RawFd};
use std::sync;
use std::sync::atomic;
use log::{debug, warn};
use nix::poll::{self, PollFlags};
use nix::sys::signal;
use nix::sys::termios;
use nix::sys::termios::SetArg;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use utf8parse::{Parser, Receiver};
use super::{RawMode, RawReader, Renderer, Term};
use crate::config::{BellStyle, ColorMode, Config, OutputStreamType};
use crate::error;
use crate::highlight::Highlighter;
use crate::keys::{self, KeyPress};
use crate::layout::{Layout, Position};
use crate::line_buffer::LineBuffer;
use crate::Result;
const STDIN_FILENO: RawFd = libc::STDIN_FILENO;
const UNSUPPORTED_TERM: [&str; 3] = ["dumb", "cons25", "emacs"];
const BRACKETED_PASTE_ON: &[u8] = b"\x1b[?2004h";
const BRACKETED_PASTE_OFF: &[u8] = b"\x1b[?2004l";
impl AsRawFd for OutputStreamType {
fn as_raw_fd(&self) -> RawFd {
match self {
OutputStreamType::Stdout => libc::STDOUT_FILENO,
OutputStreamType::Stderr => libc::STDERR_FILENO,
}
}
}
nix::ioctl_read_bad!(win_size, libc::TIOCGWINSZ, libc::winsize);
#[allow(clippy::identity_conversion)]
fn get_win_size<T: AsRawFd + ?Sized>(fileno: &T) -> (usize, usize) {
use std::mem::zeroed;
if cfg!(test) {
return (80, 24);
}
unsafe {
let mut size: libc::winsize = zeroed();
match win_size(fileno.as_raw_fd(), &mut size) {
Ok(0) => (size.ws_col as usize, size.ws_row as usize),
_ => (80, 24),
}
}
}
fn is_unsupported_term() -> bool {
match std::env::var("TERM") {
Ok(term) => {
for iter in &UNSUPPORTED_TERM {
if (*iter).eq_ignore_ascii_case(&term) {
return true;
}
}
false
}
Err(_) => false,
}
}
fn is_a_tty(fd: RawFd) -> bool {
unsafe { libc::isatty(fd) != 0 }
}
pub struct PosixMode {
termios: termios::Termios,
out: Option<OutputStreamType>,
}
#[cfg(not(test))]
pub type Mode = PosixMode;
impl RawMode for PosixMode {
fn disable_raw_mode(&self) -> Result<()> {
termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, &self.termios)?;
if let Some(out) = self.out {
write_and_flush(out, BRACKETED_PASTE_OFF)?;
}
Ok(())
}
}
struct StdinRaw {}
impl Read for StdinRaw {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
loop {
let res = unsafe {
libc::read(
STDIN_FILENO,
buf.as_mut_ptr() as *mut libc::c_void,
buf.len() as libc::size_t,
)
};
if res == -1 {
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted
|| SIGWINCH.load(atomic::Ordering::Relaxed)
{
return Err(error);
}
} else {
#[allow(clippy::cast_sign_loss)]
return Ok(res as usize);
}
}
}
}
pub struct PosixRawReader {
stdin: StdinRaw,
timeout_ms: i32,
buf: [u8; 1],
parser: Parser,
receiver: Utf8,
}
struct Utf8 {
c: Option<char>,
valid: bool,
}
impl PosixRawReader {
fn new(config: &Config) -> Result<Self> {
Ok(Self {
stdin: StdinRaw {},
timeout_ms: config.keyseq_timeout(),
buf: [0; 1],
parser: Parser::new(),
receiver: Utf8 {
c: None,
valid: true,
},
})
}
fn escape_sequence(&mut self) -> Result<KeyPress> {
let seq1 = self.next_char()?;
if seq1 == '[' {
self.escape_csi()
} else if seq1 == 'O' {
self.escape_o()
} else if seq1 == '\x1b' {
Ok(KeyPress::Esc)
} else {
Ok(KeyPress::Meta(seq1))
}
}
fn escape_csi(&mut self) -> Result<KeyPress> {
let seq2 = self.next_char()?;
if seq2.is_digit(10) {
match seq2 {
'0' | '9' => {
debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2);
Ok(KeyPress::UnknownEscSeq)
}
_ => {
self.extended_escape(seq2)
}
}
} else if seq2 == '[' {
let seq3 = self.next_char()?;
Ok(match seq3 {
'A' => KeyPress::F(1),
'B' => KeyPress::F(2),
'C' => KeyPress::F(3),
'D' => KeyPress::F(4),
'E' => KeyPress::F(5),
_ => {
debug!(target: "rustyline", "unsupported esc sequence: ESC [ [ {:?}", seq3);
KeyPress::UnknownEscSeq
}
})
} else {
Ok(match seq2 {
'A' => KeyPress::Up,
'B' => KeyPress::Down,
'C' => KeyPress::Right,
'D' => KeyPress::Left,
'F' => KeyPress::End,
'H' => KeyPress::Home,
'Z' => KeyPress::BackTab,
_ => {
debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2);
KeyPress::UnknownEscSeq
}
})
}
}
#[allow(clippy::cognitive_complexity)]
fn extended_escape(&mut self, seq2: char) -> Result<KeyPress> {
let seq3 = self.next_char()?;
if seq3 == '~' {
Ok(match seq2 {
'1' | '7' => KeyPress::Home,
'2' => KeyPress::Insert,
'3' => KeyPress::Delete,
'4' | '8' => KeyPress::End,
'5' => KeyPress::PageUp,
'6' => KeyPress::PageDown,
_ => {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} ~", seq2);
KeyPress::UnknownEscSeq
}
})
} else if seq3.is_digit(10) {
let seq4 = self.next_char()?;
if seq4 == '~' {
Ok(match (seq2, seq3) {
('1', '1') => KeyPress::F(1),
('1', '2') => KeyPress::F(2),
('1', '3') => KeyPress::F(3),
('1', '4') => KeyPress::F(4),
('1', '5') => KeyPress::F(5),
('1', '7') => KeyPress::F(6),
('1', '8') => KeyPress::F(7),
('1', '9') => KeyPress::F(8),
('2', '0') => KeyPress::F(9),
('2', '1') => KeyPress::F(10),
('2', '3') => KeyPress::F(11),
('2', '4') => KeyPress::F(12),
_ => {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} ~", seq2, seq3);
KeyPress::UnknownEscSeq
}
})
} else if seq4 == ';' {
let seq5 = self.next_char()?;
if seq5.is_digit(10) {
let seq6 = self.next_char()?;
if seq6.is_digit(10) {
self.next_char()?;
} else if seq6 == 'R' {
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} ; {} {}", seq2, seq3, seq5, seq6);
}
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} ; {:?}", seq2, seq3, seq5);
}
Ok(KeyPress::UnknownEscSeq)
} else if seq4.is_digit(10) {
let seq5 = self.next_char()?;
if seq5 == '~' {
Ok(match (seq2, seq3, seq4) {
('2', '0', '0') => KeyPress::BracketedPasteStart,
('2', '0', '1') => KeyPress::BracketedPasteEnd,
_ => {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{}{}~", seq2, seq3, seq4);
KeyPress::UnknownEscSeq
}
})
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{}{} {}", seq2, seq3, seq4, seq5);
Ok(KeyPress::UnknownEscSeq)
}
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} {:?}", seq2, seq3, seq4);
Ok(KeyPress::UnknownEscSeq)
}
} else if seq3 == ';' {
let seq4 = self.next_char()?;
if seq4.is_digit(10) {
let seq5 = self.next_char()?;
if seq5.is_digit(10) {
self.next_char()?;
Ok(KeyPress::UnknownEscSeq)
} else if seq2 == '1' {
Ok(match (seq4, seq5) {
('5', 'A') => KeyPress::ControlUp,
('5', 'B') => KeyPress::ControlDown,
('5', 'C') => KeyPress::ControlRight,
('5', 'D') => KeyPress::ControlLeft,
('2', 'A') => KeyPress::ShiftUp,
('2', 'B') => KeyPress::ShiftDown,
('2', 'C') => KeyPress::ShiftRight,
('2', 'D') => KeyPress::ShiftLeft,
_ => {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ 1 ; {} {:?}", seq4, seq5);
KeyPress::UnknownEscSeq
}
})
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} ; {} {:?}", seq2, seq4, seq5);
Ok(KeyPress::UnknownEscSeq)
}
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} ; {:?}", seq2, seq4);
Ok(KeyPress::UnknownEscSeq)
}
} else {
Ok(match (seq2, seq3) {
('5', 'A') => KeyPress::ControlUp,
('5', 'B') => KeyPress::ControlDown,
('5', 'C') => KeyPress::ControlRight,
('5', 'D') => KeyPress::ControlLeft,
_ => {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} {:?}", seq2, seq3);
KeyPress::UnknownEscSeq
}
})
}
}
fn escape_o(&mut self) -> Result<KeyPress> {
let seq2 = self.next_char()?;
Ok(match seq2 {
'A' => KeyPress::Up,
'B' => KeyPress::Down,
'C' => KeyPress::Right,
'D' => KeyPress::Left,
'F' => KeyPress::End,
'H' => KeyPress::Home,
'P' => KeyPress::F(1),
'Q' => KeyPress::F(2),
'R' => KeyPress::F(3),
'S' => KeyPress::F(4),
'a' => KeyPress::ControlUp,
'b' => KeyPress::ControlDown,
'c' => KeyPress::ControlRight,
'd' => KeyPress::ControlLeft,
_ => {
debug!(target: "rustyline", "unsupported esc sequence: ESC O {:?}", seq2);
KeyPress::UnknownEscSeq
}
})
}
fn poll(&mut self, timeout_ms: i32) -> ::nix::Result<i32> {
let mut fds = [poll::PollFd::new(STDIN_FILENO, PollFlags::POLLIN)];
poll::poll(&mut fds, timeout_ms)
}
}
impl RawReader for PosixRawReader {
fn next_key(&mut self, single_esc_abort: bool) -> Result<KeyPress> {
let c = self.next_char()?;
let mut key = keys::char_to_key_press(c);
if key == KeyPress::Esc {
let timeout_ms = if single_esc_abort && self.timeout_ms == -1 {
0
} else {
self.timeout_ms
};
match self.poll(timeout_ms) {
Ok(n) if n == 0 => {
}
Ok(_) => {
key = self.escape_sequence()?
}
Err(e) => return Err(e.into()),
}
}
debug!(target: "rustyline", "key: {:?}", key);
Ok(key)
}
fn next_char(&mut self) -> Result<char> {
loop {
let n = self.stdin.read(&mut self.buf)?;
if n == 0 {
return Err(error::ReadlineError::Eof);
}
let b = self.buf[0];
self.parser.advance(&mut self.receiver, b);
if !self.receiver.valid {
return Err(error::ReadlineError::Utf8Error);
} else if let Some(c) = self.receiver.c.take() {
return Ok(c);
}
}
}
fn read_pasted_text(&mut self) -> Result<String> {
let mut buffer = String::new();
loop {
match self.next_char()? {
'\x1b' => {
let key = self.escape_sequence()?;
if key == KeyPress::BracketedPasteEnd {
break;
} else {
continue;
}
}
c => buffer.push(c),
};
}
let buffer = buffer.replace("\r\n", "\n");
let buffer = buffer.replace("\r", "\n");
Ok(buffer)
}
}
impl Receiver for Utf8 {
fn codepoint(&mut self, c: char) {
self.c = Some(c);
self.valid = true;
}
fn invalid_sequence(&mut self) {
self.c = None;
self.valid = false;
}
}
pub struct PosixRenderer {
out: OutputStreamType,
cols: usize,
buffer: String,
tab_stop: usize,
colors_enabled: bool,
bell_style: BellStyle,
}
impl PosixRenderer {
fn new(
out: OutputStreamType,
tab_stop: usize,
colors_enabled: bool,
bell_style: BellStyle,
) -> Self {
let (cols, _) = get_win_size(&out);
Self {
out,
cols,
buffer: String::with_capacity(1024),
tab_stop,
colors_enabled,
bell_style,
}
}
}
impl Renderer for PosixRenderer {
type Reader = PosixRawReader;
fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> {
use std::fmt::Write;
self.buffer.clear();
let row_ordering = new.row.cmp(&old.row);
if row_ordering == Ordering::Greater {
let row_shift = new.row - old.row;
if row_shift == 1 {
self.buffer.push_str("\x1b[B");
} else {
write!(self.buffer, "\x1b[{}B", row_shift).unwrap();
}
} else if row_ordering == Ordering::Less {
let row_shift = old.row - new.row;
if row_shift == 1 {
self.buffer.push_str("\x1b[A");
} else {
write!(self.buffer, "\x1b[{}A", row_shift).unwrap();
}
}
let col_ordering = new.col.cmp(&old.col);
if col_ordering == Ordering::Greater {
let col_shift = new.col - old.col;
if col_shift == 1 {
self.buffer.push_str("\x1b[C");
} else {
write!(self.buffer, "\x1b[{}C", col_shift).unwrap();
}
} else if col_ordering == Ordering::Less {
let col_shift = old.col - new.col;
if col_shift == 1 {
self.buffer.push_str("\x1b[D");
} else {
write!(self.buffer, "\x1b[{}D", col_shift).unwrap();
}
}
self.write_and_flush(self.buffer.as_bytes())
}
fn refresh_line(
&mut self,
prompt: &str,
line: &LineBuffer,
hint: Option<&str>,
old_layout: &Layout,
new_layout: &Layout,
highlighter: Option<&dyn Highlighter>,
) -> Result<()> {
use std::fmt::Write;
self.buffer.clear();
let default_prompt = new_layout.default_prompt;
let cursor = new_layout.cursor;
let end_pos = new_layout.end;
let current_row = old_layout.cursor.row;
let old_rows = old_layout.end.row;
let cursor_row_movement = old_rows.saturating_sub(current_row);
if cursor_row_movement > 0 {
write!(self.buffer, "\x1b[{}B", cursor_row_movement).unwrap();
}
for _ in 0..old_rows {
self.buffer.push_str("\r\x1b[0K\x1b[A");
}
self.buffer.push_str("\r\x1b[0K");
if let Some(highlighter) = highlighter {
self.buffer
.push_str(&highlighter.highlight_prompt(prompt, default_prompt));
self.buffer
.push_str(&highlighter.highlight(line, line.pos()));
} else {
self.buffer.push_str(prompt);
self.buffer.push_str(line);
}
if let Some(hint) = hint {
if let Some(highlighter) = highlighter {
self.buffer.push_str(&highlighter.highlight_hint(hint));
} else {
self.buffer.push_str(hint);
}
}
if end_pos.col == 0 && end_pos.row > 0 && !self.buffer.ends_with('\n') {
self.buffer.push_str("\n");
}
let new_cursor_row_movement = end_pos.row - cursor.row;
if new_cursor_row_movement > 0 {
write!(self.buffer, "\x1b[{}A", new_cursor_row_movement).unwrap();
}
if cursor.col > 0 {
write!(self.buffer, "\r\x1b[{}C", cursor.col).unwrap();
} else {
self.buffer.push('\r');
}
self.write_and_flush(self.buffer.as_bytes())?;
Ok(())
}
fn write_and_flush(&self, buf: &[u8]) -> Result<()> {
write_and_flush(self.out, buf)
}
fn calculate_position(&self, s: &str, orig: Position) -> Position {
let mut pos = orig;
let mut esc_seq = 0;
for c in s.graphemes(true) {
if c == "\n" {
pos.row += 1;
pos.col = 0;
continue;
}
let cw = if c == "\t" {
self.tab_stop - (pos.col % self.tab_stop)
} else {
width(c, &mut esc_seq)
};
pos.col += cw;
if pos.col > self.cols {
pos.row += 1;
pos.col = cw;
}
}
if pos.col == self.cols {
pos.col = 0;
pos.row += 1;
}
pos
}
fn beep(&mut self) -> Result<()> {
match self.bell_style {
BellStyle::Audible => {
io::stderr().write_all(b"\x07")?;
io::stderr().flush()?;
Ok(())
}
_ => Ok(()),
}
}
fn clear_screen(&mut self) -> Result<()> {
self.write_and_flush(b"\x1b[H\x1b[2J")
}
fn sigwinch(&self) -> bool {
SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst)
}
fn update_size(&mut self) {
let (cols, _) = get_win_size(&self.out);
self.cols = cols;
}
fn get_columns(&self) -> usize {
self.cols
}
fn get_rows(&self) -> usize {
let (_, rows) = get_win_size(&self.out);
rows
}
fn colors_enabled(&self) -> bool {
self.colors_enabled
}
fn move_cursor_at_leftmost(&mut self, rdr: &mut PosixRawReader) -> Result<()> {
if rdr.poll(0)? != 0 {
debug!(target: "rustyline", "cannot request cursor location");
return Ok(());
}
self.write_and_flush(b"\x1b[6n")?;
if rdr.poll(100)? == 0
|| rdr.next_char()? != '\x1b'
|| rdr.next_char()? != '['
|| read_digits_until(rdr, ';')?.is_none()
{
warn!(target: "rustyline", "cannot read initial cursor location");
return Ok(());
}
let col = read_digits_until(rdr, 'R')?;
debug!(target: "rustyline", "initial cursor location: {:?}", col);
if col.is_some() && col != Some(1) {
self.write_and_flush(b"\n")?;
}
Ok(())
}
}
fn width(s: &str, esc_seq: &mut u8) -> usize {
if *esc_seq == 1 {
if s == "[" {
*esc_seq = 2;
} else {
*esc_seq = 0;
}
0
} else if *esc_seq == 2 {
if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') {
} else {
*esc_seq = 0;
}
0
} else if s == "\x1b" {
*esc_seq = 1;
0
} else if s == "\n" {
0
} else {
s.width()
}
}
fn read_digits_until(rdr: &mut PosixRawReader, sep: char) -> Result<Option<u32>> {
let mut num: u32 = 0;
loop {
match rdr.next_char()? {
digit @ '0'..='9' => {
num = num
.saturating_mul(10)
.saturating_add(digit.to_digit(10).unwrap());
continue;
}
c if c == sep => break,
_ => return Ok(None),
}
}
Ok(Some(num))
}
static SIGWINCH_ONCE: sync::Once = sync::Once::new();
static SIGWINCH: atomic::AtomicBool = atomic::AtomicBool::new(false);
fn install_sigwinch_handler() {
SIGWINCH_ONCE.call_once(|| unsafe {
let sigwinch = signal::SigAction::new(
signal::SigHandler::Handler(sigwinch_handler),
signal::SaFlags::empty(),
signal::SigSet::empty(),
);
let _ = signal::sigaction(signal::SIGWINCH, &sigwinch);
});
}
extern "C" fn sigwinch_handler(_: libc::c_int) {
SIGWINCH.store(true, atomic::Ordering::SeqCst);
debug!(target: "rustyline", "SIGWINCH");
}
#[cfg(not(test))]
pub type Terminal = PosixTerminal;
#[derive(Clone, Debug)]
pub struct PosixTerminal {
unsupported: bool,
stdin_isatty: bool,
stdstream_isatty: bool,
pub(crate) color_mode: ColorMode,
stream_type: OutputStreamType,
tab_stop: usize,
bell_style: BellStyle,
}
impl PosixTerminal {
fn colors_enabled(&self) -> bool {
match self.color_mode {
ColorMode::Enabled => self.stdstream_isatty,
ColorMode::Forced => true,
ColorMode::Disabled => false,
}
}
}
impl Term for PosixTerminal {
type Mode = PosixMode;
type Reader = PosixRawReader;
type Writer = PosixRenderer;
fn new(
color_mode: ColorMode,
stream_type: OutputStreamType,
tab_stop: usize,
bell_style: BellStyle,
) -> Self {
let term = Self {
unsupported: is_unsupported_term(),
stdin_isatty: is_a_tty(STDIN_FILENO),
stdstream_isatty: is_a_tty(stream_type.as_raw_fd()),
color_mode,
stream_type,
tab_stop,
bell_style,
};
if !term.unsupported && term.stdin_isatty && term.stdstream_isatty {
install_sigwinch_handler();
}
term
}
fn is_unsupported(&self) -> bool {
self.unsupported
}
fn is_stdin_tty(&self) -> bool {
self.stdin_isatty
}
fn is_output_tty(&self) -> bool {
self.stdstream_isatty
}
fn enable_raw_mode(&mut self) -> Result<Self::Mode> {
use nix::errno::Errno::ENOTTY;
use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags, SpecialCharacterIndices};
if !self.stdin_isatty {
return Err(nix::Error::from_errno(ENOTTY).into());
}
let original_mode = termios::tcgetattr(STDIN_FILENO)?;
let mut raw = original_mode.clone();
raw.input_flags &= !(InputFlags::BRKINT
| InputFlags::ICRNL
| InputFlags::INPCK
| InputFlags::ISTRIP
| InputFlags::IXON);
raw.control_flags |= ControlFlags::CS8;
raw.local_flags &=
!(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG);
raw.control_chars[SpecialCharacterIndices::VMIN as usize] = 1;
raw.control_chars[SpecialCharacterIndices::VTIME as usize] = 0;
termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, &raw)?;
let out = if let Err(e) = write_and_flush(self.stream_type, BRACKETED_PASTE_ON) {
debug!(target: "rustyline", "Cannot enable bracketed paste: {}", e);
None
} else {
Some(self.stream_type)
};
Ok(PosixMode {
termios: original_mode,
out,
})
}
fn create_reader(&self, config: &Config) -> Result<PosixRawReader> {
PosixRawReader::new(config)
}
fn create_writer(&self) -> PosixRenderer {
PosixRenderer::new(
self.stream_type,
self.tab_stop,
self.colors_enabled(),
self.bell_style,
)
}
}
#[cfg(not(test))]
pub fn suspend() -> Result<()> {
use nix::unistd::Pid;
signal::kill(Pid::from_raw(0), signal::SIGTSTP)?;
Ok(())
}
fn write_and_flush(out: OutputStreamType, buf: &[u8]) -> Result<()> {
match out {
OutputStreamType::Stdout => {
io::stdout().write_all(buf)?;
io::stdout().flush()?;
}
OutputStreamType::Stderr => {
io::stderr().write_all(buf)?;
io::stderr().flush()?;
}
}
Ok(())
}
#[cfg(test)]
mod test {
use super::{Position, PosixRenderer, PosixTerminal, Renderer};
use crate::config::{BellStyle, OutputStreamType};
use crate::line_buffer::LineBuffer;
#[test]
#[ignore]
fn prompt_with_ansi_escape_codes() {
let out = PosixRenderer::new(OutputStreamType::Stdout, 4, true, BellStyle::default());
let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default());
assert_eq!(3, pos.col);
assert_eq!(0, pos.row);
}
#[test]
fn test_unsupported_term() {
::std::env::set_var("TERM", "xterm");
assert_eq!(false, super::is_unsupported_term());
::std::env::set_var("TERM", "dumb");
assert_eq!(true, super::is_unsupported_term());
}
#[test]
fn test_send() {
fn assert_send<T: Send>() {}
assert_send::<PosixTerminal>();
}
#[test]
fn test_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<PosixTerminal>();
}
#[test]
fn test_line_wrap() {
let mut out = PosixRenderer::new(OutputStreamType::Stdout, 4, true, BellStyle::default());
let prompt = "> ";
let default_prompt = true;
let prompt_size = out.calculate_position(prompt, Position::default());
let mut line = LineBuffer::init("", 0, None);
let old_layout = out.compute_layout(prompt_size, default_prompt, &line, None);
assert_eq!(Position { col: 2, row: 0 }, old_layout.cursor);
assert_eq!(old_layout.cursor, old_layout.end);
assert_eq!(Some(true), line.insert('a', out.cols - prompt_size.col + 1));
let new_layout = out.compute_layout(prompt_size, default_prompt, &line, None);
assert_eq!(Position { col: 1, row: 1 }, new_layout.cursor);
assert_eq!(new_layout.cursor, new_layout.end);
out.refresh_line(prompt, &line, None, &old_layout, &new_layout, None)
.unwrap();
#[rustfmt::skip]
assert_eq!(
"\r\u{1b}[0K> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\u{1b}[1C",
out.buffer
);
}
}