mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-04-08 17:05:59 +02:00
Adds back some of the general program keybinds, and fixes both a bug causing widget movement via keybinds to be incorrect, and not correcting the last selected widget in the layout tree rows/cols after clicking/setting the default widget!
443 lines
18 KiB
Rust
443 lines
18 KiB
Rust
use std::{collections::HashMap, str::FromStr};
|
|
|
|
use fxhash::FxHashMap;
|
|
use indextree::{Arena, NodeId};
|
|
use tui::{
|
|
backend::Backend,
|
|
layout::{Constraint, Direction, Layout, Rect},
|
|
text::{Span, Spans},
|
|
widgets::Paragraph,
|
|
Frame, Terminal,
|
|
};
|
|
|
|
use canvas_colours::*;
|
|
use dialogs::*;
|
|
|
|
use crate::{
|
|
app::{
|
|
self,
|
|
layout_manager::{generate_layout, ColLayout, LayoutNode, RowLayout},
|
|
text_table::TextTableData,
|
|
widgets::{Component, Widget},
|
|
TmpBottomWidget,
|
|
},
|
|
constants::*,
|
|
data_conversion::{ConvertedBatteryData, ConvertedCpuData, ConvertedProcessData},
|
|
options::Config,
|
|
utils::error,
|
|
utils::error::BottomError,
|
|
Pid,
|
|
};
|
|
|
|
mod canvas_colours;
|
|
mod dialogs;
|
|
|
|
/// Point is of time, data
|
|
type Point = (f64, f64);
|
|
|
|
#[derive(Default)]
|
|
pub struct DisplayableData {
|
|
pub rx_display: String,
|
|
pub tx_display: String,
|
|
pub total_rx_display: String,
|
|
pub total_tx_display: String,
|
|
pub network_data_rx: Vec<Point>,
|
|
pub network_data_tx: Vec<Point>,
|
|
pub disk_data: TextTableData,
|
|
pub temp_sensor_data: TextTableData,
|
|
pub single_process_data: HashMap<Pid, ConvertedProcessData>, // Contains single process data, key is PID
|
|
pub stringified_process_data_map: HashMap<NodeId, Vec<(Vec<(String, Option<String>)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID
|
|
|
|
pub mem_labels: Option<(String, String)>,
|
|
pub swap_labels: Option<(String, String)>,
|
|
pub mem_data: Vec<Point>,
|
|
pub swap_data: Vec<Point>,
|
|
|
|
pub load_avg_data: [f32; 3],
|
|
pub cpu_data: Vec<ConvertedCpuData>,
|
|
pub battery_data: Vec<ConvertedBatteryData>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum ColourScheme {
|
|
Default,
|
|
DefaultLight,
|
|
Gruvbox,
|
|
GruvboxLight,
|
|
Nord,
|
|
NordLight,
|
|
Custom,
|
|
}
|
|
|
|
impl FromStr for ColourScheme {
|
|
type Err = BottomError;
|
|
|
|
fn from_str(s: &str) -> error::Result<Self> {
|
|
let lower_case = s.to_lowercase();
|
|
match lower_case.as_str() {
|
|
"default" => Ok(ColourScheme::Default),
|
|
"default-light" => Ok(ColourScheme::DefaultLight),
|
|
"gruvbox" => Ok(ColourScheme::Gruvbox),
|
|
"gruvbox-light" => Ok(ColourScheme::GruvboxLight),
|
|
"nord" => Ok(ColourScheme::Nord),
|
|
"nord-light" => Ok(ColourScheme::NordLight),
|
|
_ => Err(BottomError::ConfigError(format!(
|
|
"\"{}\" is an invalid built-in color scheme.",
|
|
s
|
|
))),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handles the canvas' state.
|
|
pub struct Painter {
|
|
pub colours: CanvasColours,
|
|
styled_help_text: Vec<Spans<'static>>,
|
|
}
|
|
|
|
impl Painter {
|
|
pub fn init(config: &Config, colour_scheme: ColourScheme) -> anyhow::Result<Self> {
|
|
let mut painter = Painter {
|
|
colours: CanvasColours::default(),
|
|
styled_help_text: Vec::default(),
|
|
};
|
|
|
|
if let ColourScheme::Custom = colour_scheme {
|
|
painter.generate_config_colours(config)?;
|
|
} else {
|
|
painter.generate_colour_scheme(colour_scheme)?;
|
|
}
|
|
painter.complete_painter_init();
|
|
|
|
Ok(painter)
|
|
}
|
|
|
|
fn generate_config_colours(&mut self, config: &Config) -> anyhow::Result<()> {
|
|
if let Some(colours) = &config.colors {
|
|
self.colours.set_colours_from_palette(colours)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_colour_scheme(&mut self, colour_scheme: ColourScheme) -> anyhow::Result<()> {
|
|
match colour_scheme {
|
|
ColourScheme::Default => {
|
|
// Don't have to do anything.
|
|
}
|
|
ColourScheme::DefaultLight => {
|
|
self.colours
|
|
.set_colours_from_palette(&*DEFAULT_LIGHT_MODE_COLOUR_PALETTE)?;
|
|
}
|
|
ColourScheme::Gruvbox => {
|
|
self.colours
|
|
.set_colours_from_palette(&*GRUVBOX_COLOUR_PALETTE)?;
|
|
}
|
|
ColourScheme::GruvboxLight => {
|
|
self.colours
|
|
.set_colours_from_palette(&*GRUVBOX_LIGHT_COLOUR_PALETTE)?;
|
|
}
|
|
ColourScheme::Nord => {
|
|
self.colours
|
|
.set_colours_from_palette(&*NORD_COLOUR_PALETTE)?;
|
|
}
|
|
ColourScheme::NordLight => {
|
|
self.colours
|
|
.set_colours_from_palette(&*NORD_LIGHT_COLOUR_PALETTE)?;
|
|
}
|
|
ColourScheme::Custom => {
|
|
// This case should never occur, just do nothing.
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Must be run once before drawing, but after setting colours.
|
|
/// This is to set some remaining styles and text.
|
|
fn complete_painter_init(&mut self) {
|
|
let mut styled_help_spans = Vec::new();
|
|
|
|
// Init help text:
|
|
(*HELP_TEXT).iter().enumerate().for_each(|(itx, section)| {
|
|
if itx == 0 {
|
|
styled_help_spans.extend(
|
|
section
|
|
.iter()
|
|
.map(|&text| Span::styled(text, self.colours.text_style))
|
|
.collect::<Vec<_>>(),
|
|
);
|
|
} else {
|
|
// Not required check but it runs only a few times... so whatever ig, prevents me from
|
|
// being dumb and leaving a help text section only one line long.
|
|
if section.len() > 1 {
|
|
styled_help_spans.push(Span::raw(""));
|
|
styled_help_spans
|
|
.push(Span::styled(section[0], self.colours.table_header_style));
|
|
styled_help_spans.extend(
|
|
section[1..]
|
|
.iter()
|
|
.map(|&text| Span::styled(text, self.colours.text_style))
|
|
.collect::<Vec<_>>(),
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
self.styled_help_text = styled_help_spans.into_iter().map(Spans::from).collect();
|
|
}
|
|
|
|
// TODO: [CONFIG] write this, should call painter init and any changed colour functions...
|
|
pub fn update_painter_colours(&mut self) {}
|
|
|
|
fn draw_frozen_indicator<B: Backend>(&self, f: &mut Frame<'_, B>, draw_loc: Rect) {
|
|
f.render_widget(
|
|
Paragraph::new(Span::styled(
|
|
"Frozen, press 'f' to unfreeze",
|
|
self.colours.currently_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::AppState,
|
|
) -> error::Result<()> {
|
|
terminal.draw(|mut f| {
|
|
let (draw_area, frozen_draw_loc) = if app_state.is_frozen() {
|
|
let split_loc = Layout::default()
|
|
.constraints([Constraint::Min(0), Constraint::Length(1)])
|
|
.split(f.size());
|
|
(split_loc[0], Some(split_loc[1]))
|
|
} else {
|
|
(f.size(), None)
|
|
};
|
|
let terminal_height = draw_area.height;
|
|
let terminal_width = draw_area.width;
|
|
|
|
if app_state.help_dialog_state.is_showing_help {
|
|
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),
|
|
])
|
|
.split(draw_area);
|
|
|
|
let middle_dialog_chunk = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(if terminal_width < 100 {
|
|
// TODO: [REFACTOR] The point we start changing size at currently hard-coded in.
|
|
[
|
|
Constraint::Percentage(0),
|
|
Constraint::Percentage(100),
|
|
Constraint::Percentage(0),
|
|
]
|
|
} else {
|
|
[
|
|
Constraint::Percentage(20),
|
|
Constraint::Percentage(60),
|
|
Constraint::Percentage(20),
|
|
]
|
|
})
|
|
.split(vertical_dialog_chunk[1]);
|
|
|
|
self.draw_help_dialog(&mut f, app_state, middle_dialog_chunk[1]);
|
|
} else if app_state.delete_dialog_state.is_showing_dd {
|
|
// TODO: This needs the paragraph wrap feature from tui-rs to be pushed to complete... but for now it's pretty close!
|
|
// The main problem right now is that I cannot properly calculate the height offset since
|
|
// line-wrapping is NOT the same as taking the width of the text and dividing by width.
|
|
// So, I need the height AFTER wrapping.
|
|
// See: https://github.com/fdehau/tui-rs/pull/349. Land this after this pushes to release.
|
|
|
|
let dd_text = self.get_dd_spans(app_state);
|
|
|
|
let text_width = if terminal_width < 100 {
|
|
terminal_width * 90 / 100
|
|
} else {
|
|
terminal_width * 50 / 100
|
|
};
|
|
|
|
let text_height = if cfg!(target_os = "windows")
|
|
|| !app_state.app_config_fields.is_advanced_kill
|
|
{
|
|
7
|
|
} else {
|
|
22
|
|
};
|
|
|
|
// let (text_width, text_height) = if let Some(dd_text) = &dd_text {
|
|
// let width = if current_width < 100 {
|
|
// current_width * 90 / 100
|
|
// } else {
|
|
// let min_possible_width = (current_width * 50 / 100) as usize;
|
|
// let mut width = dd_text.width();
|
|
|
|
// // This should theoretically never allow width to be 0... we can be safe and do an extra check though.
|
|
// while width > (current_width as usize) && width / 2 > min_possible_width {
|
|
// width /= 2;
|
|
// }
|
|
|
|
// std::cmp::max(width, min_possible_width) as u16
|
|
// };
|
|
|
|
// (
|
|
// width,
|
|
// (dd_text.height() + 2 + (dd_text.width() / width as usize)) as u16,
|
|
// )
|
|
// } else {
|
|
// // AFAIK this shouldn't happen, unless something went wrong...
|
|
// (
|
|
// if current_width < 100 {
|
|
// current_width * 90 / 100
|
|
// } else {
|
|
// current_width * 50 / 100
|
|
// },
|
|
// 7,
|
|
// )
|
|
// };
|
|
|
|
let vertical_bordering = terminal_height.saturating_sub(text_height) / 2;
|
|
let vertical_dialog_chunk = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([
|
|
Constraint::Length(vertical_bordering),
|
|
Constraint::Length(text_height),
|
|
Constraint::Length(vertical_bordering),
|
|
])
|
|
.split(draw_area);
|
|
|
|
let horizontal_bordering = terminal_width.saturating_sub(text_width) / 2;
|
|
let middle_dialog_chunk = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints([
|
|
Constraint::Length(horizontal_bordering),
|
|
Constraint::Length(text_width),
|
|
Constraint::Length(horizontal_bordering),
|
|
])
|
|
.split(vertical_dialog_chunk[1]);
|
|
|
|
// This is a bit nasty, but it works well... I guess.
|
|
app_state.delete_dialog_state.is_showing_dd =
|
|
self.draw_dd_dialog(&mut f, dd_text, app_state, middle_dialog_chunk[1]);
|
|
} else if app_state.is_expanded {
|
|
if let Some(frozen_draw_loc) = frozen_draw_loc {
|
|
self.draw_frozen_indicator(&mut f, frozen_draw_loc);
|
|
}
|
|
|
|
if let Some(current_widget) = app_state
|
|
.widget_lookup_map
|
|
.get_mut(&app_state.selected_widget)
|
|
{
|
|
current_widget.set_bounds(draw_area);
|
|
current_widget.draw(self, f, draw_area, true, true);
|
|
}
|
|
} else {
|
|
/// A simple traversal through the `arena`, drawing all leaf elements.
|
|
fn traverse_and_draw_tree<B: Backend>(
|
|
node: NodeId, arena: &Arena<LayoutNode>, f: &mut Frame<'_, B>,
|
|
lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, painter: &Painter,
|
|
canvas_data: &DisplayableData, selected_id: NodeId, offset_x: u16,
|
|
offset_y: u16,
|
|
) {
|
|
if let Some(layout_node) = arena.get(node).map(|n| n.get()) {
|
|
match layout_node {
|
|
LayoutNode::Row(RowLayout { bound, .. })
|
|
| LayoutNode::Col(ColLayout { bound, .. }) => {
|
|
for child in node.children(arena) {
|
|
traverse_and_draw_tree(
|
|
child,
|
|
arena,
|
|
f,
|
|
lookup_map,
|
|
painter,
|
|
canvas_data,
|
|
selected_id,
|
|
offset_x + bound.x,
|
|
offset_y + bound.y,
|
|
);
|
|
}
|
|
}
|
|
LayoutNode::Widget(widget_layout) => {
|
|
let bound = widget_layout.bound;
|
|
let area = Rect::new(
|
|
bound.x + offset_x,
|
|
bound.y + offset_y,
|
|
bound.width,
|
|
bound.height,
|
|
);
|
|
|
|
if let Some(widget) = lookup_map.get_mut(&node) {
|
|
// debug!(
|
|
// "Original bound: {:?}, offset_x: {}, offset_y: {}, area: {:?}, widget: {}",
|
|
// bound,
|
|
// offset_x,
|
|
// offset_y,
|
|
// area,
|
|
// widget.get_pretty_name()
|
|
// );
|
|
|
|
if let TmpBottomWidget::Carousel(carousel) = widget {
|
|
let remaining_area: Rect =
|
|
carousel.draw_carousel(painter, f, area);
|
|
if let Some(to_draw_node) =
|
|
carousel.get_currently_selected()
|
|
{
|
|
if let Some(child_widget) =
|
|
lookup_map.get_mut(&to_draw_node)
|
|
{
|
|
child_widget.set_bounds(remaining_area);
|
|
child_widget.draw(
|
|
painter,
|
|
f,
|
|
remaining_area,
|
|
selected_id == to_draw_node,
|
|
false,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
widget.set_bounds(area);
|
|
widget.draw(painter, f, area, selected_id == node, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(frozen_draw_loc) = frozen_draw_loc {
|
|
self.draw_frozen_indicator(&mut f, frozen_draw_loc);
|
|
}
|
|
|
|
let root = &app_state.layout_tree_root;
|
|
let arena = &mut app_state.layout_tree;
|
|
let canvas_data = &app_state.canvas_data;
|
|
let selected_id = app_state.selected_widget;
|
|
|
|
generate_layout(*root, arena, draw_area, &app_state.widget_lookup_map);
|
|
|
|
let lookup_map = &mut app_state.widget_lookup_map;
|
|
traverse_and_draw_tree(
|
|
*root,
|
|
arena,
|
|
f,
|
|
lookup_map,
|
|
self,
|
|
canvas_data,
|
|
selected_id,
|
|
0,
|
|
0,
|
|
);
|
|
}
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|