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 {
Mingshen Sun
committed
// FIXME: ".into()" used as a temporary fix for a libc bug
match libc::ioctl(STDOUT_FILENO, libc::TIOCGWINSZ.into(), &mut size) {
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);
}
buf: [u8; 1],
parser: Parser,
receiver: Utf8,
}
struct Utf8 {
c: Option<char>,
valid: bool,
fn new(config: &Config) -> Result<PosixRawReader> {
buf: [0; 1],
parser: Parser::new(),
receiver: Utf8 {
c: None,
valid: true,
},
fn escape_sequence(&mut self) -> Result<KeyPress> {
// Read the next byte representing the escape sequence.
let seq1 = try!(self.next_char());
if seq1 == '[' {
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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
}
})
}
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> {
loop {
let n = try!(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 self.receiver.c.is_some() {
return Ok(self.receiver.c.take().unwrap());
impl Receiver for Utf8 {
/// Called whenever a codepoint is parsed successfully
fn codepoint(&mut self, c: char) {
self.c = Some(c);
self.valid = true;
}
/// Called when an invalid_sequence is detected
fn invalid_sequence(&mut self) {
self.c = None;
self.valid = false;
}
}
/// 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(),
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
420
421
422
423
}
}
}
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,
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");
if let Some(highlighter) = highlighter {
// display the prompt
ab.push_str(&highlighter.highlight_prompt(prompt));
// display the input line
ab.push_str(&highlighter.highlight(line, line.pos()));
} else {
// display the prompt
ab.push_str(prompt);
// display the input line
ab.push_str(line);
}
let truncate = truncate(&hint, end_pos.col, self.cols);
if let Some(highlighter) = highlighter {
ab.push_str(&highlighter.highlight_hint(truncate));
} else {
ab.push_str(truncate);
}
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
// 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).
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 {
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
}
}
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);
}