diff options
-rw-r--r-- | src/state/history/entry.rs | 305 | ||||
-rw-r--r-- | src/state/history/mod.rs | 304 | ||||
-rw-r--r-- | src/state/history/pty.rs | 3 |
3 files changed, 311 insertions, 301 deletions
diff --git a/src/state/history/entry.rs b/src/state/history/entry.rs new file mode 100644 index 0000000..932ec80 --- /dev/null +++ b/src/state/history/entry.rs @@ -0,0 +1,305 @@ +use std::os::unix::process::ExitStatusExt as _; + +pub struct Entry { + cmdline: String, + env: crate::env::Env, + vt: vt100::Parser, + audible_bell_state: usize, + visual_bell_state: usize, + fullscreen: Option<bool>, + input: async_std::channel::Sender<Vec<u8>>, + resize: async_std::channel::Sender<(u16, u16)>, + start_time: time::OffsetDateTime, + start_instant: std::time::Instant, + exit_info: Option<ExitInfo>, +} + +impl Entry { + pub fn new( + cmdline: String, + env: crate::env::Env, + size: (u16, u16), + input: async_std::channel::Sender<Vec<u8>>, + resize: async_std::channel::Sender<(u16, u16)>, + ) -> Self { + Self { + cmdline, + env, + vt: vt100::Parser::new(size.0, size.1, 0), + audible_bell_state: 0, + visual_bell_state: 0, + input, + resize, + fullscreen: None, + start_time: time::OffsetDateTime::now_utc(), + start_instant: std::time::Instant::now(), + exit_info: None, + } + } + + pub fn render( + &self, + out: &mut impl textmode::Textmode, + idx: usize, + entry_count: usize, + width: u16, + focused: bool, + scrolling: bool, + offset: time::UtcOffset, + ) { + let time = self.exit_info.as_ref().map_or_else( + || { + format!( + "[{}]", + crate::format::time(self.start_time.to_offset(offset)) + ) + }, + |info| { + format!( + "({}) [{}]", + crate::format::duration( + info.instant - self.start_instant + ), + crate::format::time(self.start_time.to_offset(offset)), + ) + }, + ); + + set_bgcolor(out, idx, focused); + out.set_fgcolor(textmode::color::YELLOW); + let entry_count_width = format!("{}", entry_count + 1).len(); + let idx_str = format!("{}", idx + 1); + out.write_str(&" ".repeat(entry_count_width - idx_str.len())); + out.write_str(&idx_str); + out.write_str(" "); + out.reset_attributes(); + + set_bgcolor(out, idx, focused); + if let Some(info) = &self.exit_info { + if info.status.signal().is_some() { + out.set_fgcolor(textmode::color::MAGENTA); + } else if info.status.success() { + out.set_fgcolor(textmode::color::DARKGREY); + } else { + out.set_fgcolor(textmode::color::RED); + } + out.write_str(&crate::format::exit_status(info.status)); + } else { + out.write_str(" "); + } + out.reset_attributes(); + + set_bgcolor(out, idx, focused); + out.write_str("$ "); + if self.running() { + out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); + } + let cmd = self.cmd(); + let start = usize::from(out.screen().cursor_position().1); + let end = usize::from(width) - time.len() - 2; + let max_len = end - start; + if cmd.len() > max_len { + out.write_str(&cmd[..(max_len - 4)]); + out.set_fgcolor(textmode::color::BLUE); + out.write_str(" ..."); + } else { + out.write_str(cmd); + } + out.reset_attributes(); + + set_bgcolor(out, idx, focused); + let cur_pos = out.screen().cursor_position(); + out.write_str(&" ".repeat( + usize::from(width) - time.len() - 1 - usize::from(cur_pos.1), + )); + out.write_str(&time); + out.write_str(" "); + out.reset_attributes(); + + if self.binary() { + let msg = "This appears to be binary data. Fullscreen this entry to view anyway."; + let len: u16 = msg.len().try_into().unwrap(); + out.move_to( + out.screen().cursor_position().0 + 1, + (width - len) / 2, + ); + out.set_fgcolor(textmode::color::RED); + out.write_str(msg); + out.hide_cursor(true); + } else { + let last_row = self.output_lines(width, focused && !scrolling); + if last_row > 5 { + out.write(b"\r\n"); + out.set_fgcolor(textmode::color::BLUE); + out.write_str("..."); + out.reset_attributes(); + } + let mut out_row = out.screen().cursor_position().0 + 1; + let screen = self.vt.screen(); + let pos = screen.cursor_position(); + let mut wrapped = false; + let mut cursor_found = None; + for (idx, row) in screen + .rows_formatted(0, width) + .enumerate() + .take(last_row) + .skip(last_row.saturating_sub(5)) + { + let idx: u16 = idx.try_into().unwrap(); + out.reset_attributes(); + if !wrapped { + out.move_to(out_row, 0); + } + out.write(&row); + wrapped = screen.row_wrapped(idx); + if pos.0 == idx { + cursor_found = Some(out_row); + } + out_row += 1; + } + if focused && !scrolling { + if let Some(row) = cursor_found { + out.hide_cursor(screen.hide_cursor()); + out.move_to(row, pos.1); + } else { + out.hide_cursor(true); + } + } + } + out.reset_attributes(); + } + + pub fn render_fullscreen(&mut self, out: &mut impl textmode::Textmode) { + let screen = self.vt.screen(); + let new_audible_bell_state = screen.audible_bell_count(); + let new_visual_bell_state = screen.visual_bell_count(); + + out.write(&screen.state_formatted()); + + if self.audible_bell_state != new_audible_bell_state { + out.write(b"\x07"); + self.audible_bell_state = new_audible_bell_state; + } + + if self.visual_bell_state != new_visual_bell_state { + out.write(b"\x1bg"); + self.visual_bell_state = new_visual_bell_state; + } + + out.reset_attributes(); + } + + pub async fn send_input(&self, bytes: Vec<u8>) { + if self.running() { + self.input.send(bytes).await.unwrap(); + } + } + + pub async fn resize(&mut self, size: (u16, u16)) { + if self.running() { + self.resize.send(size).await.unwrap(); + self.vt.set_size(size.0, size.1); + } + } + + pub fn size(&self) -> (u16, u16) { + self.vt.screen().size() + } + + pub fn process(&mut self, input: &[u8]) { + self.vt.process(input); + } + + pub fn cmd(&self) -> &str { + &self.cmdline + } + + pub fn env(&self) -> &crate::env::Env { + &self.env + } + + pub fn toggle_fullscreen(&mut self) { + if let Some(fullscreen) = self.fullscreen { + self.fullscreen = Some(!fullscreen); + } else { + self.fullscreen = Some(!self.vt.screen().alternate_screen()); + } + } + + pub fn set_fullscreen(&mut self, fullscreen: bool) { + self.fullscreen = Some(fullscreen); + } + + pub fn running(&self) -> bool { + self.exit_info.is_none() + } + + pub fn binary(&self) -> bool { + self.vt.screen().errors() > 5 + } + + pub fn lines(&self, width: u16, focused: bool) -> usize { + let lines = self.output_lines(width, focused); + 1 + std::cmp::min(6, lines) + } + + pub fn output_lines(&self, width: u16, focused: bool) -> usize { + if self.binary() { + return 1; + } + + let screen = self.vt.screen(); + let mut last_row = 0; + for (idx, row) in screen.rows(0, width).enumerate() { + if !row.is_empty() { + last_row = idx + 1; + } + } + if focused && self.running() { + last_row = std::cmp::max( + last_row, + usize::from(screen.cursor_position().0) + 1, + ); + } + last_row + } + + pub fn should_fullscreen(&self) -> bool { + self.fullscreen + .unwrap_or_else(|| self.vt.screen().alternate_screen()) + } + + pub async fn finish( + &mut self, + env: crate::env::Env, + event_w: async_std::channel::Sender<crate::event::Event>, + ) { + self.exit_info = Some(ExitInfo::new(*env.latest_status())); + self.env = env; + event_w.send(crate::event::Event::PtyClose).await.unwrap(); + } +} + +struct ExitInfo { + status: async_std::process::ExitStatus, + instant: std::time::Instant, +} + +impl ExitInfo { + fn new(status: async_std::process::ExitStatus) -> Self { + Self { + status, + instant: std::time::Instant::now(), + } + } +} + +fn set_bgcolor(out: &mut impl textmode::Textmode, idx: usize, focus: bool) { + if focus { + out.set_bgcolor(textmode::Color::Rgb(0x56, 0x1b, 0x8b)); + } else if idx % 2 == 0 { + out.set_bgcolor(textmode::Color::Rgb(0x24, 0x21, 0x00)); + } else { + out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20)); + } +} diff --git a/src/state/history/mod.rs b/src/state/history/mod.rs index 8b15916..9df3103 100644 --- a/src/state/history/mod.rs +++ b/src/state/history/mod.rs @@ -3,6 +3,8 @@ use futures_lite::future::FutureExt as _; use std::os::unix::io::{FromRawFd as _, IntoRawFd as _}; use std::os::unix::process::ExitStatusExt as _; +mod entry; +pub use entry::Entry; mod pty; pub struct History { @@ -144,7 +146,7 @@ impl History { self.entries.push(async_std::sync::Arc::clone(&entry)); let mut entry = entry.lock_arc().await; - entry.vt.process(err_str.replace('\n', "\r\n").as_bytes()); + entry.process(err_str.replace('\n', "\r\n").as_bytes()); let mut env = env.clone(); env.set_status(async_std::process::ExitStatus::from_raw(1 << 8)); entry.finish(env, event_w).await; @@ -267,291 +269,6 @@ impl std::iter::DoubleEndedIterator for VisibleEntries { } } -pub struct Entry { - cmdline: String, - env: crate::env::Env, - vt: vt100::Parser, - audible_bell_state: usize, - visual_bell_state: usize, - fullscreen: Option<bool>, - input: async_std::channel::Sender<Vec<u8>>, - resize: async_std::channel::Sender<(u16, u16)>, - start_time: time::OffsetDateTime, - start_instant: std::time::Instant, - exit_info: Option<ExitInfo>, -} - -impl Entry { - fn new( - cmdline: String, - env: crate::env::Env, - size: (u16, u16), - input: async_std::channel::Sender<Vec<u8>>, - resize: async_std::channel::Sender<(u16, u16)>, - ) -> Self { - Self { - cmdline, - env, - vt: vt100::Parser::new(size.0, size.1, 0), - audible_bell_state: 0, - visual_bell_state: 0, - input, - resize, - fullscreen: None, - start_time: time::OffsetDateTime::now_utc(), - start_instant: std::time::Instant::now(), - exit_info: None, - } - } - - fn render( - &self, - out: &mut impl textmode::Textmode, - idx: usize, - entry_count: usize, - width: u16, - focused: bool, - scrolling: bool, - offset: time::UtcOffset, - ) { - let time = self.exit_info.as_ref().map_or_else( - || { - format!( - "[{}]", - crate::format::time(self.start_time.to_offset(offset)) - ) - }, - |info| { - format!( - "({}) [{}]", - crate::format::duration( - info.instant - self.start_instant - ), - crate::format::time(self.start_time.to_offset(offset)), - ) - }, - ); - - set_bgcolor(out, idx, focused); - out.set_fgcolor(textmode::color::YELLOW); - let entry_count_width = format!("{}", entry_count + 1).len(); - let idx_str = format!("{}", idx + 1); - out.write_str(&" ".repeat(entry_count_width - idx_str.len())); - out.write_str(&idx_str); - out.write_str(" "); - out.reset_attributes(); - - set_bgcolor(out, idx, focused); - if let Some(info) = &self.exit_info { - if info.status.signal().is_some() { - out.set_fgcolor(textmode::color::MAGENTA); - } else if info.status.success() { - out.set_fgcolor(textmode::color::DARKGREY); - } else { - out.set_fgcolor(textmode::color::RED); - } - out.write_str(&crate::format::exit_status(info.status)); - } else { - out.write_str(" "); - } - out.reset_attributes(); - - set_bgcolor(out, idx, focused); - out.write_str("$ "); - if self.running() { - out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); - } - let cmd = self.cmd(); - let start = usize::from(out.screen().cursor_position().1); - let end = usize::from(width) - time.len() - 2; - let max_len = end - start; - if cmd.len() > max_len { - out.write_str(&cmd[..(max_len - 4)]); - out.set_fgcolor(textmode::color::BLUE); - out.write_str(" ..."); - } else { - out.write_str(cmd); - } - out.reset_attributes(); - - set_bgcolor(out, idx, focused); - let cur_pos = out.screen().cursor_position(); - out.write_str(&" ".repeat( - usize::from(width) - time.len() - 1 - usize::from(cur_pos.1), - )); - out.write_str(&time); - out.write_str(" "); - out.reset_attributes(); - - if self.binary() { - let msg = "This appears to be binary data. Fullscreen this entry to view anyway."; - let len: u16 = msg.len().try_into().unwrap(); - out.move_to( - out.screen().cursor_position().0 + 1, - (width - len) / 2, - ); - out.set_fgcolor(textmode::color::RED); - out.write_str(msg); - out.hide_cursor(true); - } else { - let last_row = self.output_lines(width, focused && !scrolling); - if last_row > 5 { - out.write(b"\r\n"); - out.set_fgcolor(textmode::color::BLUE); - out.write_str("..."); - out.reset_attributes(); - } - let mut out_row = out.screen().cursor_position().0 + 1; - let screen = self.vt.screen(); - let pos = screen.cursor_position(); - let mut wrapped = false; - let mut cursor_found = None; - for (idx, row) in screen - .rows_formatted(0, width) - .enumerate() - .take(last_row) - .skip(last_row.saturating_sub(5)) - { - let idx: u16 = idx.try_into().unwrap(); - out.reset_attributes(); - if !wrapped { - out.move_to(out_row, 0); - } - out.write(&row); - wrapped = screen.row_wrapped(idx); - if pos.0 == idx { - cursor_found = Some(out_row); - } - out_row += 1; - } - if focused && !scrolling { - if let Some(row) = cursor_found { - out.hide_cursor(screen.hide_cursor()); - out.move_to(row, pos.1); - } else { - out.hide_cursor(true); - } - } - } - out.reset_attributes(); - } - - fn render_fullscreen(&mut self, out: &mut impl textmode::Textmode) { - let screen = self.vt.screen(); - let new_audible_bell_state = screen.audible_bell_count(); - let new_visual_bell_state = screen.visual_bell_count(); - - out.write(&screen.state_formatted()); - - if self.audible_bell_state != new_audible_bell_state { - out.write(b"\x07"); - self.audible_bell_state = new_audible_bell_state; - } - - if self.visual_bell_state != new_visual_bell_state { - out.write(b"\x1bg"); - self.visual_bell_state = new_visual_bell_state; - } - - out.reset_attributes(); - } - - pub async fn send_input(&self, bytes: Vec<u8>) { - if self.running() { - self.input.send(bytes).await.unwrap(); - } - } - - pub async fn resize(&self, size: (u16, u16)) { - if self.running() { - self.resize.send(size).await.unwrap(); - } - } - - pub fn cmd(&self) -> &str { - &self.cmdline - } - - pub fn env(&self) -> &crate::env::Env { - &self.env - } - - pub fn toggle_fullscreen(&mut self) { - if let Some(fullscreen) = self.fullscreen { - self.fullscreen = Some(!fullscreen); - } else { - self.fullscreen = Some(!self.vt.screen().alternate_screen()); - } - } - - pub fn set_fullscreen(&mut self, fullscreen: bool) { - self.fullscreen = Some(fullscreen); - } - - pub fn running(&self) -> bool { - self.exit_info.is_none() - } - - pub fn binary(&self) -> bool { - self.vt.screen().errors() > 5 - } - - pub fn lines(&self, width: u16, focused: bool) -> usize { - let lines = self.output_lines(width, focused); - 1 + std::cmp::min(6, lines) - } - - pub fn output_lines(&self, width: u16, focused: bool) -> usize { - if self.binary() { - return 1; - } - - let screen = self.vt.screen(); - let mut last_row = 0; - for (idx, row) in screen.rows(0, width).enumerate() { - if !row.is_empty() { - last_row = idx + 1; - } - } - if focused && self.running() { - last_row = std::cmp::max( - last_row, - usize::from(screen.cursor_position().0) + 1, - ); - } - last_row - } - - pub fn should_fullscreen(&self) -> bool { - self.fullscreen - .unwrap_or_else(|| self.vt.screen().alternate_screen()) - } - - async fn finish( - &mut self, - env: crate::env::Env, - event_w: async_std::channel::Sender<crate::event::Event>, - ) { - self.exit_info = Some(ExitInfo::new(*env.latest_status())); - self.env = env; - event_w.send(crate::event::Event::PtyClose).await.unwrap(); - } -} - -struct ExitInfo { - status: async_std::process::ExitStatus, - instant: std::time::Instant, -} - -impl ExitInfo { - fn new(status: async_std::process::ExitStatus) -> Self { - Self { - status, - instant: std::time::Instant::now(), - } - } -} - fn run_commands( ast: crate::parse::Commands, entry: async_std::sync::Arc<async_std::sync::Mutex<Entry>>, @@ -562,7 +279,7 @@ fn run_commands( ) { async_std::task::spawn(async move { let pty = match pty::Pty::new( - entry.lock_arc().await.vt.screen().size(), + entry.lock_arc().await.size(), &entry, input_r, resize_r, @@ -571,7 +288,7 @@ fn run_commands( Ok(pty) => pty, Err(e) => { let mut entry = entry.lock_arc().await; - entry.vt.process( + entry.process( format!("nbsh: failed to allocate pty: {}\r\n", e) .as_bytes(), ); @@ -596,7 +313,6 @@ fn run_commands( entry .lock_arc() .await - .vt .process(format!("nbsh: {}\r\n", e).as_bytes()); env.set_status(async_std::process::ExitStatus::from_raw( 1 << 8, @@ -696,13 +412,3 @@ async fn run_pipeline( } } } - -fn set_bgcolor(out: &mut impl textmode::Textmode, idx: usize, focus: bool) { - if focus { - out.set_bgcolor(textmode::Color::Rgb(0x56, 0x1b, 0x8b)); - } else if idx % 2 == 0 { - out.set_bgcolor(textmode::Color::Rgb(0x24, 0x21, 0x00)); - } else { - out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20)); - } -} diff --git a/src/state/history/pty.rs b/src/state/history/pty.rs index 4df5358..96be550 100644 --- a/src/state/history/pty.rs +++ b/src/state/history/pty.rs @@ -67,7 +67,7 @@ async fn pty_task( match read.race(write).race(resize).or(close).await { Res::Read(res) => match res { Ok(bytes) => { - entry.lock_arc().await.vt.process(&buf[..bytes]); + entry.lock_arc().await.process(&buf[..bytes]); event_w .send(crate::event::Event::PtyOutput) .await @@ -91,7 +91,6 @@ async fn pty_task( Ok(size) => { pty.resize(pty_process::Size::new(size.0, size.1)) .unwrap(); - entry.lock_arc().await.vt.set_size(size.0, size.1); } Err(e) => { panic!("failed to read from resize channel: {}", e); |