Update fuchsia tty to work with latest
Change-Id: I930c7ec67ad87d7dee13f30fefdd673d2216aadf
diff --git a/src/tty/fuchsia.rs b/src/tty/fuchsia.rs
index a63723b..febb577 100644
--- a/src/tty/fuchsia.rs
+++ b/src/tty/fuchsia.rs
@@ -3,17 +3,22 @@
use std::mem;
use std::sync::atomic;
use libc;
-use char_iter;
+use unicode_segmentation::UnicodeSegmentation;
+use utf8parse::{Parser, Receiver};
-use config::Config;
-use consts::{self, KeyPress};
+use config::{ColorMode, Config, OutputStreamType};
+use highlight::Highlighter;
+use keys::{self, KeyPress};
+use line_buffer::LineBuffer;
use error;
use Result;
-use super::{RawMode, RawReader, Term};
+use super::{truncate, width, Position, RawMode, RawReader, Renderer, Term};
use fuchsia_device::pty;
+use StdStream;
-const STDIN_FILENO: usize = 0;
-const STDOUT_FILENO: usize = 1;
+const STDIN_FILENO: libc::c_int = libc::STDIN_FILENO;
+const STDOUT_FILENO: libc::c_int = libc::STDOUT_FILENO;
+const STDERR_FILENO: libc::c_int = libc::STDERR_FILENO;
fn get_win_size() -> (usize, usize) {
match pty::get_window_size() {
@@ -60,185 +65,522 @@
}
}
-pub type Terminal = Console;
+pub type Terminal = FuchsiaTerminal;
#[derive(Clone, Debug)]
-pub struct Console {
+pub struct FuchsiaTerminal {
+ unsupported: bool,
stdin_isatty: bool,
+ stdstream_isatty: bool,
+ pub(crate) color_mode: ColorMode,
+ stream_type: OutputStreamType,
+}
+
+struct Utf8 {
+ c: Option<char>,
+ valid: bool,
}
pub struct FuchsiaRawReader {
- chars: char_iter::Chars<StdinRaw>,
+ stdin: StdinRaw,
+ timeout_ms: i32,
+ buf: [u8; 1],
+ parser: Parser,
+ receiver: Utf8,
}
impl FuchsiaRawReader {
- pub fn new() -> Result<FuchsiaRawReader> {
- let stdin = StdinRaw {};
+ pub fn new(config: &Config) -> Result<FuchsiaRawReader> {
Ok(FuchsiaRawReader {
- chars: char_iter::chars(stdin),
+ stdin: StdinRaw {},
+ timeout_ms: config.keyseq_timeout(),
+ buf: [0; 1],
+ parser: Parser::new(),
+ receiver: Utf8 {
+ c: None,
+ valid: true,
+ }
})
}
+
+ /// Handle ESC <seq1> sequences
fn escape_sequence(&mut self) -> Result<KeyPress> {
- // Read the next two bytes representing the escape sequence.
+ // Read the next byte representing the escape sequence.
let seq1 = try!(self.next_char());
if seq1 == '[' {
- // ESC [ sequences.
- let seq2 = try!(self.next_char());
- if seq2.is_digit(10) {
- // Extended escape, read additional byte.
- let seq3 = try!(self.next_char());
- if seq3 == '~' {
- Ok(match seq2 {
- '1' | '7' => KeyPress::Home, // '1': xterm
- '3' => KeyPress::Delete,
- '4' | '8' => KeyPress::End, // '4': xterm
- '5' => KeyPress::PageUp,
- '6' => KeyPress::PageDown,
- _ => {
- debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}{:?}", seq1, seq2, seq3);
- KeyPress::UnknownEscSeq
- }
- })
- } else {
- debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}{:?}", seq1, seq2, seq3);
+ // ESC [ sequences. (CSI)
+ 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)
}
- } else {
- Ok(match seq2 {
- 'A' => KeyPress::Up, // ANSI
- 'B' => KeyPress::Down,
- 'C' => KeyPress::Right,
- 'D' => KeyPress::Left,
- 'F' => KeyPress::End,
- 'H' => KeyPress::Home,
- _ => {
- debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}", seq1, seq2);
- KeyPress::UnknownEscSeq
- }
- })
- }
- } else if seq1 == 'O' {
- // ESC O sequences.
- let seq2 = try!(self.next_char());
- Ok(match seq2 {
- 'A' => KeyPress::Up,
- 'B' => KeyPress::Down,
- 'C' => KeyPress::Right,
- 'D' => KeyPress::Left,
- 'F' => KeyPress::End,
- 'H' => KeyPress::Home,
_ => {
- debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}", seq1, seq2);
+ // Extended escape, read additional byte.
+ self.extended_escape(seq2)
+ }
+ }
+ } else if seq2 == '[' {
+ let seq3 = try!(self.next_char());
+ // Linux console
+ 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 {
- // TODO ESC-R (r): Undo all changes made to this line.
- Ok(match seq1 {
- '\x08' => KeyPress::Meta('\x08'), // Backspace
- '-' => KeyPress::Meta('-'),
- '0'...'9' => KeyPress::Meta(seq1),
- '<' => KeyPress::Meta('<'),
- '>' => KeyPress::Meta('>'),
- 'b' | 'B' => KeyPress::Meta('B'),
- 'c' | 'C' => KeyPress::Meta('C'),
- 'd' | 'D' => KeyPress::Meta('D'),
- 'f' | 'F' => KeyPress::Meta('F'),
- 'l' | 'L' => KeyPress::Meta('L'),
- 'n' | 'N' => KeyPress::Meta('N'),
- 'p' | 'P' => KeyPress::Meta('P'),
- 't' | 'T' => KeyPress::Meta('T'),
- 'u' | 'U' => KeyPress::Meta('U'),
- 'y' | 'Y' => KeyPress::Meta('Y'),
- '\x7f' => KeyPress::Meta('\x7f'), // Delete
+ // 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
+ 'Z' => KeyPress::BackTab,
_ => {
- debug!(target: "rustyline", "unsupported esc sequence: M-{:?}", seq1);
+ 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
+ _ => {
+ debug!(target: "rustyline",
+ "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);
+ } else {
+ 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,
+ _ => {
+ 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
+ }
+ })
+ }
+ }
+
+ /// 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, // rxvt
+ 'd' => KeyPress::ControlLeft, // rxvt
+ _ => {
+ debug!(target: "rustyline", "unsupported esc sequence: ESC O {:?}", seq2);
+ KeyPress::UnknownEscSeq
+ }
+ })
+ }
}
-// TODO properly set raw mode, process escape keys
+pub struct FuchsiaRenderer {
+ out: StdStream,
+ cols: usize,
+ buffer: String,
+}
+
+impl FuchsiaRenderer {
+ pub fn new(stream_type: OutputStreamType) -> FuchsiaRenderer {
+ let out = StdStream::from_stream_type(stream_type);
+ let (cols, _) = get_win_size();
+ FuchsiaRenderer {
+ out,
+ cols,
+ buffer: String::with_capacity(1024),
+ }
+ }
+}
+
+impl Renderer for FuchsiaRenderer {
+ 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())
+ }
+
+ /// Display `prompt`, line and cursor in terminal output
+ fn refresh_line(
+ &mut self,
+ prompt: &str,
+ prompt_size: Position,
+ line: &LineBuffer,
+ hint: Option<String>,
+ current_row: usize,
+ old_rows: usize,
+ highlighter: Option<&Highlighter>,
+ ) -> Result<(Position, Position)> {
+ use std::fmt::Write;
+ self.buffer.clear();
+
+ // 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!(self.buffer, "\x1b[{}B", cursor_row_movement).unwrap();
+ }
+ // clear old rows
+ for _ in 0..old_rows {
+ self.buffer.push_str("\r\x1b[0K\x1b[A");
+ }
+ // clear the line
+ self.buffer.push_str("\r\x1b[0K");
+
+ if let Some(highlighter) = highlighter {
+ // display the prompt
+ self.buffer.push_str(&highlighter.highlight_prompt(prompt));
+ // display the input line
+ self.buffer
+ .push_str(&highlighter.highlight(line, line.pos()));
+ } else {
+ // display the prompt
+ self.buffer.push_str(prompt);
+ // display the input line
+ self.buffer.push_str(line);
+ }
+ // display hint
+ if let Some(hint) = hint {
+ let truncate = truncate(&hint, end_pos.col, self.cols);
+ if let Some(highlighter) = highlighter {
+ self.buffer.push_str(&highlighter.highlight_hint(truncate));
+ } else {
+ self.buffer.push_str(truncate);
+ }
+ }
+ // we have to generate our own newline on line wrap
+ if end_pos.col == 0 && end_pos.row > 0 {
+ self.buffer.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!(self.buffer, "\x1b[{}A", cursor_row_movement).unwrap();
+ }
+ // position the cursor within the line
+ if cursor.col > 0 {
+ write!(self.buffer, "\r\x1b[{}C", cursor.col).unwrap();
+ } else {
+ self.buffer.push('\r');
+ }
+
+ try!(self.out.write_all(self.buffer.as_bytes()));
+ try!(self.out.flush());
+ Ok((cursor, end_pos))
+ }
+
+ /// Calculate the number of columns and rows used to display `s` on a
+ /// `cols` width terminal starting at `orig`.
+ 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 = 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 write_and_flush(&mut self, buf: &[u8]) -> Result<()> {
+ try!(self.out.write_all(buf));
+ try!(self.out.flush());
+ Ok(())
+ }
+
+ /// 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 {
+ false
+ }
+
+ /// Update the number of columns/rows in the current terminal.
+ fn update_size(&mut self) {
+ let (cols, _) = get_win_size();
+ self.cols = cols;
+ }
+
+ /// Get the number of columns in the current terminal.
+ fn get_columns(&self) -> usize {
+ let (cols, _) = get_win_size();
+ cols
+ }
+
+ /// Get the number of rows in the current terminal.
+ fn get_rows(&self) -> usize {
+ let (_, rows) = get_win_size();
+ rows
+ }
+}
+
+
+// TODO properly set raw mode, handle escape key timeouts
impl RawReader for FuchsiaRawReader {
- fn next_key(&mut self) -> Result<KeyPress> {
+ 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 mut key = keys::char_to_key_press(c);
if key == KeyPress::Esc {
- // TODO
- debug!(target: "rustyline", "ESC + {:?} currently unsupported", key);
+ return self.escape_sequence();
}
Ok(key)
}
fn next_char(&mut self) -> Result<char> {
- match self.chars.next() {
- Some(ch) => Ok(ch?),
- None => Err(error::ReadlineError::Eof),
+ 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 Term for Console {
- type Reader = FuchsiaRawReader;
- type Writer = Stdout;
- type Mode = Mode;
-
- fn new() -> Console {
- let stdin_isatty = true;
- Console {
- stdin_isatty: stdin_isatty,
- }
+impl Receiver for Utf8 {
+ /// Called whenever a codepoint is parsed successfully
+ fn codepoint(&mut self, c: char) {
+ self.c = Some(c);
+ self.valid = true;
}
- /// Checking for an unsupported TERM in fuchsia is a no-op
+ /// Called when an invalid_sequence is detected
+ fn invalid_sequence(&mut self) {
+ self.c = None;
+ self.valid = false;
+ }
+}
+
+fn is_a_tty(fd: libc::c_int) -> bool {
+ unsafe { libc::isatty(fd) != 0 }
+}
+
+impl Term for FuchsiaTerminal {
+ type Reader = FuchsiaRawReader;
+ type Writer = FuchsiaRenderer;
+ type Mode = Mode;
+
+ fn new(color_mode: ColorMode, stream_type: OutputStreamType) -> FuchsiaTerminal {
+ FuchsiaTerminal {
+ unsupported: false,
+ stdin_isatty: is_a_tty(STDIN_FILENO as i32),
+ stdstream_isatty: is_a_tty(if stream_type == OutputStreamType::Stdout {
+ STDOUT_FILENO as i32
+ } else {
+ STDERR_FILENO as i32
+ }),
+ color_mode,
+ stream_type,
+ }
+ }
+ /// Check if current terminal can provide a rich line-editing user
+ /// interface.
fn is_unsupported(&self) -> bool {
false
}
-
+ /// check if stdin is connected to a terminal.
fn is_stdin_tty(&self) -> bool {
self.stdin_isatty
}
-
- /// Try to get the number of columns in the current terminal,
- /// or assume 80 if it fails.
- fn get_columns(&self) -> usize {
- let (cols, _) = get_win_size();
- cols
+ /// Check if output supports colors.
+ fn colors_enabled(&self) -> bool {
+ true
}
-
- /// 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
- }
-
- /// Enable RAW mode for the terminal. No termios support, so this fakes it
- fn enable_raw_mode(&self) -> Result<Mode> {
+ /// Enable RAW mode for the terminal.
+ fn enable_raw_mode(&mut self) -> Result<Self::Mode> {
Ok(Mode {})
}
-
- fn create_reader(&self, _: &Config) -> Result<FuchsiaRawReader> {
- FuchsiaRawReader::new()
+ /// Create a RAW reader
+ fn create_reader(&self, config: &Config) -> Result<Self::Reader> {
+ FuchsiaRawReader::new(config)
}
-
- fn create_writer(&self) -> Stdout {
- io::stdout()
- }
-
- fn sigwinch(&self) -> bool {
- false
- }
-
- /// Clear the screen. Used to handle ctrl+l
- fn clear_screen(&mut self, w: &mut Write) -> Result<()> {
- try!(w.write_all(b"\x1b[H\x1b[2J"));
- try!(w.flush());
- Ok(())
+ /// Create a writer
+ fn create_writer(&self) -> Self::Writer {
+ FuchsiaRenderer::new(self.stream_type)
}
}