summaryrefslogtreecommitdiffstats
path: root/src/state/history/entry.rs
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2022-01-05 06:22:35 -0500
committerJesse Luehrs <doy@tozt.net>2022-01-05 06:22:35 -0500
commit53193c426d19d529382e75e8af4ab9266b992557 (patch)
tree225eb74a24f1a46a5dbf1ac445a49d717fe930ba /src/state/history/entry.rs
parentf97c276722fda4fe12d81041706ea5c7753eb0e2 (diff)
downloadnbsh-53193c426d19d529382e75e8af4ab9266b992557.tar.gz
nbsh-53193c426d19d529382e75e8af4ab9266b992557.zip
split out more
Diffstat (limited to 'src/state/history/entry.rs')
-rw-r--r--src/state/history/entry.rs305
1 files changed, 305 insertions, 0 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));
+ }
+}