diff --git a/CHANGELOG.md b/CHANGELOG.md index a3afd99e..7e6060a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#690](https://github.com/ClementTsang/bottom/pull/690): Add some colour to `-h`/`--help` as part of updating to clap 3.0. - [#807](https://github.com/ClementTsang/bottom/pull/807): Add more human friendly temperature sensor names for Linux. +- [#845](https://github.com/ClementTsang/bottom/pull/845), [#922](https://github.com/ClementTsang/bottom/pull/922): Add macOS M1, FreeBSD 12, and FreeBSD 13 binary build tasks. +- [#916](https://github.com/ClementTsang/bottom/pull/916), [#937](https://github.com/ClementTsang/bottom/pull/937): Improve CPU usage by optimizing draw logic of charts and tables. ## Features @@ -34,7 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#870](https://github.com/ClementTsang/bottom/pull/870): Make disk widget sortable. - [#881](https://github.com/ClementTsang/bottom/pull/881): Add pasting to the search bar. - [#892](https://github.com/ClementTsang/bottom/pull/892): Add custom retention periods for data. -- [#899](https://github.com/ClementTsang/bottom/pull/899): Add non-normalized CPU usage to processes. +- [#899](https://github.com/ClementTsang/bottom/pull/899), [#910](https://github.com/ClementTsang/bottom/pull/910), [#912](https://github.com/ClementTsang/bottom/pull/912): Add non-normalized CPU usage to processes. - [#919](https://github.com/ClementTsang/bottom/pull/919): Add an option to expand the default widget on startup. ## [0.6.8] - 2022-02-01 diff --git a/Cargo.toml b/Cargo.toml index 27b01edf..da6993ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,9 +128,21 @@ clap_mangen = "0.1.6" [package.metadata.deb] section = "utility" assets = [ - ["target/release/btm", "usr/bin/", "755"], - ["LICENSE", "usr/share/doc/btm/", "644"], - ["manpage/btm.1.gz", "usr/share/man/man1/btm.1.gz", "644"], + [ + "target/release/btm", + "usr/bin/", + "755", + ], + [ + "LICENSE", + "usr/share/doc/btm/", + "644", + ], + [ + "manpage/btm.1.gz", + "usr/share/man/man1/btm.1.gz", + "644", + ], [ "completion/btm.bash", "usr/share/bash-completion/completions/btm", @@ -141,7 +153,11 @@ assets = [ "usr/share/fish/vendor_completions.d/btm.fish", "644", ], - ["completion/_btm", "usr/share/zsh/vendor-completions/", "644"], + [ + "completion/_btm", + "usr/share/zsh/vendor-completions/", + "644", + ], ] extended-description = """ diff --git a/src/components/tui_widget/time_chart.rs b/src/components/tui_widget/time_chart.rs index 605d13f8..c0aed91e 100644 --- a/src/components/tui_widget/time_chart.rs +++ b/src/components/tui_widget/time_chart.rs @@ -1,5 +1,8 @@ +mod canvas; + use std::{borrow::Cow, cmp::max}; +use canvas::*; use tui::{ buffer::Buffer, layout::{Constraint, Rect}, @@ -7,7 +10,7 @@ use tui::{ symbols::{self, Marker}, text::{Span, Spans}, widgets::{ - canvas::{Canvas, Line, Points}, + canvas::{Line, Points}, Block, Borders, GraphType, Widget, }, }; @@ -428,6 +431,13 @@ impl<'a> Widget for TimeChart<'a> { .y_bounds(self.y_axis.bounds) .marker(self.marker) .paint(|ctx| { + // Idea is to: + // - Go over all datasets, determine *where* a point will be drawn. + // - We take the topmost (last) point first. + // - After we determine all points, then we paint them all. + // This helps relieve the issue where normally, braille grids are painted via |=, when we want + // an exclusive replacement. + for dataset in &self.datasets { let color = dataset.style.fg.unwrap_or(Color::Reset); diff --git a/src/components/tui_widget/time_chart/canvas.rs b/src/components/tui_widget/time_chart/canvas.rs new file mode 100644 index 00000000..a864d89e --- /dev/null +++ b/src/components/tui_widget/time_chart/canvas.rs @@ -0,0 +1,548 @@ +//! Vendored from https://github.com/fdehau/tui-rs/blob/fafad6c96109610825aad89c4bba5253e01101ed/src/widgets/canvas/mod.rs. +//! Main difference is in the Braille rendering, which can now effectively be done in a single layer without the effects +//! of doing it all in a single layer via the normal tui-rs crate. This means you can do it all in a single pass, with +//! just one string alloc and no resets. + +use std::fmt::Debug; + +use tui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Style}, + symbols, + text::Spans, + widgets::{ + canvas::{Line, Points}, + Block, Widget, + }, +}; + +/// Interface for all shapes that may be drawn on a Canvas widget. +pub trait Shape { + fn draw(&self, painter: &mut Painter<'_, '_>); +} + +impl Shape for Line { + fn draw(&self, painter: &mut Painter<'_, '_>) { + let (x1, y1) = match painter.get_point(self.x1, self.y1) { + Some(c) => c, + None => return, + }; + let (x2, y2) = match painter.get_point(self.x2, self.y2) { + Some(c) => c, + None => return, + }; + let (dx, x_range) = if x2 >= x1 { + (x2 - x1, x1..=x2) + } else { + (x1 - x2, x2..=x1) + }; + let (dy, y_range) = if y2 >= y1 { + (y2 - y1, y1..=y2) + } else { + (y1 - y2, y2..=y1) + }; + + if dx == 0 { + for y in y_range { + painter.paint(x1, y, self.color); + } + } else if dy == 0 { + for x in x_range { + painter.paint(x, y1, self.color); + } + } else if dy < dx { + if x1 > x2 { + draw_line_low(painter, x2, y2, x1, y1, self.color); + } else { + draw_line_low(painter, x1, y1, x2, y2, self.color); + } + } else if y1 > y2 { + draw_line_high(painter, x2, y2, x1, y1, self.color); + } else { + draw_line_high(painter, x1, y1, x2, y2, self.color); + } + } +} + +fn draw_line_low( + painter: &mut Painter<'_, '_>, x1: usize, y1: usize, x2: usize, y2: usize, color: Color, +) { + let dx = (x2 - x1) as isize; + let dy = (y2 as isize - y1 as isize).abs(); + let mut d = 2 * dy - dx; + let mut y = y1; + for x in x1..=x2 { + painter.paint(x, y, color); + if d > 0 { + y = if y1 > y2 { + y.saturating_sub(1) + } else { + y.saturating_add(1) + }; + d -= 2 * dx; + } + d += 2 * dy; + } +} + +fn draw_line_high( + painter: &mut Painter<'_, '_>, x1: usize, y1: usize, x2: usize, y2: usize, color: Color, +) { + let dx = (x2 as isize - x1 as isize).abs(); + let dy = (y2 - y1) as isize; + let mut d = 2 * dx - dy; + let mut x = x1; + for y in y1..=y2 { + painter.paint(x, y, color); + if d > 0 { + x = if x1 > x2 { + x.saturating_sub(1) + } else { + x.saturating_add(1) + }; + d -= 2 * dy; + } + d += 2 * dx; + } +} + +impl Shape for Points<'_> { + fn draw(&self, painter: &mut Painter<'_, '_>) { + for (x, y) in self.coords { + if let Some((x, y)) = painter.get_point(*x, *y) { + painter.paint(x, y, self.color); + } + } + } +} + +/// Label to draw some text on the canvas +#[derive(Debug, Clone)] +pub struct Label<'a> { + x: f64, + y: f64, + spans: Spans<'a>, +} + +#[derive(Debug, Clone)] +struct Layer { + string: String, + colors: Vec, +} + +trait Grid: Debug { + fn width(&self) -> u16; + fn height(&self) -> u16; + fn resolution(&self) -> (f64, f64); + fn paint(&mut self, x: usize, y: usize, color: Color); + fn save(&self) -> Layer; + fn reset(&mut self); +} + +#[derive(Debug, Clone)] +struct BrailleGrid { + width: u16, + height: u16, + cells: Vec, + colors: Vec, +} + +impl BrailleGrid { + fn new(width: u16, height: u16) -> BrailleGrid { + let length = usize::from(width * height); + BrailleGrid { + width, + height, + cells: vec![symbols::braille::BLANK; length], + colors: vec![Color::Reset; length], + } + } +} + +impl Grid for BrailleGrid { + fn width(&self) -> u16 { + self.width + } + + fn height(&self) -> u16 { + self.height + } + + fn resolution(&self) -> (f64, f64) { + ( + f64::from(self.width) * 2.0 - 1.0, + f64::from(self.height) * 4.0 - 1.0, + ) + } + + fn save(&self) -> Layer { + Layer { + string: String::from_utf16(&self.cells).unwrap(), + colors: self.colors.clone(), + } + } + + fn reset(&mut self) { + for c in &mut self.cells { + *c = symbols::braille::BLANK; + } + for c in &mut self.colors { + *c = Color::Reset; + } + } + + fn paint(&mut self, x: usize, y: usize, color: Color) { + let index = y / 4 * self.width as usize + x / 2; + if let Some(curr_color) = self.colors.get_mut(index) { + if *curr_color != color { + *curr_color = color; + if let Some(cell) = self.cells.get_mut(index) { + *cell = symbols::braille::BLANK; + + *cell |= symbols::braille::DOTS[y % 4][x % 2]; + } + } else if let Some(c) = self.cells.get_mut(index) { + *c |= symbols::braille::DOTS[y % 4][x % 2]; + } + } + } +} + +#[derive(Debug, Clone)] +struct CharGrid { + width: u16, + height: u16, + cells: Vec, + colors: Vec, + cell_char: char, +} + +impl CharGrid { + fn new(width: u16, height: u16, cell_char: char) -> CharGrid { + let length = usize::from(width * height); + CharGrid { + width, + height, + cells: vec![' '; length], + colors: vec![Color::Reset; length], + cell_char, + } + } +} + +impl Grid for CharGrid { + fn width(&self) -> u16 { + self.width + } + + fn height(&self) -> u16 { + self.height + } + + fn resolution(&self) -> (f64, f64) { + (f64::from(self.width) - 1.0, f64::from(self.height) - 1.0) + } + + fn save(&self) -> Layer { + Layer { + string: self.cells.iter().collect(), + colors: self.colors.clone(), + } + } + + fn reset(&mut self) { + for c in &mut self.cells { + *c = ' '; + } + for c in &mut self.colors { + *c = Color::Reset; + } + } + + fn paint(&mut self, x: usize, y: usize, color: Color) { + let index = y * self.width as usize + x; + if let Some(c) = self.cells.get_mut(index) { + *c = self.cell_char; + } + if let Some(c) = self.colors.get_mut(index) { + *c = color; + } + } +} + +#[derive(Debug)] +pub struct Painter<'a, 'b> { + context: &'a mut Context<'b>, + resolution: (f64, f64), +} + +impl<'a, 'b> Painter<'a, 'b> { + /// Convert the (x, y) coordinates to location of a point on the grid + /// + /// # Examples: + /// ``` + /// use tui::{ + /// symbols, + /// widgets::canvas::{Context, Painter}, + /// }; + /// + /// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille); + /// let mut painter = Painter::from(&mut ctx); + /// let point = painter.get_point(1.0, 0.0); + /// assert_eq!(point, Some((0, 7))); + /// let point = painter.get_point(1.5, 1.0); + /// assert_eq!(point, Some((1, 3))); + /// let point = painter.get_point(0.0, 0.0); + /// assert_eq!(point, None); + /// let point = painter.get_point(2.0, 2.0); + /// assert_eq!(point, Some((3, 0))); + /// let point = painter.get_point(1.0, 2.0); + /// assert_eq!(point, Some((0, 0))); + /// ``` + pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> { + let left = self.context.x_bounds[0]; + let right = self.context.x_bounds[1]; + let top = self.context.y_bounds[1]; + let bottom = self.context.y_bounds[0]; + if x < left || x > right || y < bottom || y > top { + return None; + } + let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs(); + let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs(); + if width == 0.0 || height == 0.0 { + return None; + } + let x = ((x - left) * self.resolution.0 / width) as usize; + let y = ((top - y) * self.resolution.1 / height) as usize; + Some((x, y)) + } + + /// Paint a point of the grid + /// + /// # Examples: + /// ``` + /// use tui::{ + /// style::Color, + /// symbols, + /// widgets::canvas::{Context, Painter}, + /// }; + /// + /// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille); + /// let mut painter = Painter::from(&mut ctx); + /// let cell = painter.paint(1, 3, Color::Red); + /// ``` + pub fn paint(&mut self, x: usize, y: usize, color: Color) { + self.context.grid.paint(x, y, color); + } +} + +impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> { + fn from(context: &'a mut Context<'b>) -> Painter<'a, 'b> { + let resolution = context.grid.resolution(); + Painter { + context, + resolution, + } + } +} + +/// Holds the state of the Canvas when painting to it. +#[derive(Debug)] +pub struct Context<'a> { + x_bounds: [f64; 2], + y_bounds: [f64; 2], + grid: Box, + dirty: bool, + labels: Vec>, +} + +impl<'a> Context<'a> { + pub fn new( + width: u16, height: u16, x_bounds: [f64; 2], y_bounds: [f64; 2], marker: symbols::Marker, + ) -> Context<'a> { + let grid: Box = match marker { + symbols::Marker::Dot => Box::new(CharGrid::new(width, height, '•')), + symbols::Marker::Block => Box::new(CharGrid::new(width, height, '▄')), + symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)), + }; + Context { + x_bounds, + y_bounds, + grid, + dirty: false, + labels: Vec::new(), + } + } + + /// Draw any object that may implement the Shape trait + pub fn draw(&mut self, shape: &S) + where + S: Shape, + { + self.dirty = true; + let mut painter = Painter::from(self); + shape.draw(&mut painter); + } +} + +/// The Canvas widget may be used to draw more detailed figures using braille patterns (each +/// cell can have a braille character in 8 different positions). +pub struct Canvas<'a, F> +where + F: Fn(&mut Context<'_>), +{ + block: Option>, + x_bounds: [f64; 2], + y_bounds: [f64; 2], + painter: Option, + background_color: Color, + marker: symbols::Marker, +} + +impl<'a, F> Default for Canvas<'a, F> +where + F: Fn(&mut Context<'_>), +{ + fn default() -> Canvas<'a, F> { + Canvas { + block: None, + x_bounds: [0.0, 0.0], + y_bounds: [0.0, 0.0], + painter: None, + background_color: Color::Reset, + marker: symbols::Marker::Braille, + } + } +} + +impl<'a, F> Canvas<'a, F> +where + F: Fn(&mut Context<'_>), +{ + pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> { + self.x_bounds = bounds; + self + } + + pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> { + self.y_bounds = bounds; + self + } + + /// Store the closure that will be used to draw to the Canvas + pub fn paint(mut self, f: F) -> Canvas<'a, F> { + self.painter = Some(f); + self + } + + pub fn background_color(mut self, color: Color) -> Canvas<'a, F> { + self.background_color = color; + self + } + + /// Change the type of points used to draw the shapes. By default the braille patterns are used + /// as they provide a more fine grained result but you might want to use the simple dot or + /// block instead if the targeted terminal does not support those symbols. + /// + /// # Examples + /// + /// ``` + /// # use tui::widgets::canvas::Canvas; + /// # use tui::symbols; + /// Canvas::default() + /// .marker(symbols::Marker::Braille) + /// .paint(|ctx| {}); + /// + /// Canvas::default() + /// .marker(symbols::Marker::Dot) + /// .paint(|ctx| {}); + /// + /// Canvas::default() + /// .marker(symbols::Marker::Block) + /// .paint(|ctx| {}); + /// ``` + pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> { + self.marker = marker; + self + } +} + +impl<'a, F> Widget for Canvas<'a, F> +where + F: Fn(&mut Context<'_>), +{ + fn render(mut self, area: Rect, buf: &mut Buffer) { + let canvas_area = match self.block.take() { + Some(b) => { + let inner_area = b.inner(area); + b.render(area, buf); + inner_area + } + None => area, + }; + + buf.set_style(canvas_area, Style::default().bg(self.background_color)); + + let width = canvas_area.width as usize; + + let painter = match self.painter { + Some(ref p) => p, + None => return, + }; + + // Create a blank context that match the size of the canvas + let mut ctx = Context::new( + canvas_area.width, + canvas_area.height, + self.x_bounds, + self.y_bounds, + self.marker, + ); + // Paint to this context + painter(&mut ctx); + + // Paint whatever is in the ctx. + let layer = ctx.grid.save(); + + for (i, (ch, color)) in layer + .string + .chars() + .zip(layer.colors.into_iter()) + .enumerate() + { + if ch != ' ' && ch != '\u{2800}' { + let (x, y) = (i % width, i / width); + buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top()) + .set_char(ch) + .set_fg(color); + } + } + + // Reset the grid and mark as non-dirty. + ctx.grid.reset(); + ctx.dirty = false; + + // Finally draw the labels + let left = self.x_bounds[0]; + let right = self.x_bounds[1]; + let top = self.y_bounds[1]; + let bottom = self.y_bounds[0]; + let width = (self.x_bounds[1] - self.x_bounds[0]).abs(); + let height = (self.y_bounds[1] - self.y_bounds[0]).abs(); + let resolution = { + let width = f64::from(canvas_area.width - 1); + let height = f64::from(canvas_area.height - 1); + (width, height) + }; + for label in ctx + .labels + .iter() + .filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom) + { + let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left(); + let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top(); + buf.set_spans(x, y, &label.spans, canvas_area.right() - x); + } + } +} diff --git a/tests/util.rs b/tests/util.rs index 0e74eafa..f7dbebee 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -59,7 +59,6 @@ fn cross_runner() -> Option { } /// Returns the [`Command`] of a binary invocation of bottom. -/// pub fn btm_command() -> Command { let btm_exe = env!("CARGO_BIN_EXE_btm"); match cross_runner() {