mirror of
https://github.com/ClementTsang/bottom.git
synced 2026-09-29 22:15:58 +00:00
514 lines
20 KiB
Rust
514 lines
20 KiB
Rust
//! Code related to drawing.
|
|
//!
|
|
//! Note that eventually this should not contain any widget-specific draw code,
|
|
//! but rather just generic code or components.
|
|
|
|
pub mod components;
|
|
pub mod dialogs;
|
|
mod drawing_utils;
|
|
mod widgets;
|
|
|
|
use ratatui::{
|
|
Frame, Terminal,
|
|
backend::Backend,
|
|
layout::{Constraint, Direction, Flex, Layout, Position, Rect},
|
|
style::Style,
|
|
symbols::Marker,
|
|
text::Span,
|
|
widgets::Paragraph,
|
|
};
|
|
|
|
use crate::{
|
|
app::{
|
|
App,
|
|
layout_manager::{BottomColRow, BottomLayout, BottomWidgetType},
|
|
},
|
|
constants::*,
|
|
options::config::style::Styles,
|
|
};
|
|
|
|
/// Handles the canvas' state.
|
|
pub struct Painter {
|
|
pub styles: Styles,
|
|
|
|
/// Used to know whether to invalidate things.
|
|
previous_height: u16,
|
|
|
|
/// Used to know whether to invalidate things.
|
|
previous_width: u16,
|
|
|
|
/// The layout.
|
|
layout: BottomLayout,
|
|
}
|
|
|
|
impl Painter {
|
|
pub fn init(layout: BottomLayout, styling: Styles) -> anyhow::Result<Self> {
|
|
let painter = Painter {
|
|
styles: styling,
|
|
previous_height: 0,
|
|
previous_width: 0,
|
|
layout,
|
|
};
|
|
|
|
Ok(painter)
|
|
}
|
|
|
|
/// Determines the border style.
|
|
pub fn get_border_style(&self, widget_id: u64, selected_widget_id: u64) -> Style {
|
|
let is_on_widget = widget_id == selected_widget_id;
|
|
if is_on_widget {
|
|
self.styles.highlighted_border_style
|
|
} else {
|
|
self.styles.border_style
|
|
}
|
|
}
|
|
|
|
pub(crate) fn get_marker(&self, use_dot: bool) -> Marker {
|
|
if use_dot {
|
|
Marker::Dot
|
|
} else {
|
|
Marker::Braille
|
|
}
|
|
}
|
|
|
|
fn draw_frozen_indicator(&self, f: &mut Frame<'_>, draw_loc: Rect) {
|
|
f.render_widget(
|
|
Paragraph::new(Span::styled(
|
|
"Frozen, press 'f' to unfreeze",
|
|
self.styles.selected_text_style,
|
|
)),
|
|
Layout::default()
|
|
.horizontal_margin(1)
|
|
.constraints([Constraint::Length(1)])
|
|
.split(draw_loc)[0],
|
|
)
|
|
}
|
|
|
|
pub fn draw_data<B: Backend>(
|
|
&mut self, terminal: &mut Terminal<B>, app_state: &mut App,
|
|
) -> Result<(), B::Error> {
|
|
use BottomWidgetType::*;
|
|
|
|
terminal.draw(|f| {
|
|
let (terminal_size, frozen_draw_loc) = if app_state.data_store.is_frozen() {
|
|
// TODO: Remove built-in cache?
|
|
let split_loc = Layout::default()
|
|
.constraints([Constraint::Min(0), Constraint::Length(1)])
|
|
.split(f.area());
|
|
(split_loc[0], Some(split_loc[1]))
|
|
} else {
|
|
(f.area(), None)
|
|
};
|
|
let terminal_height = terminal_size.height;
|
|
let terminal_width = terminal_size.width;
|
|
|
|
if (self.previous_height == 0 && self.previous_width == 0)
|
|
|| (self.previous_height != terminal_height
|
|
|| self.previous_width != terminal_width)
|
|
{
|
|
app_state.is_force_redraw = true;
|
|
self.previous_height = terminal_height;
|
|
self.previous_width = terminal_width;
|
|
}
|
|
|
|
// TODO: We should probably remove this or make it done elsewhere,
|
|
// not the responsibility of the app.
|
|
if app_state.should_get_widget_bounds() {
|
|
// If we're force drawing, reset ALL mouse boundaries.
|
|
for widget in app_state.widget_map.values_mut() {
|
|
widget.top_left_corner = None;
|
|
widget.bottom_right_corner = None;
|
|
}
|
|
|
|
// Reset process kill dialog button locations...
|
|
app_state.process_kill_dialog.handle_redraw();
|
|
|
|
// Reset battery dialog button locations...
|
|
for battery_widget in app_state.states.battery_state.widget_states.values_mut() {
|
|
battery_widget.tab_click_locs = None;
|
|
}
|
|
}
|
|
|
|
// TODO: Make drawing dialog generic.
|
|
if app_state.help_dialog_state.is_showing_help {
|
|
let area = f.area();
|
|
f.buffer_mut()
|
|
.set_style(area, self.styles.general_widget_style);
|
|
|
|
let gen_help_len = GENERAL_HELP_TEXT.len() as u16 + 3;
|
|
let border_len = terminal_height.saturating_sub(gen_help_len) / 2;
|
|
let [_, vertical_dialog_chunk, _] = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([
|
|
Constraint::Length(border_len),
|
|
Constraint::Length(gen_help_len),
|
|
Constraint::Length(border_len),
|
|
])
|
|
.areas(terminal_size);
|
|
|
|
// An approximate proxy for the max line length to use.
|
|
const MAX_TEXT_LENGTH: u16 = const {
|
|
let mut max = 0;
|
|
|
|
let mut i = 0;
|
|
while i < HELP_TEXT.len() {
|
|
let section = HELP_TEXT[i];
|
|
let mut j = 0;
|
|
while j < section.len() {
|
|
let line = section[j];
|
|
if line.len() > max {
|
|
max = line.len();
|
|
}
|
|
|
|
j += 1;
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
|
|
max as u16
|
|
};
|
|
|
|
let dialog_width = vertical_dialog_chunk.width;
|
|
let [middle_dialog_chunk] = if dialog_width < MAX_TEXT_LENGTH {
|
|
Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints([Constraint::Percentage(100)])
|
|
.areas(vertical_dialog_chunk)
|
|
} else {
|
|
// We calculate this so that the margins never have to split
|
|
// an odd number.
|
|
let len = if (dialog_width.saturating_sub(MAX_TEXT_LENGTH)) % 2 == 0 {
|
|
MAX_TEXT_LENGTH
|
|
} else {
|
|
// It can only be 1 if the difference is greater than 1,
|
|
// so this is fine.
|
|
MAX_TEXT_LENGTH + 1
|
|
};
|
|
|
|
Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints([Constraint::Length(len)])
|
|
.flex(Flex::SpaceAround)
|
|
.areas(vertical_dialog_chunk)
|
|
};
|
|
|
|
self.draw_help_dialog(f, app_state, middle_dialog_chunk);
|
|
} else if app_state.process_kill_dialog.is_open() {
|
|
let area = f.area();
|
|
f.buffer_mut()
|
|
.set_style(area, self.styles.general_widget_style);
|
|
|
|
// FIXME: For width, just limit to a max size or full width. For
|
|
// height, not sure. Maybe pass max and let
|
|
// child handle?
|
|
let horizontal_padding = if terminal_width < 100 { 0 } else { 5 };
|
|
let vertical_padding = if terminal_height < 100 { 0 } else { 5 };
|
|
|
|
let vertical_dialog_chunk = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([
|
|
Constraint::Length(vertical_padding),
|
|
Constraint::Fill(1),
|
|
Constraint::Length(vertical_padding),
|
|
])
|
|
.areas::<3>(terminal_size)[1];
|
|
|
|
let dialog_draw_area = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints([
|
|
Constraint::Length(horizontal_padding),
|
|
Constraint::Fill(1),
|
|
Constraint::Length(horizontal_padding),
|
|
])
|
|
.areas::<3>(vertical_dialog_chunk)[1];
|
|
|
|
app_state
|
|
.process_kill_dialog
|
|
.draw(f, dialog_draw_area, &self.styles);
|
|
} else if app_state.is_expanded {
|
|
if let Some(frozen_draw_loc) = frozen_draw_loc {
|
|
self.draw_frozen_indicator(f, frozen_draw_loc);
|
|
}
|
|
|
|
let rect = Layout::default()
|
|
.margin(0)
|
|
.constraints([Constraint::Percentage(100)])
|
|
.split(terminal_size);
|
|
match &app_state.current_widget.widget_type {
|
|
Cpu => self.draw_cpu(f, app_state, rect[0], app_state.current_widget.widget_id),
|
|
CpuLegend => self.draw_cpu(
|
|
f,
|
|
app_state,
|
|
rect[0],
|
|
app_state.current_widget.widget_id - 1,
|
|
),
|
|
Mem | BasicMem => self.draw_memory_graph(
|
|
f,
|
|
app_state,
|
|
rect[0],
|
|
app_state.current_widget.widget_id,
|
|
),
|
|
Disk => self.draw_disk_table(
|
|
f,
|
|
app_state,
|
|
rect[0],
|
|
app_state.current_widget.widget_id,
|
|
),
|
|
Temp => self.draw_temp_table(
|
|
f,
|
|
app_state,
|
|
rect[0],
|
|
app_state.current_widget.widget_id,
|
|
),
|
|
Net => {
|
|
self.draw_network(f, app_state, rect[0], app_state.current_widget.widget_id)
|
|
}
|
|
Proc | ProcSearch | ProcSort => {
|
|
let widget_id = app_state.current_widget.widget_id
|
|
- match &app_state.current_widget.widget_type {
|
|
ProcSearch => 1,
|
|
ProcSort => 2,
|
|
_ => 0,
|
|
};
|
|
|
|
self.draw_process(f, app_state, rect[0], widget_id);
|
|
}
|
|
Battery =>
|
|
{
|
|
#[cfg(feature = "battery")]
|
|
self.draw_battery(f, app_state, rect[0], app_state.current_widget.widget_id)
|
|
}
|
|
TempGraph => self.draw_temperature_graph(
|
|
f,
|
|
app_state,
|
|
rect[0],
|
|
app_state.current_widget.widget_id,
|
|
),
|
|
DiskIoGraph => self.draw_disk_io_graph(
|
|
f,
|
|
app_state,
|
|
rect[0],
|
|
app_state.current_widget.widget_id,
|
|
),
|
|
_ => {}
|
|
}
|
|
} else if app_state.app_config_fields.use_basic_mode {
|
|
// Basic mode. This basically removes all graphs but otherwise
|
|
// the same info.
|
|
if let Some(frozen_draw_loc) = frozen_draw_loc {
|
|
self.draw_frozen_indicator(f, frozen_draw_loc);
|
|
}
|
|
|
|
let data = app_state.data_store.get_data();
|
|
let actual_cpu_data_len = data.cpu_harvest.len();
|
|
|
|
// This fixes #397, apparently if the height is 1, it can't
|
|
// render the CPU bars...
|
|
let cpu_height = {
|
|
let c = (actual_cpu_data_len / 4) as u16
|
|
+ u16::from(!actual_cpu_data_len.is_multiple_of(4))
|
|
+ u16::from(
|
|
app_state.app_config_fields.show_average_cpu
|
|
&& app_state.app_config_fields.dedicated_average_row
|
|
&& !actual_cpu_data_len.saturating_sub(1).is_multiple_of(4),
|
|
);
|
|
|
|
if c <= 1 { 1 } else { c }
|
|
};
|
|
|
|
let mut mem_rows = 1;
|
|
|
|
if data.swap_harvest.is_some() {
|
|
mem_rows += 1; // add row for swap
|
|
}
|
|
|
|
#[cfg(feature = "zfs")]
|
|
{
|
|
if data.arc_harvest.is_some() {
|
|
mem_rows += 1; // add row for arc
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
if data.cache_harvest.is_some() {
|
|
mem_rows += 1;
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "gpu")]
|
|
{
|
|
mem_rows += data.gpu_harvest.len() as u16; // add row(s) for gpu
|
|
}
|
|
|
|
let network_rows = if app_state.app_config_fields.network_show_packets {
|
|
4 // 4 rows for RX/TX and Packet Rates (Avg sizes moved to right side)
|
|
} else {
|
|
2 // 2 rows for RX and TX
|
|
};
|
|
|
|
if mem_rows < network_rows {
|
|
mem_rows += network_rows - mem_rows; // min rows
|
|
}
|
|
|
|
let vertical_chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.margin(0)
|
|
.constraints([
|
|
Constraint::Length(cpu_height),
|
|
Constraint::Length(mem_rows),
|
|
Constraint::Length(2),
|
|
Constraint::Min(5),
|
|
])
|
|
.split(terminal_size);
|
|
|
|
let middle_chunks = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
|
.split(vertical_chunks[1]);
|
|
|
|
if vertical_chunks[0].width >= 2 {
|
|
self.draw_basic_cpu(f, app_state, vertical_chunks[0], 1);
|
|
}
|
|
if middle_chunks[0].width >= 2 {
|
|
self.draw_basic_memory(f, app_state, middle_chunks[0], 2);
|
|
}
|
|
if middle_chunks[1].width >= 2 {
|
|
self.draw_basic_network(f, app_state, middle_chunks[1], 3);
|
|
}
|
|
|
|
let mut later_widget_id: Option<u64> = None;
|
|
if let Some(basic_table_widget_state) = &app_state.states.basic_table_widget_state {
|
|
let widget_id = basic_table_widget_state.currently_displayed_widget_id;
|
|
later_widget_id = Some(widget_id);
|
|
if vertical_chunks[3].width >= 2 {
|
|
match basic_table_widget_state.currently_displayed_widget_type {
|
|
Disk => {
|
|
self.draw_disk_table(f, app_state, vertical_chunks[3], widget_id)
|
|
}
|
|
Proc | ProcSort => {
|
|
let wid = widget_id
|
|
- match basic_table_widget_state.currently_displayed_widget_type
|
|
{
|
|
ProcSearch => 1,
|
|
ProcSort => 2,
|
|
_ => 0,
|
|
};
|
|
self.draw_process(f, app_state, vertical_chunks[3], wid);
|
|
}
|
|
Temp => {
|
|
self.draw_temp_table(f, app_state, vertical_chunks[3], widget_id)
|
|
}
|
|
Battery =>
|
|
{
|
|
#[cfg(feature = "battery")]
|
|
self.draw_battery(f, app_state, vertical_chunks[3], widget_id)
|
|
}
|
|
TempGraph => self.draw_temperature_graph(
|
|
f,
|
|
app_state,
|
|
vertical_chunks[3],
|
|
widget_id,
|
|
),
|
|
DiskIoGraph => {
|
|
self.draw_disk_io_graph(f, app_state, vertical_chunks[3], widget_id)
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(widget_id) = later_widget_id {
|
|
self.draw_basic_table_arrows(f, app_state, vertical_chunks[2], widget_id);
|
|
}
|
|
} else {
|
|
// Draws using the passed in (or default) layout.
|
|
if let Some(frozen_draw_loc) = frozen_draw_loc {
|
|
self.draw_frozen_indicator(f, frozen_draw_loc);
|
|
}
|
|
|
|
// A two-pass algorithm - get layouts using constraints (first
|
|
// pass), then pass each layout to the
|
|
// corresponding widget (second pass). Note that
|
|
// layouts are already cached in ratatui, so we don't need
|
|
// to do it manually!
|
|
let base = Layout::vertical(self.layout.rows.iter().map(|r| r.constraint))
|
|
.split(terminal_size);
|
|
|
|
for (br, base) in self.layout.rows.iter().zip(base.iter()) {
|
|
let base =
|
|
Layout::horizontal(br.children.iter().map(|bc| bc.constraint)).split(*base);
|
|
|
|
for (bc, base) in br.children.iter().zip(base.iter()) {
|
|
let base = Layout::vertical(bc.children.iter().map(|bcr| bcr.constraint))
|
|
.split(*base);
|
|
|
|
for (widgets, base) in bc.children.iter().zip(base.iter()) {
|
|
let widget_draw_locs =
|
|
Layout::horizontal(widgets.children.iter().map(|bw| bw.constraint))
|
|
.split(*base);
|
|
|
|
self.draw_widgets_with_constraints(
|
|
f,
|
|
app_state,
|
|
widgets,
|
|
&widget_draw_locs,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})?;
|
|
|
|
// We also move it back to the origin to try rand avoid wasting CPU
|
|
// cycles for things like kitty's `cursor_trail` calculations.
|
|
let backend = terminal.backend_mut();
|
|
backend.set_cursor_position(Position::ORIGIN)?;
|
|
backend.flush()?;
|
|
|
|
if let Some(updated_current_widget) = app_state
|
|
.widget_map
|
|
.get(&app_state.current_widget.widget_id)
|
|
{
|
|
app_state.current_widget = updated_current_widget.clone();
|
|
}
|
|
|
|
app_state.is_force_redraw = false;
|
|
app_state.is_determining_widget_boundary = false;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn draw_widgets_with_constraints(
|
|
&self, f: &mut Frame<'_>, app_state: &mut App, widgets: &BottomColRow,
|
|
widget_draw_locs: &[Rect],
|
|
) {
|
|
use BottomWidgetType::*;
|
|
for (widget, draw_loc) in widgets.children.iter().zip(widget_draw_locs) {
|
|
if draw_loc.width >= 2 && draw_loc.height >= 2 {
|
|
match &widget.widget_type {
|
|
Cpu => self.draw_cpu(f, app_state, *draw_loc, widget.widget_id),
|
|
Mem => self.draw_memory_graph(f, app_state, *draw_loc, widget.widget_id),
|
|
Net => self.draw_network(f, app_state, *draw_loc, widget.widget_id),
|
|
Temp => self.draw_temp_table(f, app_state, *draw_loc, widget.widget_id),
|
|
Disk => self.draw_disk_table(f, app_state, *draw_loc, widget.widget_id),
|
|
Proc => self.draw_process(f, app_state, *draw_loc, widget.widget_id),
|
|
Battery =>
|
|
{
|
|
#[cfg(feature = "battery")]
|
|
self.draw_battery(f, app_state, *draw_loc, widget.widget_id)
|
|
}
|
|
TempGraph => {
|
|
self.draw_temperature_graph(f, app_state, *draw_loc, widget.widget_id)
|
|
}
|
|
DiskIoGraph => {
|
|
self.draw_disk_io_graph(f, app_state, *draw_loc, widget.widget_id)
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|