Newer
Older
use nix::sys::termios;
use super::{truncate, width, Position, RawMode, RawReader, Renderer, Term};
use consts::{self, KeyPress};
const STDIN_FILENO: libc::c_int = libc::STDIN_FILENO;
const STDOUT_FILENO: libc::c_int = libc::STDOUT_FILENO;
/// Unsupported Terminals that don't support RAW mode
static UNSUPPORTED_TERM: [&'static str; 3] = ["dumb", "cons25", "emacs"];
use std::mem::zeroed;
unsafe {
match libc::ioctl(STDOUT_FILENO, libc::TIOCGWINSZ.into(), &mut size) {
// .into() for FreeBSD
0 => (size.ws_col as usize, size.ws_row as usize), // TODO getCursorPosition
_ => (80, 24),
}
}
}
/// Check TERM environment variable to see if current term is in our
/// unsupported list
fn is_unsupported_term() -> bool {
match std::env::var("TERM") {
Ok(term) => {
for iter in &UNSUPPORTED_TERM {
}
Err(_) => false,
}
}
/// Return whether or not STDIN, STDOUT or STDERR is a TTY
fn is_a_tty(fd: libc::c_int) -> bool {
unsafe { libc::isatty(fd) != 0 }
}
impl RawMode for Mode {
/// Disable RAW mode for the terminal.
fn disable_raw_mode(&self) -> Result<()> {
try!(termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, self));
// Rust std::io::Stdin is buffered with no way to know if bytes are available.
// So we use low-level stuff instead...
struct StdinRaw {}
impl Read for StdinRaw {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
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 {
return Ok(res as usize);
}
fn new(config: &Config) -> Result<PosixRawReader> {
fn escape_sequence(&mut self) -> Result<KeyPress> {
// Read the next byte representing the escape sequence.
let seq1 = try!(self.next_char());
if seq1 == '[' {
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
self.escape_csi()
} else if seq1 == 'O' {
// xterm
// ESC O sequences. (SS3)
self.escape_o()
} else if seq1 == '\x1b' {
// ESC ESC
Ok(KeyPress::Esc)
} else {
// TODO ESC-R (r): Undo all changes made to this line.
Ok(KeyPress::Meta(seq1))
}
}
/// Handle ESC [ <seq2> escape sequences
fn escape_csi(&mut self) -> Result<KeyPress> {
let seq2 = try!(self.next_char());
if seq2.is_digit(10) {
match seq2 {
'0' | '9' => {
debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2);
Ok(KeyPress::UnknownEscSeq)
}
_ => {
// Extended escape, read additional byte.
self.extended_escape(seq2)
}
}
} else {
// ANSI
Ok(match seq2 {
'A' => KeyPress::Up, // kcuu1
'B' => KeyPress::Down, // kcud1
'C' => KeyPress::Right, // kcuf1
'D' => KeyPress::Left, // kcub1
'F' => KeyPress::End,
'H' => KeyPress::Home, // khome
_ => {
debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2);
KeyPress::UnknownEscSeq
}
})
}
}
/// Handle ESC [ <seq2:digit> escape sequences
fn extended_escape(&mut self, seq2: char) -> Result<KeyPress> {
let seq3 = try!(self.next_char());
if seq3 == '~' {
Ok(match seq2 {
'1' | '7' => KeyPress::Home, // tmux, xrvt
'2' => KeyPress::Insert,
'3' => KeyPress::Delete, // kdch1
'4' | '8' => KeyPress::End, // tmux, xrvt
'5' => KeyPress::PageUp, // kpp
'6' => KeyPress::PageDown, // knp
_ => {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} ~", seq2);
KeyPress::UnknownEscSeq
}
})
} else if seq3.is_digit(10) {
let seq4 = try!(self.next_char());
if seq4 == '~' {
Ok(match (seq2, seq3) {
('1', '1') => KeyPress::F(1), // rxvt-unicode
('1', '2') => KeyPress::F(2), // rxvt-unicode
('1', '3') => KeyPress::F(3), // rxvt-unicode
('1', '4') => KeyPress::F(4), // rxvt-unicode
('1', '5') => KeyPress::F(5), // kf5
('1', '7') => KeyPress::F(6), // kf6
('1', '8') => KeyPress::F(7), // kf7
('1', '9') => KeyPress::F(8), // kf8
('2', '0') => KeyPress::F(9), // kf9
('2', '1') => KeyPress::F(10), // kf10
('2', '3') => KeyPress::F(11), // kf11
('2', '4') => KeyPress::F(12), // kf12
_ => {
"unsupported esc sequence: ESC [ {}{} ~", seq2, seq3);
KeyPress::UnknownEscSeq
})
} else if seq4 == ';' {
let seq5 = try!(self.next_char());
if seq5.is_digit(10) {
let seq6 = try!(self.next_char()); // '~' expected
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} ; {} {}", seq2, seq3, seq5, seq6);
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} ; {:?}", seq2, seq3, seq5);
}
Ok(KeyPress::UnknownEscSeq)
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} {:?}", seq2, seq3, seq4);
Ok(KeyPress::UnknownEscSeq)
}
} else if seq3 == ';' {
let seq4 = try!(self.next_char());
if seq4.is_digit(10) {
let seq5 = try!(self.next_char());
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,
"unsupported esc sequence: ESC [ {} ; {} {}", seq2, seq4, seq5);
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} ; {} {}", seq2, seq4, seq5);
Ok(KeyPress::UnknownEscSeq)
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);
/// Handle ESC O <seq2> escape sequences
fn escape_o(&mut self) -> Result<KeyPress> {
let seq2 = try!(self.next_char());
Ok(match seq2 {
'A' => KeyPress::Up, // kcuu1
'B' => KeyPress::Down, // kcud1
'C' => KeyPress::Right, // kcuf1
'D' => KeyPress::Left, // kcub1
'F' => KeyPress::End, // kend
'H' => KeyPress::Home, // khome
'P' => KeyPress::F(1), // kf1
'Q' => KeyPress::F(2), // kf2
'R' => KeyPress::F(3), // kf3
'S' => KeyPress::F(4), // kf4
'a' => KeyPress::ControlUp,
'b' => KeyPress::ControlDown,
'c' => KeyPress::ControlRight,
'd' => KeyPress::ControlLeft,
_ => {
debug!(target: "rustyline", "unsupported esc sequence: ESC O {:?}", seq2);
KeyPress::UnknownEscSeq
}
})
}
static UTF8_CHAR_WIDTH: [u8; 256] = [
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x1F
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x3F
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x5F
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x7F
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0x9F
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0xBF
0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // 0xDF
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, // 0xEF
4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0, // 0xFF
];
fn next_key(&mut self, single_esc_abort: bool) -> Result<KeyPress> {
let c = try!(self.next_char());
let mut key = consts::char_to_key_press(c);
let timeout_ms = if single_esc_abort && self.timeout_ms == -1 {
0
} else {
self.timeout_ms
};
let mut fds = [poll::PollFd::new(STDIN_FILENO, EventFlags::POLLIN)];
Ok(n) if n == 0 => {
// single escape
}
Ok(_) => {
// escape sequence
key = try!(self.escape_sequence())
}
// Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
Ok(key)
}
fn next_char(&mut self) -> Result<char> {
let n = try!(self.stdin.read(&mut self.buf[..1]));
if n == 0 {
return Err(error::ReadlineError::Eof);
}
let first = self.buf[0];
if first >= 128 {
let width = UTF8_CHAR_WIDTH[first as usize] as usize;
if width == 0 {
try!(std::str::from_utf8(&self.buf[..1]));
unreachable!()
}
try!(self.stdin.read_exact(&mut self.buf[1..width]));
let s = try!(std::str::from_utf8(&self.buf[..width]));
Ok(s.chars().next().unwrap())
} else {
/// Console output writer
pub struct PosixRenderer {
out: Stdout,
cols: usize, // Number of columns in terminal
}
impl PosixRenderer {
fn new() -> PosixRenderer {
let (cols, _) = get_win_size();
PosixRenderer {
out: io::stdout(),
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
}
}
}
impl Renderer for PosixRenderer {
fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> {
use std::fmt::Write;
let mut ab = String::new();
if new.row > old.row {
// move down
let row_shift = new.row - old.row;
if row_shift == 1 {
ab.push_str("\x1b[B");
} else {
write!(ab, "\x1b[{}B", row_shift).unwrap();
}
} else if new.row < old.row {
// move up
let row_shift = old.row - new.row;
if row_shift == 1 {
ab.push_str("\x1b[A");
} else {
write!(ab, "\x1b[{}A", row_shift).unwrap();
}
}
if new.col > old.col {
// move right
let col_shift = new.col - old.col;
if col_shift == 1 {
ab.push_str("\x1b[C");
} else {
write!(ab, "\x1b[{}C", col_shift).unwrap();
}
} else if new.col < old.col {
// move left
let col_shift = old.col - new.col;
if col_shift == 1 {
ab.push_str("\x1b[D");
} else {
write!(ab, "\x1b[{}D", col_shift).unwrap();
}
}
self.write_and_flush(ab.as_bytes())
}
fn refresh_line(
&mut self,
prompt: &str,
prompt_size: Position,
line: &LineBuffer,
current_row: usize,
old_rows: usize,
) -> Result<(Position, Position)> {
use std::fmt::Write;
let mut ab = String::new();
// calculate the position of the end of the input line
let end_pos = self.calculate_position(line, prompt_size);
// calculate the desired position of the cursor
let cursor = self.calculate_position(&line[..line.pos()], prompt_size);
// self.old_rows < self.cursor.row if the prompt spans multiple lines and if
// this is the default State.
let cursor_row_movement = old_rows.checked_sub(current_row).unwrap_or(0);
// move the cursor down as required
if cursor_row_movement > 0 {
write!(ab, "\x1b[{}B", cursor_row_movement).unwrap();
}
// clear old rows
for _ in 0..old_rows {
}
// clear the line
ab.push_str("\r\x1b[0K");
// display the prompt
ab.push_str(prompt);
// display the input line
ab.push_str(line);
// display hint
if let Some(hint) = hint {
ab.push_str(truncate(&hint, end_pos.col, self.cols));
}
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
// we have to generate our own newline on line wrap
if end_pos.col == 0 && end_pos.row > 0 {
ab.push_str("\n");
}
// position the cursor
let cursor_row_movement = end_pos.row - cursor.row;
// move the cursor up as required
if cursor_row_movement > 0 {
write!(ab, "\x1b[{}A", cursor_row_movement).unwrap();
}
// position the cursor within the line
if cursor.col > 0 {
write!(ab, "\r\x1b[{}C", cursor.col).unwrap();
} else {
ab.push('\r');
}
try!(self.write_and_flush(ab.as_bytes()));
Ok((cursor, end_pos))
}
fn write_and_flush(&mut self, buf: &[u8]) -> Result<()> {
try!(self.out.write_all(buf));
try!(self.out.flush());
Ok(())
}
/// Control characters are treated as having zero width.
/// Characters with 2 column width are correctly handled (not splitted).
#[allow(if_same_then_else)]
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;
continue;
}
let cw = width(c, &mut esc_seq);
pos.col += cw;
if pos.col > self.cols {
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
}
}
if pos.col == self.cols {
pos.col = 0;
pos.row += 1;
}
pos
}
/// Clear the screen. Used to handle ctrl+l
fn clear_screen(&mut self) -> Result<()> {
self.write_and_flush(b"\x1b[H\x1b[2J")
}
/// Check if a SIGWINCH signal has been received
fn sigwinch(&self) -> bool {
SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst)
}
/// Try to update the number of columns in the current terminal,
fn update_size(&mut self) {
let (cols, _) = get_win_size();
self.cols = cols;
}
fn get_columns(&self) -> usize {
self.cols
}
/// Try to get the number of rows in the current terminal,
/// or assume 24 if it fails.
fn get_rows(&self) -> usize {
let (_, rows) = get_win_size();
rows
}
}
static SIGWINCH_ONCE: sync::Once = sync::ONCE_INIT;
static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT;
let sigwinch = signal::SigAction::new(
signal::SigHandler::Handler(sigwinch_handler),
signal::SaFlags::empty(),
signal::SigSet::empty(),
);
let _ = signal::sigaction(signal::SIGWINCH, &sigwinch);
});
}
SIGWINCH.store(true, atomic::Ordering::SeqCst);
pub type Terminal = PosixTerminal;
pub struct PosixTerminal {
unsupported: bool,
stdin_isatty: bool,
unsupported: is_unsupported_term(),
stdin_isatty: is_a_tty(STDIN_FILENO),
stdout_isatty: is_a_tty(STDOUT_FILENO),
color_mode,
if !term.unsupported && term.stdin_isatty && term.stdout_isatty {
install_sigwinch_handler();
}
/// Check if current terminal can provide a rich line-editing user
/// interface.
self.unsupported
}
/// check if stdin is connected to a terminal.
/// Check if output supports colors.
fn colors_enabled(&self) -> bool {
match self.color_mode {
ColorMode::Enabled => self.stdout_isatty,
ColorMode::Forced => true,
ColorMode::Disabled => false,
}
}
use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags, SpecialCharacterIndices};
if !self.stdin_isatty {
try!(Err(nix::Error::from_errno(ENOTTY)));
}
let original_mode = try!(termios::tcgetattr(STDIN_FILENO));
// disable BREAK interrupt, CR to NL conversion on input,
// input parity check, strip high bit (bit 8), output flow control
raw.input_flags &= !(InputFlags::BRKINT
| InputFlags::ICRNL
| InputFlags::INPCK
| InputFlags::ISTRIP
| InputFlags::IXON);
// we don't want raw output, it turns newlines into straight linefeeds
// raw.c_oflag = raw.c_oflag & !(OutputFlags::OPOST); // disable all output
// processing
// character-size mark (8 bits)
raw.control_flags |= ControlFlags::CS8;
// disable echoing, canonical mode, extended input processing and signals
raw.local_flags &=
!(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG);
raw.control_chars[SpecialCharacterIndices::VMIN as usize] = 1; // One character-at-a-time input
raw.control_chars[SpecialCharacterIndices::VTIME as usize] = 0; // with blocking read
try!(termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, &raw));
fn create_reader(&self, config: &Config) -> Result<PosixRawReader> {
PosixRawReader::new(config)
fn create_writer(&self) -> PosixRenderer {
PosixRenderer::new()
#[test]
fn prompt_with_ansi_escape_codes() {
let out = io::stdout();
let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default(), 80);
assert_eq!(3, pos.col);
assert_eq!(0, pos.row);
}