From d961687b4fe67c4654306e805eb279fcea7f61e0 Mon Sep 17 00:00:00 2001 From: Luckas Date: Thu, 8 Aug 2024 12:37:11 +0300 Subject: [PATCH] perf: only highlight visible lines --- src/actors/command.rs | 15 ++++--- src/actors/console.rs | 100 +++++++++++++++++++++++++++--------------- src/tests.rs | 10 ++--- 3 files changed, 77 insertions(+), 48 deletions(-) diff --git a/src/actors/command.rs b/src/actors/command.rs index f2795b9..3a1b09c 100644 --- a/src/actors/command.rs +++ b/src/actors/command.rs @@ -25,7 +25,7 @@ use crate::config::{ }; use crate::exec::ExecBuilder; -use super::console::{Output, PanelStatus, RegisterPanel}; +use super::console::{Output, OutputKind, PanelStatus, RegisterPanel}; use super::watcher::{IgnorePath, WatchGlob}; #[cfg(not(test))] @@ -265,7 +265,8 @@ impl CommandActor { fn log_info(&self, log: String) { let job_name = self.operator.name.clone(); - self.console.do_send(Output::now(job_name, log, true)); + self.console + .do_send(Output::now(job_name, log, OutputKind::Service)); } fn log_debug(&self, log: String) { @@ -353,7 +354,11 @@ impl CommandActor { colors: task_colors.clone(), }); } - console.do_send(Output::now(tab_name.to_owned(), line.clone(), false)); + console.do_send(Output::now( + tab_name.to_owned(), + line, + OutputKind::Command, + )); } OutputRedirection::File(path) => { let path = task_pipe.regex.replace(&line, path); @@ -383,11 +388,11 @@ impl CommandActor { // append new line since strings from the buffer reader don't include it line.push('\n'); - file.write_all(line.clone().as_bytes()).unwrap(); + file.write_all(line.as_bytes()).unwrap(); } } } else { - console.do_send(Output::now(op_name.clone(), line.clone(), false)); + console.do_send(Output::now(op_name.clone(), line, OutputKind::Command)); } } diff --git a/src/actors/console.rs b/src/actors/console.rs index 49a6aaa..5aa91db 100644 --- a/src/actors/console.rs +++ b/src/actors/console.rs @@ -64,8 +64,8 @@ impl AppMode { } pub struct Panel { - logs: Vec<(String, Style)>, - lines: u16, + logs: Vec<(String, OutputKind)>, + line_offsets: Vec, shift: u16, command: Addr, status: Option, @@ -76,7 +76,7 @@ impl Panel { pub fn new(command: Addr, colors: Vec) -> Self { Self { logs: Vec::default(), - lines: 0, + line_offsets: Vec::default(), shift: 0, command, status: None, @@ -138,7 +138,8 @@ impl ConsoleActor { if let Some(focused_panel) = self.panels.get_mut(&self.index) { // maximum_scroll is the number of lines // overflowing in the current focused panel - let maximum_scroll = focused_panel.lines - min(focused_panel.lines, log_height); + let lines = focused_panel.line_offsets.len() as u16; + let maximum_scroll = lines - min(lines, log_height); // `focused_panel.shift` goes from 0 until maximum_scroll focused_panel.shift = min(focused_panel.shift + shift, maximum_scroll); @@ -201,23 +202,33 @@ impl ConsoleActor { .draw(|f| { let chunks = chunks(&self.mode, &self.layout_direction, f); let logs = &focused_panel.logs; - - let log_height = chunks[0].height; - let maximum_scroll = focused_panel.lines - min(focused_panel.lines, log_height); - - let lines: Vec = logs - .iter() - .flat_map(|(str, base_style)| { - let colorizer = Colorizer::new(&focused_panel.colors, *base_style); - colorizer.patch_text(str) - }) - .collect(); - - let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false }); + let shift = focused_panel.shift as usize; + let line_offsets = &focused_panel.line_offsets; + let lines = line_offsets.len(); + let log_height = chunks[0].height as usize; + + let maximum_scroll = lines - min(lines, log_height); + let scroll_offset = maximum_scroll - min(maximum_scroll, shift); + let offset_end = min(lines, scroll_offset + log_height).wrapping_sub(1); + + let start_line = line_offsets.get(scroll_offset).cloned().unwrap_or(0); + let end_line = line_offsets.get(offset_end).cloned().unwrap_or(0); + + let lines: Vec = + logs.iter() + .enumerate() + .flat_map(|(i, (str, kind))| match i >= start_line && i <= end_line { + true => Colorizer::new(&focused_panel.colors, kind.style()) + .patch_text(str), + false => vec![Line::from(str.as_str())], + }) + .collect(); // scroll by default until the last line - let paragraph = paragraph - .scroll((maximum_scroll - min(maximum_scroll, focused_panel.shift), 0)); + let paragraph = Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .scroll((scroll_offset as u16, 0)); + f.render_widget(paragraph, chunks[0]); //Format titles @@ -435,11 +446,13 @@ impl Handler for ConsoleActor { Event::Resize(width, _) => { for panel in self.panels.values_mut() { panel.shift = 0; - let new_lines = panel + let new_offsets = panel .logs .iter() - .fold(0, |agg, l| agg + wrapped_lines(&l.0, width)); - panel.lines = new_lines; + .enumerate() + .flat_map(|(i, l)| vec![i; wrapped_lines(&l.0, width)]) + .collect(); + panel.line_offsets = new_offsets; } } Event::Mouse(e) => match e.kind { @@ -457,29 +470,44 @@ impl Handler for ConsoleActor { } } +#[derive(Debug)] +pub enum OutputKind { + Service, + Command, +} + +impl OutputKind { + fn style(&self) -> Style { + match self { + OutputKind::Service => Style::default().bg(Color::DarkGray), + OutputKind::Command => Style::default(), + } + } +} + #[derive(Message)] #[rtype(result = "()")] pub struct Output { panel_name: String, pub message: String, - service: bool, + kind: OutputKind, timestamp: DateTime, } impl Output { - pub fn now(panel_name: String, message: String, service: bool) -> Self { + pub fn now(panel_name: String, message: String, kind: OutputKind) -> Self { Self { panel_name, message, - service, + kind, timestamp: Local::now(), } } } -fn wrapped_lines(message: &String, width: u16) -> u16 { +fn wrapped_lines(message: &String, width: u16) -> usize { let clean = strip_ansi_escapes::strip(message); - textwrap::wrap(str::from_utf8(&clean).unwrap(), width as usize).len() as u16 + textwrap::wrap(str::from_utf8(&clean).unwrap(), width as usize).len() } // Replace the character that are max that MAX_CHARS with an ellipse ... @@ -502,19 +530,19 @@ impl Handler for ConsoleActor { type Result = (); fn handle(&mut self, msg: Output, _: &mut Context) -> Self::Result { - let panel = self.panels.get_mut(&msg.panel_name).unwrap(); - let style = match msg.service { - true => Style::default().bg(Color::DarkGray), - false => Style::default(), - }; - let message = match self.timestamp { true => format_message(&msg.message, &msg.timestamp), false => msg.message, }; + + let panel = self.panels.get_mut(&msg.panel_name).unwrap(); let width = self.terminal.get_frame().size().width; - panel.lines += wrapped_lines(&message, width); - panel.logs.push((message, style)); + let line_count = wrapped_lines(&message, width); + let line_offset = panel.logs.len(); + + panel.line_offsets.extend(vec![line_offset; line_count]); + panel.logs.push((message, msg.kind)); + self.draw(); } } @@ -558,7 +586,7 @@ impl Handler for ConsoleActor { if let Some(message) = msg.status.map(|c| format!("Status: {:?}", c)) { ctx.address() - .do_send(Output::now(msg.panel_name, message, true)); + .do_send(Output::now(msg.panel_name, message, OutputKind::Service)); } self.draw(); diff --git a/src/tests.rs b/src/tests.rs index 2acad84..77a8d7c 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -7,7 +7,7 @@ use anyhow::{Ok, Result}; use subprocess::ExitStatus; use crate::actors::command::{CommandActorsBuilder, WaitStatus}; -use crate::actors::console::RegisterPanel; +use crate::actors::console::{OutputKind, RegisterPanel}; use crate::actors::watcher::WatchGlob; use crate::args::Args; use crate::config::{ConfigInner, RawConfig}; @@ -94,7 +94,7 @@ fn hello() { .send(Output::now( "test".to_string(), "message".to_string(), - false, + OutputKind::Command, )) .await?; @@ -102,11 +102,7 @@ fn hello() { .build() .await?; - let status = commands - .get("test") - .unwrap() - .send(WaitStatus) - .await?; + let status = commands.get("test").unwrap().send(WaitStatus).await?; println!("status: {:?}", status); Ok(())