use crate::error::{Error, Result}; use crate::highlight::Highlighting; use crate::input::{InputSeq, KeySeq}; use crate::row::Row; use crate::signal::SigwinchWatcher; use crate::status_bar::StatusBar; use crate::term_color::{Color, TermColor}; use crate::text_buffer::TextBuffer; use std::cmp; use std::io::Write; use std::time::SystemTime; use unicode_width::UnicodeWidthChar; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const HELP: &str = "\ Ctrl-Q : Quit Ctrl-S : Save to file Ctrl-O : Open text buffer Ctrl-X : Next text buffer Alt-X : Previous text buffer Ctrl-P or UP : Move cursor up Ctrl-N or DOWN : Move cursor down Ctrl-F or RIGHT : Move cursor right Ctrl-B or LEFT : Move cursor left Ctrl-A or Alt-LEFT or HOME : Move cursor to head of line Ctrl-E or Alt-RIGHT or END : Move cursor to end of line Ctrl-[ or Ctrl-V or PAGE DOWN : Next page Ctrl-] or Alt-V or PAGE UP : Previous page Alt-F or Ctrl-RIGHT : Move cursor to next word Alt-B or Ctrl-LEFT : Move cursor to previous word Alt-N or Ctrl-DOWN : Move cursor to next paragraph Alt-P or Ctrl-UP : Move cursor to previous paragraph Alt-< : Move cursor to top of file Alt-> : Move cursor to bottom of file Ctrl-H or BACKSPACE : Delete character Ctrl-D or DELETE : Delete next character Ctrl-W : Delete a word Ctrl-J : Delete until head of line Ctrl-K : Delete until end of line Ctrl-U : Undo last change Ctrl-R : Redo last undo change Ctrl-G : Search text Ctrl-M : New line Ctrl-L : Refresh screen Ctrl-? : Show this help"; #[derive(PartialEq)] enum StatusMessageKind { Info, Error, } struct StatusMessage { text: String, timestamp: SystemTime, kind: StatusMessageKind, } impl StatusMessage { fn new>(message: S, kind: StatusMessageKind) -> StatusMessage { StatusMessage { text: message.into(), timestamp: SystemTime::now(), kind, } } } fn get_window_size(input: I, mut output: W) -> Result<(usize, usize)> where I: Iterator>, W: Write, { if let Some(s) = term_size::dimensions_stdout() { return Ok(s); } // By moving cursor at the bottom-right corner by 'B' and 'C' commands, get the size of // current screen. \x1b[9999;9999H is not available since it does not guarantee cursor // stops on the corner. Finally command 'n' queries cursor position. output.write(b"\x1b[9999C\x1b[9999B\x1b[6n")?; output.flush()?; // Wait for response from terminal discarding other sequences for seq in input { if let KeySeq::Cursor(r, c) = seq?.key { return Ok((c, r)); } } Err(Error::UnknownWindowSize) // Give up } fn too_small_window(width: usize, height: usize) -> bool { width < 1 || height < 3 } #[derive(PartialEq, Clone, Copy, Debug)] enum DrawMessage { Open, Close, Update, DoNothing, } impl DrawMessage { fn fold(self, rhs: Self) -> Self { // Folding new state into current state. For example, when current state is Open and new state // is Close, applying Open then Close means DoNothing. use DrawMessage::*; match (self, rhs) { (Open, Open) => unreachable!(), (Open, Close) => DoNothing, (Open, Update) => Open, (Close, Open) => Update, (Close, Close) => unreachable!(), (Close, Update) => unreachable!(), (Update, Open) => unreachable!(), (Update, Close) => Close, (Update, Update) => Update, (lhs, DoNothing) => lhs, (DoNothing, rhs) => rhs, } } } pub struct Screen { output: W, // X coordinate in `render` text of rows rx: usize, // Screen size num_cols: usize, num_rows: usize, message: Option, draw_message: DrawMessage, // Dirty line which requires rendering update. After this line must be updated since // updating line may affect highlights of succeeding lines dirty_start: Option, // Watch resize signal sigwinch: SigwinchWatcher, term_color: TermColor, pub cursor_moved: bool, pub rowoff: usize, // Row scroll offset pub coloff: usize, // Column scroll offset } impl Screen { pub fn new(size: Option<(usize, usize)>, input: I, mut output: W) -> Result where I: Iterator>, { let (w, h) = if let Some(s) = size { s } else { get_window_size(input, &mut output)? }; if too_small_window(w, h) { return Err(Error::TooSmallWindow(w, h)); } // Enter alternate screen buffer to restore previous screen on quit // Note that 'CSI ? 47 h' does not work on WSL environment (#11) // https://www.xfree86.org/current/ctlseqs.html#The%20Alternate%20Screen%20Buffer // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html (CSI ? Pm h) output.write(b"\x1b[?1049h")?; Ok(Self { output, rx: 0, num_cols: w, // Screen height is 1 line less than window height due to status bar num_rows: h.saturating_sub(2), message: Some(StatusMessage::new( "Ctrl-? for help", StatusMessageKind::Info, )), draw_message: DrawMessage::Open, dirty_start: Some(0), // Render entire screen at first paint sigwinch: SigwinchWatcher::new()?, term_color: TermColor::from_env(), cursor_moved: true, rowoff: 0, coloff: 0, }) } fn write_flush(&mut self, bytes: &[u8]) -> Result<()> { self.output.write(bytes)?; self.output.flush()?; Ok(()) } fn trim_line>(&self, line: &S) -> String { let line = line.as_ref(); if line.len() <= self.coloff { return "".to_string(); } line.chars().skip(self.coloff).take(self.num_cols).collect() } fn draw_status_bar(&self, mut buf: B, status_bar: &StatusBar) -> Result<()> { write!(buf, "\x1b[{}H", self.rows() + 1)?; buf.write(self.term_color.sequence(Color::Invert))?; let left = status_bar.left(); // TODO: Handle multi-byte chars correctly let left = &left[..cmp::min(left.len(), self.num_cols)]; buf.write(left.as_bytes())?; // Left of status bar let rest_len = self.num_cols - left.len(); if rest_len == 0 { buf.write(self.term_color.sequence(Color::Reset))?; return Ok(()); } let right = status_bar.right(); if right.len() > rest_len { for _ in 0..rest_len { buf.write(b" ")?; } buf.write(self.term_color.sequence(Color::Reset))?; return Ok(()); } for _ in 0..rest_len - right.len() { buf.write(b" ")?; // Add spaces at center of status bar } buf.write(right.as_bytes())?; buf.write(self.term_color.sequence(Color::Reset))?; Ok(()) } fn draw_message_bar(&self, mut buf: B, message: &StatusMessage) -> Result<()> { // TODO: Handle multi-byte chars correctly let text = &message.text[..cmp::min(message.text.len(), self.num_cols)]; write!(buf, "\x1b[{}H", self.num_rows + 2)?; if message.kind == StatusMessageKind::Error { buf.write(self.term_color.sequence(Color::RedBg))?; } buf.write(text.as_bytes())?; if message.kind != StatusMessageKind::Info { buf.write(self.term_color.sequence(Color::Reset))?; } buf.write(b"\x1b[K")?; Ok(()) } pub fn render_welcome(&mut self, status_bar: &StatusBar) -> Result<()> { self.write_flush(b"\x1b[?25l")?; // Hide cursor let mut buf = Vec::with_capacity((self.rows() + 2 + self.num_cols) * 3); buf.write(self.term_color.sequence(Color::Reset))?; for y in 0..self.rows() { write!(buf, "\x1b[{}H", y + 1)?; if y == self.rows() / 3 { let msg_buf = format!("Kiro editor -- version {}", VERSION); let welcome = self.trim_line(&msg_buf); let padding = (self.num_cols - welcome.len()) / 2; if padding > 0 { buf.write(self.term_color.sequence(Color::NonText))?; buf.write(b"~")?; buf.write(self.term_color.sequence(Color::Reset))?; for _ in 0..padding - 1 { buf.write(b" ")?; } } buf.write(welcome.as_bytes())?; } else { buf.write(self.term_color.sequence(Color::NonText))?; buf.write(b"~")?; } buf.write(b"\x1b[K")?; } buf.write(self.term_color.sequence(Color::Reset))?; self.draw_status_bar(&mut buf, status_bar)?; if let Some(message) = &self.message { self.draw_message_bar(&mut buf, message)?; } write!(buf, "\x1b[H")?; // Set cursor to left-top buf.write(b"\x1b[?25h")?; // Show cursor self.write_flush(&buf)?; self.after_render(); Ok(()) } fn draw_rows( &self, mut buf: B, dirty_start: usize, rows: &[Row], hl: &Highlighting, ) -> Result<()> { let row_len = rows.len(); buf.write(self.term_color.sequence(Color::Reset))?; for y in 0..self.rows() { let file_row = y + self.rowoff; if file_row < dirty_start { continue; } // H: Command to move cursor. Here \x1b[H is the same as \x1b[1;1H write!(buf, "\x1b[{}H", y + 1)?; if file_row >= row_len { buf.write(self.term_color.sequence(Color::NonText))?; buf.write(b"~")?; } else { let row = &rows[file_row]; let mut col = 0; let mut prev_color = Color::Reset; for (c, hl) in row.render_text().chars().zip(hl.lines[file_row].iter()) { col += c.width_cjk().unwrap_or(1); if col <= self.coloff { continue; } else if col > self.num_cols + self.coloff { break; } let color = hl.color(); if color != prev_color { if prev_color.has_bg_color() { buf.write(self.term_color.sequence(Color::Reset))?; } buf.write(self.term_color.sequence(color))?; prev_color = color; } write!(buf, "{}", c)?; } } // Ensure to end with reset color sequence. Otherwise, when background color is highlighted // at the end of line, highlight will continue to the end of last column in terminal window. buf.write(self.term_color.sequence(Color::Reset))?; // Erases the part of the line to the right of the cursor. http://vt100.net/docs/vt100-ug/chapter3.html#EL buf.write(b"\x1b[K")?; } Ok(()) } fn redraw( &mut self, text_buf: &TextBuffer, hl: &Highlighting, status_bar: &StatusBar, ) -> Result<()> { let cursor_row = text_buf.cy() - self.rowoff + 1; let cursor_col = self.rx - self.coloff + 1; let draw_message = self.draw_message; if self.dirty_start.is_none() && !status_bar.redraw && draw_message == DrawMessage::DoNothing { if self.cursor_moved { write!(self.output, "\x1b[{};{}H", cursor_row, cursor_col)?; self.output.flush()?; } return Ok(()); } // \x1b[: Escape sequence header // Hide cursor while updating screen. 'l' is command to set mode http://vt100.net/docs/vt100-ug/chapter3.html#SM // This command must be flushed at first otherwise cursor may move before being hidden self.write_flush(b"\x1b[?25l")?; let mut buf = Vec::with_capacity((self.rows() + 2) * self.num_cols); if let Some(s) = self.dirty_start { self.draw_rows(&mut buf, s, text_buf.rows(), hl)?; } // When message bar opens/closes, position of status bar is changed if status_bar.redraw || draw_message == DrawMessage::Open || draw_message == DrawMessage::Close { self.draw_status_bar(&mut buf, status_bar)?; } // When closing message bar, nothing to do since status bar will overwrite old message bar if draw_message == DrawMessage::Update || draw_message == DrawMessage::Open { if let Some(message) = &self.message { self.draw_message_bar(&mut buf, message)?; } } // Move cursor even if cursor_moved is false since cursor is moved by draw_* methods write!(buf, "\x1b[{};{}H", cursor_row, cursor_col)?; // Reveal cursor again. 'h' is command to reset mode https://vt100.net/docs/vt100-ug/chapter3.html#RM buf.write(b"\x1b[?25h")?; self.write_flush(&buf)?; Ok(()) } fn next_coloff(&self, want_stop: usize, row: &Row) -> usize { let mut coloff = 0; for c in row.render_text().chars() { coloff += c.width_cjk().unwrap_or(1); if coloff >= want_stop { // Screen cannot start from at the middle of double-width character break; } } coloff } fn do_scroll(&mut self, rows: &[Row], (cx, cy): (usize, usize)) { let prev_rowoff = self.rowoff; let prev_coloff = self.coloff; // Calculate X coordinate to render considering tab stop if cy < rows.len() { self.rx = rows[cy].rx_from_cx(cx); } else { self.rx = 0; } // Adjust scroll position when cursor is outside screen if cy < self.rowoff { // Scroll up when cursor is above the top of window self.rowoff = cy; } if cy >= self.rowoff + self.rows() { // Scroll down when cursor is below the bottom of screen self.rowoff = cy - self.rows() + 1; } if self.rx < self.coloff { self.coloff = self.rx; } if self.rx >= self.coloff + self.num_cols { self.coloff = self.next_coloff(self.rx - self.num_cols + 1, &rows[cy]); } if prev_rowoff != self.rowoff || prev_coloff != self.coloff { // If scroll happens, all rows on screen must be updated // TODO: Improve rendering on scrolling up/down using scroll region commands \x1b[M/\x1b[D. // But scroll down region command was implemented in tmux recently and not included in // stable release: https://github.com/tmux/tmux/commit/45f4ff54850ff9b448070a96b33e63451f973e33 self.set_dirty_start(self.rowoff); } } fn update_message_bar(&mut self) -> Result<()> { if let Some(m) = &self.message { if SystemTime::now().duration_since(m.timestamp)?.as_secs() > 5 { self.unset_message(); } } if self.draw_message == DrawMessage::Close { self.set_dirty_start(self.num_rows); // Closing message bar reveals one more line } Ok(()) } fn after_render(&mut self) { // Clear state self.dirty_start = None; self.cursor_moved = false; self.draw_message = DrawMessage::DoNothing; } pub fn render( &mut self, buf: &TextBuffer, hl: &mut Highlighting, status_bar: &StatusBar, ) -> Result<()> { self.do_scroll(buf.rows(), buf.cursor()); self.update_message_bar()?; // This must be updated here since it affects area of highlighting hl.update(buf.rows(), self.rowoff + self.rows()); self.redraw(buf, hl, status_bar)?; self.after_render(); Ok(()) } pub fn render_help(&mut self) -> Result<()> { let help: Vec<_> = HELP .split('\n') .skip_while(|s| !s.contains(':')) .map(str::trim_start) .collect(); let rows = self.rows(); let vertical_margin = if help.len() < rows { (rows - help.len()) / 2 } else { 0 }; let help_max_width = help.iter().map(|l| l.len()).max().unwrap(); let left_margin = if help_max_width < self.num_cols { (self.num_cols - help_max_width) / 2 } else { 0 }; let mut buf = Vec::with_capacity(rows * self.num_cols); for y in 0..vertical_margin { write!(buf, "\x1b[{}H", y + 1)?; buf.write(b"\x1b[K")?; } let left_pad = " ".repeat(left_margin); let help_height = cmp::min(vertical_margin + help.len(), rows); for y in vertical_margin..help_height { let idx = y - vertical_margin; write!(buf, "\x1b[{}H", y + 1)?; buf.write(left_pad.as_bytes())?; let help = &help[idx][..cmp::min(help[idx].len(), self.num_cols)]; buf.write(self.term_color.sequence(Color::Cyan))?; let mut cols = help.split(':'); if let Some(col) = cols.next() { buf.write(col.as_bytes())?; } buf.write(self.term_color.sequence(Color::Reset))?; if let Some(col) = cols.next() { write!(buf, ":{}", col)?; } buf.write(b"\x1b[K")?; } for y in help_height..rows { write!(buf, "\x1b[{}H", y + 1)?; buf.write(b"\x1b[K")?; } self.write_flush(&buf) } pub fn set_dirty_start(&mut self, start: usize) { if let Some(s) = self.dirty_start { if s < start { return; } } self.dirty_start = Some(start); } pub fn maybe_resize(&mut self, input: I) -> Result where I: Iterator>, { if !self.sigwinch.notified() { return Ok(false); // Did not receive signal } let (w, h) = get_window_size(input, &mut self.output)?; if too_small_window(w, h) { return Err(Error::TooSmallWindow(w, h)); } self.num_rows = h.saturating_sub(2); self.num_cols = w; self.dirty_start = Some(0); Ok(true) } fn set_message(&mut self, m: Option) { let op = match (&self.message, &m) { (Some(p), Some(n)) if p.text == n.text => DrawMessage::DoNothing, (Some(_), Some(_)) => DrawMessage::Update, (None, Some(_)) => DrawMessage::Open, (Some(_), None) => DrawMessage::Close, (None, None) => DrawMessage::DoNothing, }; // Folding is necessary to consider that message is set multiple times within one tick self.draw_message = self.draw_message.fold(op); self.message = m; } pub fn set_info_message>(&mut self, message: S) { self.set_message(Some(StatusMessage::new(message, StatusMessageKind::Info))); } pub fn set_error_message>(&mut self, message: S) { self.set_message(Some(StatusMessage::new(message, StatusMessageKind::Error))); } pub fn unset_message(&mut self) { self.set_message(None); } pub fn rows(&self) -> usize { if self.message.is_some() { self.num_rows } else { self.num_rows + 1 } } pub fn cols(&self) -> usize { self.num_cols } pub fn message_text(&self) -> &'_ str { self.message.as_ref().map(|m| m.text.as_str()).unwrap_or("") } pub fn force_set_cursor(&mut self, row: usize, col: usize) -> Result<()> { write!(self.output, "\x1b[{};{}H", row, col)?; self.output.flush()?; Ok(()) } } impl Drop for Screen { fn drop(&mut self) { // Back to normal screen buffer from alternate screen buffer // https://www.xfree86.org/current/ctlseqs.html#The%20Alternate%20Screen%20Buffer // // Note that we used \x1b[2J\x1b[H previously but it did not erase screen and // \x1b[?47l\x1b[H worked but not in the WSL terminal environment (#11). // // CSI ? 1049 h // > Save cursor as in DECSC, xterm. After saving the cursor, switch to the Alternate // > Screen Buffer, clearing it first. This may be disabled by the titeInhibit resource. // > This control combines the effects of the 1 0 4 7 and 1 0 4 8 modes. Use this with // > terminfo-based applications rather than the 4 7 mode. // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html self.write_flush(b"\x1b[?1049l\x1b[H") .expect("Back to normal screen buffer"); } }