diff --git a/src/main.rs b/src/main.rs index c6a77e2a..6c337250 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,14 @@ use ::signal_hook::iterator::Signals; pub type OnSigWinch = dyn Fn(Box) + Send; pub type SigCleanup = dyn Fn() + Send; +fn debug_log_to_file (message: String) { + use std::fs::OpenOptions; + use std::io::prelude::*; + let mut file = OpenOptions::new().append(true).create(true).open("/tmp/mosaic-log.txt").unwrap(); + file.write_all(message.as_bytes()).unwrap(); + file.write_all("\n".as_bytes()).unwrap(); +} + pub fn sigwinch() -> (Box, Box) { let signals = Signals::new(&[signal_hook::SIGWINCH]).unwrap(); let on_winch = { diff --git a/src/pty_bus.rs b/src/pty_bus.rs index cc166120..7a7cf790 100644 --- a/src/pty_bus.rs +++ b/src/pty_bus.rs @@ -4,6 +4,7 @@ use ::async_std::task; use ::async_std::task::*; use ::std::pin::*; use ::std::sync::mpsc::{channel, Sender, Receiver}; +use ::std::time::{Instant, Duration}; use ::vte; use crate::os_input_output::OsApi; @@ -23,10 +24,18 @@ impl ReadFromPid { } } +fn debug_log_to_file (message: String) { + use std::fs::OpenOptions; + use std::io::prelude::*; + let mut file = OpenOptions::new().append(true).create(true).open("/tmp/mosaic-log.txt").unwrap(); + file.write_all(message.as_bytes()).unwrap(); + file.write_all("\n".as_bytes()).unwrap(); +} + impl Stream for ReadFromPid { type Item = Vec; fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - let mut read_buffer = [0; 115200]; + let mut read_buffer = [0; 65535]; let pid = self.pid; let read_result = &self.os_input.read_from_tty_stdout(pid, &mut read_buffer); match read_result { @@ -136,6 +145,60 @@ pub struct PtyBus { os_input: Box, } +fn stream_terminal_bytes(pid: RawFd, send_screen_instructions: Sender, os_input: Box) { + task::spawn({ + async move { + let mut vte_parser = vte::Parser::new(); + let mut vte_event_sender = VteEventSender::new(pid, send_screen_instructions.clone()); + let mut terminal_bytes = ReadFromPid::new(&pid, os_input); + + let mut last_byte_receive_time: Option = None; + let mut pending_render = false; + let max_render_pause = Duration::from_millis(30); + + while let Some(bytes) = terminal_bytes.next().await { + let bytes_is_empty = bytes.is_empty(); + for byte in bytes { + vte_parser.advance(&mut vte_event_sender, byte); + } + if !bytes_is_empty { + // for UX reasons, if we got something on the wire, we only send the render notice if: + // 1. there aren't any more bytes on the wire afterwards + // 2. a certain period (currently 30ms) has elapsed since the last render + // (otherwise if we get a large amount of data, the display would hang + // until it's done) + // 3. the stream has ended, and so we render 1 last time + match last_byte_receive_time.as_mut() { + Some(receive_time) => { + // if receive_time.elapsed() > Duration::from_millis(30) { + if receive_time.elapsed() > max_render_pause { + pending_render = false; + send_screen_instructions.send(ScreenInstruction::Render).unwrap(); + last_byte_receive_time = Some(Instant::now()); + } else { + pending_render = true; + } + }, + None => { + last_byte_receive_time = Some(Instant::now()); + pending_render = true; + + } + }; + } else { + if pending_render { + pending_render = false; + send_screen_instructions.send(ScreenInstruction::Render).unwrap(); + } + last_byte_receive_time = None; + task::sleep(::std::time::Duration::from_millis(10)).await; + } + } + send_screen_instructions.send(ScreenInstruction::Render).unwrap(); + } + }); +} + impl PtyBus { pub fn new (send_screen_instructions: Sender, os_input: Box) -> Self { let (send_pty_instructions, receive_pty_instructions): (Sender, Receiver) = channel(); @@ -148,50 +211,12 @@ impl PtyBus { } pub fn spawn_terminal_vertically(&mut self) { let (pid_primary, _pid_secondary): (RawFd, RawFd) = self.os_input.spawn_terminal(); - task::spawn({ - let send_screen_instructions = self.send_screen_instructions.clone(); - let os_input = self.os_input.clone(); - async move { - let mut vte_parser = vte::Parser::new(); - let mut vte_event_sender = VteEventSender::new(pid_primary, send_screen_instructions.clone()); - let mut first_terminal_bytes = ReadFromPid::new(&pid_primary, os_input); - while let Some(bytes) = first_terminal_bytes.next().await { - let bytes_is_empty = bytes.is_empty(); - for byte in bytes { - vte_parser.advance(&mut vte_event_sender, byte); - } - if !bytes_is_empty { - send_screen_instructions.send(ScreenInstruction::Render).unwrap(); - } else { - task::sleep(::std::time::Duration::from_millis(10)).await; - } - } - } - }); + stream_terminal_bytes(pid_primary, self.send_screen_instructions.clone(), self.os_input.clone()); self.send_screen_instructions.send(ScreenInstruction::VerticalSplit(pid_primary)).unwrap(); } pub fn spawn_terminal_horizontally(&mut self) { let (pid_primary, _pid_secondary): (RawFd, RawFd) = self.os_input.spawn_terminal(); - task::spawn({ - let send_screen_instructions = self.send_screen_instructions.clone(); - let os_input = self.os_input.clone(); - async move { - let mut vte_parser = vte::Parser::new(); - let mut vte_event_sender = VteEventSender::new(pid_primary, send_screen_instructions.clone()); - let mut first_terminal_bytes = ReadFromPid::new(&pid_primary, os_input); - while let Some(bytes) = first_terminal_bytes.next().await { - let bytes_is_empty = bytes.is_empty(); - for byte in bytes { - vte_parser.advance(&mut vte_event_sender, byte); - } - if !bytes_is_empty { - send_screen_instructions.send(ScreenInstruction::Render).unwrap(); - } else { - task::sleep(::std::time::Duration::from_millis(10)).await; - } - } - } - }); + stream_terminal_bytes(pid_primary, self.send_screen_instructions.clone(), self.os_input.clone()); self.send_screen_instructions.send(ScreenInstruction::HorizontalSplit(pid_primary)).unwrap(); } } diff --git a/src/terminal_pane.rs b/src/terminal_pane.rs index 2b1707b6..7c05f71b 100644 --- a/src/terminal_pane.rs +++ b/src/terminal_pane.rs @@ -10,17 +10,19 @@ use crate::boundaries::Rect; const EMPTY_TERMINAL_CHARACTER: TerminalCharacter = TerminalCharacter { character: ' ', - foreground: Some(AnsiCode::Reset), - background: Some(AnsiCode::Reset), - strike: Some(AnsiCode::Reset), - hidden: Some(AnsiCode::Reset), - reverse: Some(AnsiCode::Reset), - slow_blink: Some(AnsiCode::Reset), - fast_blink: Some(AnsiCode::Reset), - underline: Some(AnsiCode::Reset), - bold: Some(AnsiCode::Reset), - dim: Some(AnsiCode::Reset), - italic: Some(AnsiCode::Reset), + styles: CharacterStyles { + foreground: Some(AnsiCode::Reset), + background: Some(AnsiCode::Reset), + strike: Some(AnsiCode::Reset), + hidden: Some(AnsiCode::Reset), + reverse: Some(AnsiCode::Reset), + slow_blink: Some(AnsiCode::Reset), + fast_blink: Some(AnsiCode::Reset), + underline: Some(AnsiCode::Reset), + bold: Some(AnsiCode::Reset), + dim: Some(AnsiCode::Reset), + italic: Some(AnsiCode::Reset), + } }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -157,7 +159,7 @@ impl CharacterStyles { self.dim = None; self.italic = None; } - pub fn update_and_return_diff(&mut self, new_styles: &TerminalCharacter) -> Option { + pub fn update_and_return_diff(&mut self, new_styles: &CharacterStyles) -> Option { let mut diff: Option = None; if self.foreground != new_styles.foreground { if let Some(new_diff) = diff.as_mut() { @@ -535,356 +537,7 @@ impl Display for CharacterStyles { #[derive(Clone, Copy)] pub struct TerminalCharacter { pub character: char, - pub foreground: Option, - pub background: Option, - pub strike: Option, - pub hidden: Option, - pub reverse: Option, - pub slow_blink: Option, - pub fast_blink: Option, - pub underline: Option, - pub bold: Option, - pub dim: Option, - pub italic: Option, -} - -impl TerminalCharacter { - pub fn new (character: char) -> Self { - TerminalCharacter { - character, - foreground: None, - background: None, - strike: None, - hidden: None, - reverse: None, - slow_blink: None, - fast_blink: None, - underline: None, - bold: None, - dim: None, - italic: None, - } - } - pub fn foreground(mut self, foreground_code: Option) -> Self { - self.foreground = foreground_code; - self - } - pub fn background(mut self, background_code: Option) -> Self { - self.background = background_code; - self - } - pub fn bold(mut self, bold_code: Option) -> Self { - self.bold = bold_code; - self - } - pub fn dim(mut self, dim_code: Option) -> Self { - self.dim = dim_code; - self - } - pub fn italic(mut self, italic_code: Option) -> Self { - self.italic = italic_code; - self - } - pub fn underline(mut self, underline_code: Option) -> Self { - self.underline = underline_code; - self - } - pub fn blink_slow(mut self, slow_blink_code: Option) -> Self { - self.slow_blink = slow_blink_code; - self - } - pub fn blink_fast(mut self, fast_blink_code: Option) -> Self { - self.fast_blink = fast_blink_code; - self - } - pub fn reverse(mut self, reverse_code: Option) -> Self { - self.reverse = reverse_code; - self - } - pub fn hidden(mut self, hidden_code: Option) -> Self { - self.hidden = hidden_code; - self - } - pub fn strike(mut self, strike_code: Option) -> Self { - self.strike = strike_code; - self - } -} - -impl Display for TerminalCharacter { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut character_ansi_codes = String::new(); // TODO: better - - if let Some(ansi_code) = self.foreground { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[38;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[38;{}m", param1)); - }, - (_, _) => { - // TODO: can this happen? - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[39m")); - }, - AnsiCode::NamedColor(named_color) => { - character_ansi_codes.push_str(&format!("\u{1b}[{}m", named_color.to_foreground_ansi_code())); - } - } - } - if let Some(ansi_code) = self.background { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[48;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[48;{}m", param1)); - }, - (_, _) => { - // TODO: can this happen? - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[49m")); - } - AnsiCode::NamedColor(named_color) => { - character_ansi_codes.push_str(&format!("\u{1b}[{}m", named_color.to_background_ansi_code())); - } - } - } - if let Some(ansi_code) = self.strike { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[9;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[9;{}m", param1)); - }, - (_, _) => { - character_ansi_codes.push_str("\u{1b}[9m"); - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[29m")); - }, - _ => {} - } - } - if let Some(ansi_code) = self.hidden { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[8;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[8;{}m", param1)); - }, - (_, _) => { - character_ansi_codes.push_str("\u{1b}[8m"); - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[28m")); - }, - _ => {} - } - } - if let Some(ansi_code) = self.reverse { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[7;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[7;{}m", param1)); - }, - (_, _) => { - character_ansi_codes.push_str("\u{1b}[7m"); - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[27m")); - }, - _ => {} - } - } - if let Some(ansi_code) = self.fast_blink { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[6;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[6;{}m", param1)); - }, - (_, _) => { - character_ansi_codes.push_str("\u{1b}[6m"); - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[25m")); - }, - _ => {} - } - } - if let Some(ansi_code) = self.slow_blink { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[5;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[5;{}m", param1)); - }, - (_, _) => { - character_ansi_codes.push_str("\u{1b}[5m"); - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[25m")); - }, - _ => {} - } - } - if let Some(ansi_code) = self.underline { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[4;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[4;{}m", param1)); - }, - (_, _) => { - character_ansi_codes.push_str("\u{1b}[4m"); - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[24m")); - }, - _ => {} - } - } - if let Some(ansi_code) = self.bold { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[1;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[1;{}m", param1)); - }, - (_, _) => { - character_ansi_codes.push_str("\u{1b}[1m"); - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[22m\u{1b}[24m")); - // character_ansi_codes.push_str(&format!("\u{1b}[22m")); - // TODO: this cancels bold + underline, if this behaviour is indeed correct, we - // need to properly handle it in the struct methods etc like dim - }, - _ => {} - } - } - // notice the order is important here, bold must be before underline - // because the bold reset also resets underline, and would override it - // otherwise - if let Some(ansi_code) = self.underline { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[4;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[4;{}m", param1)); - }, - (_, _) => { - character_ansi_codes.push_str("\u{1b}[4m"); - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[24m")); - }, - _ => {} - } - } - if let Some(ansi_code) = self.dim { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[2;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[2;{}m", param1)); - }, - (_, _) => { - character_ansi_codes.push_str("\u{1b}[2m"); - } - } - }, - AnsiCode::Reset => { - if let Some(bold) = self.bold { - // we only reset dim if both dim and bold should be reset - match bold { - AnsiCode::Reset => character_ansi_codes.push_str(&format!("\u{1b}[22m")), // TODO: better - _ => {} - } - } - }, - _ => {} - } - } - if let Some(ansi_code) = self.italic { - match ansi_code { - AnsiCode::Code((param1, param2)) => { - match (param1, param2) { - (Some(param1), Some(param2)) => { - character_ansi_codes.push_str(&format!("\u{1b}[3;{};{}m", param1, param2)); - }, - (Some(param1), None) => { - character_ansi_codes.push_str(&format!("\u{1b}[3;{}m", param1)); - }, - (_, _) => { - character_ansi_codes.push_str("\u{1b}[3m"); - } - } - }, - AnsiCode::Reset => { - character_ansi_codes.push_str(&format!("\u{1b}[23m")); - }, - _ => {} - } - }; - write!(f, "{}", character_ansi_codes) - } + pub styles: CharacterStyles, } impl ::std::fmt::Debug for TerminalCharacter { @@ -978,7 +631,7 @@ impl<'a> Grid <'a>{ let mut newline_indices: Vec = vec![]; for line in &self.cells { for character in line.iter().copied() { - characters.push(character.clone()); + characters.push(*character); } let last_newline_index = newline_indices.last().copied().unwrap_or(0); newline_indices.push(last_newline_index + line.len()); @@ -1046,9 +699,10 @@ impl Rect for &mut TerminalOutput { impl TerminalOutput { pub fn new (pid: RawFd, ws: Winsize, x_coords: u16, y_coords: u16) -> TerminalOutput { + let characters = Vec::with_capacity(100000); TerminalOutput { pid, - characters: vec![], + characters, cursor_position: 0, newline_indices: Vec::new(), linebreak_indices: Vec::new(), @@ -1073,10 +727,11 @@ impl TerminalOutput { } } pub fn handle_event(&mut self, event: VteEvent) { - self.should_render = true; // TODO: more accurately + // self.should_render = true; // TODO: more accurately match event { VteEvent::Print(c) => { self.print(c); + self.should_render = true; // TODO: more accurately }, VteEvent::Execute(byte) => { self.execute(byte); @@ -1188,7 +843,7 @@ impl TerminalOutput { // in some cases (eg. while resizing) some characters will spill over // before they are corrected by the shell (for the prompt) or by reflowing // lines - if let Some(new_styles) = character_styles.update_and_return_diff(&t_character) { + if let Some(new_styles) = character_styles.update_and_return_diff(&t_character.styles) { vte_output = format!("{}{}", vte_output, new_styles); } vte_output.push(t_character.character); @@ -1495,36 +1150,47 @@ fn debug_log_to_file (message: String, pid: RawFd) { impl vte::Perform for TerminalOutput { fn print(&mut self, c: char) { - // while not ideal that we separate the reset and actual code logic here, - // combining them is a question of rendering performance and not refactoring, - // so will be addressed separately - let terminal_character = TerminalCharacter::new(c) - .foreground(self.pending_foreground_code) - .background(self.pending_background_code) - .bold(self.pending_bold_code) - .dim(self.pending_dim_code) - .italic(self.pending_italic_code) - .underline(self.pending_underline_code) - .blink_slow(self.pending_slow_blink_code) - .blink_fast(self.pending_fast_blink_code) - .reverse(self.pending_reverse_code) - .hidden(self.pending_hidden_code) - .strike(self.pending_strike_code); + // apparently, building TerminalCharacter like this without a "new" method + // is a little faster + let terminal_character = TerminalCharacter { + character: c, + styles: CharacterStyles { + foreground: self.pending_foreground_code, + background: self.pending_background_code, + bold: self.pending_bold_code, + dim: self.pending_dim_code, + italic: self.pending_italic_code, + underline: self.pending_underline_code, + slow_blink: self.pending_slow_blink_code, + fast_blink: self.pending_fast_blink_code, + reverse: self.pending_reverse_code, + hidden: self.pending_hidden_code, + strike: self.pending_strike_code, + } + }; - if self.characters.len() > self.cursor_position { - self.characters.remove(self.cursor_position); - self.characters.insert(self.cursor_position, terminal_character); + let length_of_characters = self.characters.len(); + let current_character_capacity = self.characters.capacity(); + + if current_character_capacity <= self.characters.len() { + self.characters.reserve(current_character_capacity); + } + + if length_of_characters > self.cursor_position { + // this is a little hacky but significantly more performant + // than removing self.cursor_position and then inserting terminal_character + self.characters.push(terminal_character); + self.characters.swap_remove(self.cursor_position); if !self.newline_indices.contains(&(self.cursor_position + 1)) { // advancing the cursor beyond the borders of the line has to be done explicitly self.cursor_position += 1; } } else { - for _ in self.characters.len()..self.cursor_position { - self.characters.push(EMPTY_TERMINAL_CHARACTER.clone()); + for _ in length_of_characters..self.cursor_position { + self.characters.push(EMPTY_TERMINAL_CHARACTER); }; self.characters.push(terminal_character); - - let start_of_last_line = self.index_of_beginning_of_line(self.cursor_position); + let start_of_last_line = max(self.newline_indices.last(), self.linebreak_indices.last()).unwrap_or(&0); let difference_from_last_newline = self.cursor_position - start_of_last_line; if difference_from_last_newline == self.display_cols as usize && self.scroll_region.is_none() { self.linebreak_indices.push(self.cursor_position);