feature: support custom widget borders (#1642)

* run a dep bump

* add widget border type

* feature: support custom widget borders

* fmt

* remove none since it looks really bad

* fix bug with title for tables with no title when expanded

* fix jsonschema

* fix some unused stuff
This commit is contained in:
Clement Tsang 2024-12-05 01:52:55 -05:00 committed by GitHub
parent 1fe17ddc21
commit 0d182e4b3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 499 additions and 496 deletions

8
Cargo.lock generated
View File

@ -620,9 +620,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "hashbrown"
version = "0.15.1"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"allocator-api2",
"equivalent",
@ -1278,9 +1278,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.132"
version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",

View File

@ -81,7 +81,7 @@ crossterm = "0.28.1"
ctrlc = { version = "3.4.5", features = ["termination"] }
dirs = "5.0.1"
# Maybe consider https://github.com/rust-lang/rustc-hash for some cases too?
hashbrown = "0.15.0"
hashbrown = "0.15.2"
humantime = "2.1.0"
indexmap = "2.6.0"
indoc = "2.0.5"
@ -104,7 +104,7 @@ time = { version = "0.3.36", features = ["local-offset", "formatting", "macros"]
# These are just used for JSON schema generation.
schemars = { version = "0.8.21", optional = true }
serde_json = { version = "1.0.132", optional = true }
serde_json = { version = "1.0.133", optional = true }
strum = { version = "0.26.3", features = ["derive"], optional = true }
[target.'cfg(unix)'.dependencies]

View File

@ -955,6 +955,15 @@
}
]
},
"WidgetBorderType": {
"type": "string",
"enum": [
"Default",
"Rounded",
"Double",
"Thick"
]
},
"WidgetStyle": {
"description": "General styling for generic widgets.",
"type": "object",
@ -1014,6 +1023,17 @@
}
]
},
"widget_border_type": {
"description": "Widget borders type.",
"anyOf": [
{
"$ref": "#/definitions/WidgetBorderType"
},
{
"type": "null"
}
]
},
"widget_title": {
"description": "Text styling for a widget's title.",
"anyOf": [

View File

@ -18,12 +18,12 @@ use crate::{
App,
},
constants::*,
options::config::style::ColourPalette,
options::config::style::Styles,
};
/// Handles the canvas' state.
pub struct Painter {
pub colours: ColourPalette,
pub styles: Styles,
previous_height: u16,
previous_width: u16,
@ -47,7 +47,7 @@ pub enum LayoutConstraint {
}
impl Painter {
pub fn init(layout: BottomLayout, styling: ColourPalette) -> anyhow::Result<Self> {
pub fn init(layout: BottomLayout, styling: Styles) -> anyhow::Result<Self> {
// Now for modularity; we have to also initialize the base layouts!
// We want to do this ONCE and reuse; after this we can just construct
// based on the console size.
@ -131,7 +131,7 @@ impl Painter {
});
let painter = Painter {
colours: styling,
styles: styling,
previous_height: 0,
previous_width: 0,
row_constraints,
@ -149,9 +149,9 @@ impl Painter {
pub fn get_border_style(&self, widget_id: u64, selected_widget_id: u64) -> tui::style::Style {
let is_on_widget = widget_id == selected_widget_id;
if is_on_widget {
self.colours.highlighted_border_style
self.styles.highlighted_border_style
} else {
self.colours.border_style
self.styles.border_style
}
}
@ -159,7 +159,7 @@ impl Painter {
f.render_widget(
Paragraph::new(Span::styled(
"Frozen, press 'f' to unfreeze",
self.colours.selected_text_style,
self.styles.selected_text_style,
)),
Layout::default()
.horizontal_margin(1)
@ -333,15 +333,11 @@ impl Painter {
_ => 0,
};
self.draw_process(f, app_state, rect[0], true, widget_id);
self.draw_process(f, app_state, rect[0], widget_id);
}
Battery => {
self.draw_battery(f, app_state, rect[0], app_state.current_widget.widget_id)
}
Battery => self.draw_battery(
f,
app_state,
rect[0],
true,
app_state.current_widget.widget_id,
),
_ => {}
}
} else if app_state.app_config_fields.use_basic_mode {
@ -444,18 +440,14 @@ impl Painter {
ProcSort => 2,
_ => 0,
};
self.draw_process(f, app_state, vertical_chunks[3], false, wid);
self.draw_process(f, app_state, vertical_chunks[3], wid);
}
Temp => {
self.draw_temp_table(f, app_state, vertical_chunks[3], widget_id)
}
Battery => self.draw_battery(
f,
app_state,
vertical_chunks[3],
false,
widget_id,
),
Battery => {
self.draw_battery(f, app_state, vertical_chunks[3], widget_id)
}
_ => {}
}
}
@ -729,8 +721,8 @@ impl Painter {
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, true, widget.widget_id),
Battery => self.draw_battery(f, app_state, *draw_loc, true, widget.widget_id),
Proc => self.draw_process(f, app_state, *draw_loc, widget.widget_id),
Battery => self.draw_battery(f, app_state, *draw_loc, widget.widget_id),
_ => {}
}
}

View File

@ -3,6 +3,7 @@
pub mod data_table;
pub mod time_graph;
mod tui_widget;
pub mod widget_carousel;
pub use tui_widget::*;

View File

@ -7,10 +7,9 @@ use concat_string::concat_string;
use tui::{
layout::{Constraint, Direction, Layout, Rect},
text::{Line, Span, Text},
widgets::{Block, Borders, Row, Table},
widgets::{Block, Row, Table},
Frame,
};
use unicode_segmentation::UnicodeSegmentation;
use super::{
CalculateColumnWidths, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataToCell,
@ -18,8 +17,8 @@ use super::{
};
use crate::{
app::layout_manager::BottomWidget,
canvas::Painter,
constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
canvas::{drawing_utils::widget_block, Painter},
constants::TABLE_GAP_HEIGHT_LIMIT,
utils::strings::truncate_to_text,
};
@ -68,46 +67,41 @@ where
C: DataTableColumn<H>,
{
fn block<'a>(&self, draw_info: &'a DrawInfo, data_len: usize) -> Block<'a> {
let border_style = match draw_info.selection_state {
SelectionState::NotSelected => self.styling.border_style,
SelectionState::Selected | SelectionState::Expanded => {
self.styling.highlighted_border_style
}
let is_selected = match draw_info.selection_state {
SelectionState::NotSelected => false,
SelectionState::Selected | SelectionState::Expanded => true,
};
if !self.props.is_basic {
let block = Block::default()
.borders(Borders::ALL)
.border_style(border_style);
if let Some(title) = self.generate_title(draw_info, data_len) {
block.title(title)
} else {
block
}
} else if draw_info.is_on_widget() {
// Implies it is basic mode but selected.
Block::default()
.borders(SIDE_BORDERS)
.border_style(border_style)
let border_style = if is_selected {
self.styling.highlighted_border_style
} else {
Block::default().borders(Borders::NONE)
self.styling.border_style
};
let mut block = widget_block(self.props.is_basic, is_selected, self.styling.border_type)
.border_style(border_style);
if let Some((left_title, right_title)) = self.generate_title(draw_info, data_len) {
if !self.props.is_basic {
block = block.title_top(left_title);
}
if let Some(right_title) = right_title {
block = block.title_top(right_title);
}
}
block
}
/// Generates a title, given the available space.
pub fn generate_title<'a>(
&self, draw_info: &'a DrawInfo, total_items: usize,
) -> Option<Line<'a>> {
fn generate_title(
&self, draw_info: &'_ DrawInfo, total_items: usize,
) -> Option<(Line<'static>, Option<Line<'static>>)> {
self.props.title.as_ref().map(|title| {
let current_index = self.state.current_index.saturating_add(1);
let draw_loc = draw_info.loc;
let title_style = self.styling.title_style;
let border_style = if draw_info.is_on_widget() {
self.styling.highlighted_border_style
} else {
self.styling.border_style
};
let title = if self.props.show_table_scroll_position {
let pos = current_index.to_string();
@ -123,19 +117,15 @@ where
title.to_string()
};
if draw_info.is_expanded() {
let title_base = concat_string!(title, "── Esc to go back ");
let lines = "".repeat(usize::from(draw_loc.width).saturating_sub(
UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2,
));
let esc = concat_string!("", lines, "─ Esc to go back ");
Line::from(vec![
Span::styled(title, title_style),
Span::styled(esc, border_style),
])
let left_title = Line::from(Span::styled(title, title_style)).left_aligned();
let right_title = if draw_info.is_expanded() {
Some(Line::from(" Esc to go back ").right_aligned())
} else {
Line::from(Span::styled(title, title_style))
}
None
};
(left_title, right_title)
})
}
@ -202,8 +192,9 @@ where
if !self.data.is_empty() || !self.first_draw {
if self.first_draw {
self.first_draw = false; // TODO: Doing it this way is fine, but it could be done better (e.g. showing
// custom no results/entries message)
// TODO: Doing it this way is fine, but it could be done better (e.g. showing
// custom no results/entries message)
self.first_draw = false;
if let Some(first_index) = self.first_index {
self.set_position(first_index);
}

View File

@ -1,11 +1,12 @@
use tui::style::Style;
use tui::{style::Style, widgets::BorderType};
use crate::options::config::style::ColourPalette;
use crate::options::config::style::Styles;
#[derive(Default)]
pub struct DataTableStyling {
pub header_style: Style,
pub border_style: Style,
pub border_type: BorderType,
pub highlighted_border_style: Style,
pub text_style: Style,
pub highlighted_text_style: Style,
@ -13,14 +14,15 @@ pub struct DataTableStyling {
}
impl DataTableStyling {
pub fn from_palette(colours: &ColourPalette) -> Self {
pub fn from_palette(styles: &Styles) -> Self {
Self {
header_style: colours.table_header_style,
border_style: colours.border_style,
highlighted_border_style: colours.highlighted_border_style,
text_style: colours.text_style,
highlighted_text_style: colours.selected_text_style,
title_style: colours.widget_title_style,
header_style: styles.table_header_style,
border_style: styles.border_style,
border_type: styles.border_type,
highlighted_border_style: styles.highlighted_border_style,
text_style: styles.text_style,
highlighted_text_style: styles.selected_text_style,
title_style: styles.widget_title_style,
}
}
}

View File

@ -6,10 +6,11 @@ use tui::{
style::Style,
symbols::Marker,
text::{Line, Span},
widgets::{Block, Borders, GraphType},
widgets::{BorderType, GraphType},
Frame,
};
use unicode_segmentation::UnicodeSegmentation;
use crate::canvas::drawing_utils::widget_block;
use super::time_chart::{
Axis, Dataset, LegendPosition, Point, TimeChart, DEFAULT_LEGEND_CONSTRAINTS,
@ -42,9 +43,15 @@ pub struct TimeGraph<'a> {
/// The border style.
pub border_style: Style,
/// The border type.
pub border_type: BorderType,
/// The graph title.
pub title: Cow<'a, str>,
/// Whether this graph is selected.
pub is_selected: bool,
/// Whether this graph is expanded.
pub is_expanded: bool,
@ -100,29 +107,6 @@ impl TimeGraph<'_> {
)
}
/// Generates a title for the [`TimeGraph`] widget, given the available
/// space.
fn generate_title(&self, draw_loc: Rect) -> Line<'_> {
if self.is_expanded {
let title_base = concat_string!(self.title, "── Esc to go back ");
Line::from(vec![
Span::styled(self.title.as_ref(), self.title_style),
Span::styled(
concat_string!(
"",
"".repeat(usize::from(draw_loc.width).saturating_sub(
UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2
)),
"─ Esc to go back "
),
self.border_style,
),
])
} else {
Line::from(Span::styled(self.title.as_ref(), self.title_style))
}
}
/// Draws a time graph at [`Rect`] location provided by `draw_loc`. A time
/// graph is used to display data points throughout time in the x-axis.
///
@ -139,10 +123,18 @@ impl TimeGraph<'_> {
// This is some ugly manual loop unswitching. Maybe unnecessary.
// TODO: Optimize this step. Cut out unneeded points.
let data = graph_data.iter().map(create_dataset).collect();
let block = Block::default()
.title(self.generate_title(draw_loc))
.borders(Borders::ALL)
.border_style(self.border_style);
let block = {
let mut b = widget_block(false, self.is_selected, self.border_type)
.border_style(self.border_style)
.title_top(Line::styled(self.title.as_ref(), self.title_style));
if self.is_expanded {
b = b.title_top(Line::styled(" Esc to go back ", self.title_style).right_aligned())
}
b
};
f.render_widget(
TimeChart::new(data)
@ -186,10 +178,10 @@ mod test {
use std::borrow::Cow;
use tui::{
layout::Rect,
style::{Color, Style},
symbols::Marker,
text::{Line, Span},
text::Span,
widgets::BorderType,
};
use super::TimeGraph;
@ -210,6 +202,8 @@ mod test {
y_labels: &Y_LABELS,
graph_style: Style::default().fg(Color::Red),
border_style: Style::default().fg(Color::Blue),
border_type: BorderType::Plain,
is_selected: false,
is_expanded: false,
title_style: Style::default().fg(Color::Cyan),
legend_position: None,
@ -252,26 +246,4 @@ mod test {
assert_eq!(y_axis.labels, actual.labels);
assert_eq!(y_axis.style, actual.style);
}
#[test]
fn time_graph_gen_title() {
let mut time_graph = create_time_graph();
let draw_loc = Rect::new(0, 0, 32, 100);
let title = time_graph.generate_title(draw_loc);
assert_eq!(
title,
Line::from(Span::styled(" Network ", Style::default().fg(Color::Cyan)))
);
time_graph.is_expanded = true;
let title = time_graph.generate_title(draw_loc);
assert_eq!(
title,
Line::from(vec![
Span::styled(" Network ", Style::default().fg(Color::Cyan)),
Span::styled("───── Esc to go back ", Style::default().fg(Color::Blue))
])
);
}
}

View File

@ -84,26 +84,25 @@ impl Painter {
},
);
// TODO: I can do this text effect as just a border now!
let left_name = left_table.get_pretty_name();
let right_name = right_table.get_pretty_name();
let num_spaces =
usize::from(draw_loc.width).saturating_sub(6 + left_name.len() + right_name.len());
let carousel_text_style = if widget_id == app_state.current_widget.widget_id {
self.styles.highlighted_border_style
} else {
self.styles.text_style
};
let left_arrow_text = vec![
Line::default(),
Line::from(Span::styled(
format!("{left_name}"),
self.colours.text_style,
)),
Line::from(Span::styled(format!("{left_name}"), carousel_text_style)),
];
let right_arrow_text = vec![
Line::default(),
Line::from(Span::styled(
format!("{right_name}"),
self.colours.text_style,
)),
Line::from(Span::styled(format!("{right_name}"), carousel_text_style)),
];
let margined_draw_loc = Layout::default()

View File

@ -4,19 +4,16 @@ use std::cmp::min;
use tui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph, Wrap},
widgets::{Block, Paragraph, Wrap},
Frame,
};
use crate::{
app::{App, KillSignal, MAX_PROCESS_SIGNAL},
canvas::Painter,
canvas::{drawing_utils::dialog_block, Painter},
widgets::ProcWidgetMode,
};
const DD_BASE: &str = " Confirm Kill Process ── Esc to close ";
const DD_ERROR_BASE: &str = " Error ── Esc to close ";
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
const SIGNAL_TEXT: [&str; 63] = [
@ -211,12 +208,12 @@ impl Painter {
if MAX_PROCESS_SIGNAL == 1 || !app_state.app_config_fields.is_advanced_kill {
let (yes_button, no_button) = match app_state.delete_dialog_state.selected_signal {
KillSignal::Kill(_) => (
Span::styled("Yes", self.colours.selected_text_style),
Span::styled("No", self.colours.text_style),
Span::styled("Yes", self.styles.selected_text_style),
Span::styled("No", self.styles.text_style),
),
KillSignal::Cancel => (
Span::styled("Yes", self.colours.text_style),
Span::styled("No", self.colours.selected_text_style),
Span::styled("Yes", self.styles.text_style),
Span::styled("No", self.styles.selected_text_style),
),
};
@ -322,11 +319,11 @@ impl Painter {
let mut buttons = SIGNAL_TEXT
[scroll_offset + 1..min((layout.len()) + scroll_offset, SIGNAL_TEXT.len())]
.iter()
.map(|text| Span::styled(*text, self.colours.text_style))
.map(|text| Span::styled(*text, self.styles.text_style))
.collect::<Vec<Span<'_>>>();
buttons.insert(0, Span::styled(SIGNAL_TEXT[0], self.colours.text_style));
buttons.insert(0, Span::styled(SIGNAL_TEXT[0], self.styles.text_style));
buttons[selected - scroll_offset] =
Span::styled(SIGNAL_TEXT[selected], self.colours.selected_text_style);
Span::styled(SIGNAL_TEXT[selected], self.styles.selected_text_style);
app_state.delete_dialog_state.button_positions = layout
.iter()
@ -354,45 +351,24 @@ impl Painter {
) -> bool {
if let Some(dd_text) = dd_text {
let dd_title = if app_state.dd_err.is_some() {
Line::from(vec![
Span::styled(" Error ", self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to close ",
"".repeat(
usize::from(draw_loc.width)
.saturating_sub(DD_ERROR_BASE.chars().count() + 2)
)
),
self.colours.border_style,
),
])
Line::styled(" Error ", self.styles.widget_title_style)
} else {
Line::from(vec![
Span::styled(" Confirm Kill Process ", self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to close ",
"".repeat(
usize::from(draw_loc.width)
.saturating_sub(DD_BASE.chars().count() + 2)
)
),
self.colours.border_style,
),
])
Line::styled(" Confirm Kill Process ", self.styles.widget_title_style)
};
f.render_widget(
Paragraph::new(dd_text)
.block(
Block::default()
.title(dd_title)
.style(self.colours.border_style)
.borders(Borders::ALL)
.border_style(self.colours.border_style),
dialog_block(self.styles.border_type)
.title_top(dd_title)
.title_top(
Line::styled(" Esc to close ", self.styles.widget_title_style)
.right_aligned(),
)
.style(self.styles.border_style)
.border_style(self.styles.border_style),
)
.style(self.colours.text_style)
.style(self.styles.text_style)
.alignment(Alignment::Center)
.wrap(Wrap { trim: true }),
draw_loc,

View File

@ -3,19 +3,17 @@ use std::cmp::{max, min};
use tui::{
layout::{Alignment, Rect},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
widgets::{Paragraph, Wrap},
Frame,
};
use unicode_width::UnicodeWidthStr;
use crate::{
app::App,
canvas::Painter,
canvas::{drawing_utils::dialog_block, Painter},
constants::{self, HELP_TEXT},
};
const HELP_BASE: &str = " Help ── Esc to close ";
// TODO: [REFACTOR] Make generic dialog boxes to build off of instead?
impl Painter {
fn help_text_lines(&self) -> Vec<Line<'_>> {
@ -28,12 +26,12 @@ impl Painter {
if itx > 0 {
if let Some(header) = section.next() {
styled_help_spans.push(Span::default());
styled_help_spans.push(Span::styled(*header, self.colours.table_header_style));
styled_help_spans.push(Span::styled(*header, self.styles.table_header_style));
}
}
section.for_each(|&text| {
styled_help_spans.push(Span::styled(text, self.colours.text_style))
styled_help_spans.push(Span::styled(text, self.styles.text_style))
});
});
@ -43,24 +41,12 @@ impl Painter {
pub fn draw_help_dialog(&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect) {
let styled_help_text = self.help_text_lines();
let help_title = Line::from(vec![
Span::styled(" Help ", self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to close ",
"".repeat(
usize::from(draw_loc.width).saturating_sub(HELP_BASE.chars().count() + 2)
)
),
self.colours.border_style,
),
]);
let block = Block::default()
.title(help_title)
.style(self.colours.border_style)
.borders(Borders::ALL)
.border_style(self.colours.border_style);
let block = dialog_block(self.styles.border_type)
.border_style(self.styles.border_style)
.title_top(Line::styled(" Help ", self.styles.widget_title_style))
.title_top(
Line::styled(" Esc to close ", self.styles.widget_title_style).right_aligned(),
);
if app_state.should_get_widget_bounds() {
// We must also recalculate how many lines are wrapping to properly get
@ -116,7 +102,7 @@ impl Painter {
f.render_widget(
Paragraph::new(styled_help_text.clone())
.block(block)
.style(self.colours.text_style)
.style(self.styles.text_style)
.alignment(Alignment::Left)
.wrap(Wrap { trim: true })
.scroll((

View File

@ -1,6 +1,11 @@
use std::{cmp::min, time::Instant};
use tui::layout::Rect;
use tui::{
layout::Rect,
widgets::{Block, BorderType, Borders},
};
use super::SIDE_BORDERS;
/// Calculate how many bars are to be drawn within basic mode's components.
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
@ -30,6 +35,30 @@ pub fn should_hide_x_label(
}
}
/// Return a widget block.
pub fn widget_block(is_basic: bool, is_selected: bool, border_type: BorderType) -> Block<'static> {
let mut block = Block::default().border_type(border_type);
if is_basic {
if is_selected {
block = block.borders(SIDE_BORDERS);
} else {
block = block.borders(Borders::empty());
}
} else {
block = block.borders(Borders::all());
}
block
}
/// Return a dialog block.
pub fn dialog_block(border_type: BorderType) -> Block<'static> {
Block::default()
.border_type(border_type)
.borders(Borders::all())
}
#[cfg(test)]
mod test {

View File

@ -1,23 +1,24 @@
use tui::{
layout::{Constraint, Direction, Layout, Rect},
text::{Line, Span},
widgets::{Block, Borders, Cell, Paragraph, Row, Table, Tabs},
widgets::{Cell, Paragraph, Row, Table, Tabs},
Frame,
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{
app::App,
canvas::{drawing_utils::calculate_basic_use_bars, Painter},
canvas::{
drawing_utils::{calculate_basic_use_bars, widget_block},
Painter,
},
constants::*,
data_conversion::BatteryDuration,
};
impl Painter {
pub fn draw_battery(
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
let should_get_widget_bounds = app_state.should_get_widget_bounds();
if let Some(battery_widget_state) = app_state
@ -28,9 +29,9 @@ impl Painter {
{
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let border_style = if is_on_widget {
self.colours.highlighted_border_style
self.styles.highlighted_border_style
} else {
self.colours.border_style
self.styles.border_style
};
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
@ -38,35 +39,23 @@ impl Painter {
app_state.app_config_fields.table_gap
};
let title = if app_state.is_expanded {
const TITLE_BASE: &str = " Battery ── Esc to go back ";
Line::from(vec![
Span::styled(" Battery ", self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to go back ",
"".repeat(usize::from(draw_loc.width).saturating_sub(
UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2
))
),
border_style,
),
])
} else {
Line::from(Span::styled(" Battery ", self.colours.widget_title_style))
};
let block = {
let mut block = widget_block(
app_state.app_config_fields.use_basic_mode,
is_on_widget,
self.styles.border_type,
)
.border_style(border_style)
.title_top(Line::styled(" Battery ", self.styles.widget_title_style));
let battery_block = if draw_border {
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(border_style)
} else if is_on_widget {
Block::default()
.borders(SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style)
} else {
Block::default().borders(Borders::NONE)
if app_state.is_expanded {
block = block.title_top(
Line::styled(" Esc to go back ", self.styles.widget_title_style)
.right_aligned(),
)
}
block
};
if app_state.converted_data.battery_data.len() > 1 {
@ -95,8 +84,8 @@ impl Painter {
.collect::<Vec<_>>(),
)
.divider(tui::symbols::line::VERTICAL)
.style(self.colours.text_style)
.highlight_style(self.colours.selected_text_style)
.style(self.styles.text_style)
.highlight_style(self.styles.selected_text_style)
.select(battery_widget_state.currently_selected_battery_index),
tab_draw_loc,
);
@ -120,9 +109,11 @@ impl Painter {
}
}
let is_basic = app_state.app_config_fields.use_basic_mode;
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)])
.horizontal_margin(u16::from(!(is_on_widget || draw_border)))
.horizontal_margin(u16::from(!(is_on_widget || is_basic)))
.direction(Direction::Horizontal)
.split(draw_loc)[0];
@ -144,15 +135,15 @@ impl Painter {
let mut battery_charge_rows = Vec::with_capacity(2);
battery_charge_rows.push(Row::new([
Cell::from("Charge").style(self.colours.text_style)
Cell::from("Charge").style(self.styles.text_style)
]));
battery_charge_rows.push(Row::new([Cell::from(bars).style(
if charge_percentage < 10.0 {
self.colours.low_battery
self.styles.low_battery
} else if charge_percentage < 50.0 {
self.colours.medium_battery
self.styles.medium_battery
} else {
self.colours.high_battery
self.styles.high_battery
},
)]));
@ -160,16 +151,16 @@ impl Painter {
battery_rows.push(Row::new([""]).bottom_margin(table_gap + 1));
battery_rows.push(
Row::new(["Rate", &battery_details.watt_consumption])
.style(self.colours.text_style),
.style(self.styles.text_style),
);
battery_rows.push(
Row::new(["State", &battery_details.state]).style(self.colours.text_style),
Row::new(["State", &battery_details.state]).style(self.styles.text_style),
);
let mut time: String; // Keep string lifetime in scope.
{
let style = self.colours.text_style;
let style = self.styles.text_style;
match &battery_details.battery_duration {
BatteryDuration::ToEmpty(secs) => {
time = long_time(*secs);
@ -198,7 +189,7 @@ impl Painter {
}
battery_rows.push(
Row::new(["Health", &battery_details.health]).style(self.colours.text_style),
Row::new(["Health", &battery_details.health]).style(self.styles.text_style),
);
let header = if app_state.converted_data.battery_data.len() > 1 {
@ -210,7 +201,7 @@ impl Painter {
// Draw bar
f.render_widget(
Table::new(battery_charge_rows, [Constraint::Percentage(100)])
.block(battery_block.clone())
.block(block.clone())
.header(header.clone()),
margined_draw_loc,
);
@ -221,7 +212,7 @@ impl Painter {
battery_rows,
[Constraint::Percentage(50), Constraint::Percentage(50)],
)
.block(battery_block)
.block(block)
.header(header),
margined_draw_loc,
);
@ -230,13 +221,10 @@ impl Painter {
contents.push(Line::from(Span::styled(
"No data found for this battery",
self.colours.text_style,
self.styles.text_style,
)));
f.render_widget(
Paragraph::new(contents).block(battery_block),
margined_draw_loc,
);
f.render_widget(Paragraph::new(contents).block(block), margined_draw_loc);
}
if should_get_widget_bounds {
@ -253,7 +241,6 @@ impl Painter {
}
}
#[inline]
fn get_hms(secs: i64) -> (i64, i64, i64) {
let hours = secs / (60 * 60);
let minutes = (secs / 60) - hours * 60;
@ -266,23 +253,16 @@ fn long_time(secs: i64) -> String {
let (hours, minutes, seconds) = get_hms(secs);
if hours > 0 {
format!(
"{} hour{}, {} minute{}, {} second{}",
hours,
if hours == 1 { "" } else { "s" },
minutes,
if minutes == 1 { "" } else { "s" },
seconds,
if seconds == 1 { "" } else { "s" },
)
let h = if hours == 1 { "hour" } else { "hours" };
let m = if minutes == 1 { "minute" } else { "minutes" };
let s = if seconds == 1 { "second" } else { "seconds" };
format!("{hours} {h}, {minutes} {m}, {seconds} {s}")
} else {
format!(
"{} minute{}, {} second{}",
minutes,
if minutes == 1 { "" } else { "s" },
seconds,
if seconds == 1 { "" } else { "s" },
)
let m = if minutes == 1 { "minute" } else { "minutes" };
let s = if seconds == 1 { "second" } else { "seconds" };
format!("{minutes} {m}, {seconds} {s}")
}
}

View File

@ -2,7 +2,6 @@ use std::cmp::min;
use tui::{
layout::{Constraint, Direction, Layout, Rect},
widgets::Block,
Frame,
};
@ -10,9 +9,9 @@ use crate::{
app::App,
canvas::{
components::pipe_gauge::{LabelLimit, PipeGauge},
drawing_utils::widget_block,
Painter,
},
constants::*,
data_collection::cpu::CpuDataType,
data_conversion::CpuWidgetData,
};
@ -38,9 +37,8 @@ impl Painter {
if app_state.current_widget.widget_id == widget_id {
f.render_widget(
Block::default()
.borders(SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style),
widget_block(true, true, self.styles.border_type)
.border_style(self.styles.highlighted_border_style),
draw_loc,
);
}
@ -156,10 +154,10 @@ impl Painter {
};
let (outer, style) = match data_type {
CpuDataType::Avg => ("AVG".to_string(), self.colours.avg_cpu_colour),
CpuDataType::Avg => ("AVG".to_string(), self.styles.avg_cpu_colour),
CpuDataType::Cpu(index) => (
format!("{index:<3}",),
self.colours.cpu_colour_styles[index % self.colours.cpu_colour_styles.len()],
self.styles.cpu_colour_styles[index % self.styles.cpu_colour_styles.len()],
),
};
let inner = format!("{:>3.0}%", last_entry.round());

View File

@ -136,13 +136,13 @@ impl Painter {
CpuWidgetData::All => None,
CpuWidgetData::Entry { data, .. } => {
let style = if show_avg_cpu && itx == AVG_POSITION {
self.colours.avg_cpu_colour
self.styles.avg_cpu_colour
} else if itx == ALL_POSITION {
self.colours.all_cpu_colour
self.styles.all_cpu_colour
} else {
let offset_position = itx - 1; // Because of the all position
self.colours.cpu_colour_styles[(offset_position - show_avg_offset)
% self.colours.cpu_colour_styles.len()]
self.styles.cpu_colour_styles[(offset_position - show_avg_offset)
% self.styles.cpu_colour_styles.len()]
};
Some(GraphData {
@ -158,11 +158,11 @@ impl Painter {
cpu_data.get(current_scroll_position)
{
let style = if show_avg_cpu && current_scroll_position == AVG_POSITION {
self.colours.avg_cpu_colour
self.styles.avg_cpu_colour
} else {
let offset_position = current_scroll_position - 1; // Because of the all position
self.colours.cpu_colour_styles
[(offset_position - show_avg_offset) % self.colours.cpu_colour_styles.len()]
self.styles.cpu_colour_styles
[(offset_position - show_avg_offset) % self.styles.cpu_colour_styles.len()]
};
vec![GraphData {
@ -228,11 +228,13 @@ impl Painter {
hide_x_labels,
y_bounds: Y_BOUNDS,
y_labels: &Y_LABELS,
graph_style: self.colours.graph_style,
graph_style: self.styles.graph_style,
border_style,
border_type: self.styles.border_type,
title,
is_selected: app_state.current_widget.widget_id == widget_id,
is_expanded: app_state.is_expanded,
title_style: self.colours.widget_title_style,
title_style: self.styles.widget_title_style,
legend_position: None,
legend_constraints: None,
marker,

View File

@ -1,13 +1,11 @@
use tui::{
layout::{Constraint, Direction, Layout, Rect},
widgets::Block,
Frame,
};
use crate::{
app::App,
canvas::{components::pipe_gauge::PipeGauge, Painter},
constants::*,
canvas::{components::pipe_gauge::PipeGauge, drawing_utils::widget_block, Painter},
};
impl Painter {
@ -19,9 +17,8 @@ impl Painter {
if app_state.current_widget.widget_id == widget_id {
f.render_widget(
Block::default()
.borders(SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style),
widget_block(true, true, self.styles.border_type)
.border_style(self.styles.highlighted_border_style),
draw_loc,
);
}
@ -50,8 +47,8 @@ impl Painter {
.ratio(ram_percentage / 100.0)
.start_label("RAM")
.inner_label(memory_fraction_label)
.label_style(self.colours.ram_style)
.gauge_style(self.colours.ram_style),
.label_style(self.styles.ram_style)
.gauge_style(self.styles.ram_style),
);
#[cfg(not(target_os = "windows"))]
@ -75,8 +72,8 @@ impl Painter {
.ratio(cache_percentage / 100.0)
.start_label("CHE")
.inner_label(cache_fraction_label)
.label_style(self.colours.cache_style)
.gauge_style(self.colours.cache_style),
.label_style(self.styles.cache_style)
.gauge_style(self.styles.cache_style),
);
}
}
@ -100,8 +97,8 @@ impl Painter {
.ratio(swap_percentage / 100.0)
.start_label("SWP")
.inner_label(swap_fraction_label)
.label_style(self.colours.swap_style)
.gauge_style(self.colours.swap_style),
.label_style(self.styles.swap_style)
.gauge_style(self.styles.swap_style),
);
}
@ -124,8 +121,8 @@ impl Painter {
.ratio(arc_percentage / 100.0)
.start_label("ARC")
.inner_label(arc_fraction_label)
.label_style(self.colours.arc_style)
.gauge_style(self.colours.arc_style),
.label_style(self.styles.arc_style)
.gauge_style(self.styles.arc_style),
);
}
}
@ -133,7 +130,7 @@ impl Painter {
#[cfg(feature = "gpu")]
{
if let Some(gpu_data) = &app_state.converted_data.gpu_data {
let gpu_styles = &self.colours.gpu_colours;
let gpu_styles = &self.styles.gpu_colours;
let mut color_index = 0;
gpu_data.iter().for_each(|gpu_data_vec| {

View File

@ -55,7 +55,7 @@ impl Painter {
let mem_label = format!("RAM:{label_percent}{label_frac}");
points.push(GraphData {
points: &app_state.converted_data.mem_data,
style: self.colours.ram_style,
style: self.styles.ram_style,
name: Some(mem_label.into()),
});
}
@ -64,7 +64,7 @@ impl Painter {
let cache_label = format!("CHE:{label_percent}{label_frac}");
points.push(GraphData {
points: &app_state.converted_data.cache_data,
style: self.colours.cache_style,
style: self.styles.cache_style,
name: Some(cache_label.into()),
});
}
@ -72,7 +72,7 @@ impl Painter {
let swap_label = format!("SWP:{label_percent}{label_frac}");
points.push(GraphData {
points: &app_state.converted_data.swap_data,
style: self.colours.swap_style,
style: self.styles.swap_style,
name: Some(swap_label.into()),
});
}
@ -81,7 +81,7 @@ impl Painter {
let arc_label = format!("ARC:{label_percent}{label_frac}");
points.push(GraphData {
points: &app_state.converted_data.arc_data,
style: self.colours.arc_style,
style: self.styles.arc_style,
name: Some(arc_label.into()),
});
}
@ -89,7 +89,7 @@ impl Painter {
{
if let Some(gpu_data) = &app_state.converted_data.gpu_data {
let mut color_index = 0;
let gpu_styles = &self.colours.gpu_colours;
let gpu_styles = &self.styles.gpu_colours;
gpu_data.iter().for_each(|gpu| {
let gpu_label =
format!("{}:{}{}", gpu.name, gpu.mem_percent, gpu.mem_total);
@ -128,11 +128,13 @@ impl Painter {
hide_x_labels,
y_bounds: Y_BOUNDS,
y_labels: &Y_LABELS,
graph_style: self.colours.graph_style,
graph_style: self.styles.graph_style,
border_style,
border_type: self.styles.border_type,
title: " Memory ".into(),
is_selected: app_state.current_widget.widget_id == widget_id,
is_expanded: app_state.is_expanded,
title_style: self.colours.widget_title_style,
title_style: self.styles.widget_title_style,
legend_position: app_state.app_config_fields.memory_legend_position,
legend_constraints: Some((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))),
marker,

View File

@ -5,7 +5,10 @@ use tui::{
Frame,
};
use crate::{app::App, canvas::Painter, constants::*};
use crate::{
app::App,
canvas::{drawing_utils::widget_block, Painter},
};
impl Painter {
pub fn draw_basic_network(
@ -30,9 +33,8 @@ impl Painter {
if app_state.current_widget.widget_id == widget_id {
f.render_widget(
Block::default()
.borders(SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style),
widget_block(true, true, self.styles.border_type)
.border_style(self.styles.highlighted_border_style),
draw_loc,
);
}
@ -43,13 +45,13 @@ impl Painter {
let total_tx_label = format!("Total TX: {}", app_state.converted_data.total_tx_display);
let net_text = vec![
Line::from(Span::styled(rx_label, self.colours.rx_style)),
Line::from(Span::styled(tx_label, self.colours.tx_style)),
Line::from(Span::styled(rx_label, self.styles.rx_style)),
Line::from(Span::styled(tx_label, self.styles.tx_style)),
];
let total_net_text = vec![
Line::from(Span::styled(total_rx_label, self.colours.total_rx_style)),
Line::from(Span::styled(total_tx_label, self.colours.total_tx_style)),
Line::from(Span::styled(total_rx_label, self.styles.total_rx_style)),
Line::from(Span::styled(total_tx_label, self.styles.total_tx_style)),
];
f.render_widget(Paragraph::new(net_text).block(Block::default()), net_loc[0]);

View File

@ -107,17 +107,17 @@ impl Painter {
vec![
GraphData {
points: network_data_rx,
style: self.colours.rx_style,
style: self.styles.rx_style,
name: Some(format!("RX: {:7}", app_state.converted_data.rx_display).into()),
},
GraphData {
points: network_data_tx,
style: self.colours.tx_style,
style: self.styles.tx_style,
name: Some(format!("TX: {:7}", app_state.converted_data.tx_display).into()),
},
GraphData {
points: &[],
style: self.colours.total_rx_style,
style: self.styles.total_rx_style,
name: Some(
format!("Total RX: {:7}", app_state.converted_data.total_rx_display)
.into(),
@ -125,7 +125,7 @@ impl Painter {
},
GraphData {
points: &[],
style: self.colours.total_tx_style,
style: self.styles.total_tx_style,
name: Some(
format!("Total TX: {:7}", app_state.converted_data.total_tx_display)
.into(),
@ -136,12 +136,12 @@ impl Painter {
vec![
GraphData {
points: network_data_rx,
style: self.colours.rx_style,
style: self.styles.rx_style,
name: Some((&app_state.converted_data.rx_display).into()),
},
GraphData {
points: network_data_tx,
style: self.colours.tx_style,
style: self.styles.tx_style,
name: Some((&app_state.converted_data.tx_display).into()),
},
]
@ -158,11 +158,13 @@ impl Painter {
hide_x_labels,
y_bounds,
y_labels: &y_labels,
graph_style: self.colours.graph_style,
graph_style: self.styles.graph_style,
border_style,
border_type: self.styles.border_type,
title: " Network ".into(),
is_selected: app_state.current_widget.widget_id == widget_id,
is_expanded: app_state.is_expanded,
title_style: self.colours.widget_title_style,
title_style: self.styles.widget_title_style,
legend_position: app_state.app_config_fields.network_legend_position,
legend_constraints: Some(legend_constraints),
marker,
@ -183,10 +185,10 @@ impl Painter {
// Gross but I need it to work...
let total_network = vec![Row::new([
Text::styled(rx_display, self.colours.rx_style),
Text::styled(tx_display, self.colours.tx_style),
Text::styled(total_rx_display, self.colours.total_rx_style),
Text::styled(total_tx_display, self.colours.total_tx_style),
Text::styled(rx_display, self.styles.rx_style),
Text::styled(tx_display, self.styles.tx_style),
Text::styled(total_rx_display, self.styles.total_rx_style),
Text::styled(total_tx_display, self.styles.total_tx_style),
])];
// Draw
@ -198,15 +200,15 @@ impl Painter {
.map(Constraint::Length)
.collect::<Vec<_>>()),
)
.header(Row::new(NETWORK_HEADERS).style(self.colours.table_header_style))
.header(Row::new(NETWORK_HEADERS).style(self.styles.table_header_style))
.block(Block::default().borders(Borders::ALL).border_style(
if app_state.current_widget.widget_id == widget_id {
self.colours.highlighted_border_style
self.styles.highlighted_border_style
} else {
self.colours.border_style
self.styles.border_style
},
))
.style(self.colours.text_style),
.style(self.styles.text_style),
draw_loc,
);
}

View File

@ -2,7 +2,7 @@ use tui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::Style,
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
widgets::Paragraph,
Frame,
};
use unicode_segmentation::UnicodeSegmentation;
@ -11,9 +11,9 @@ use crate::{
app::{App, AppSearchState},
canvas::{
components::data_table::{DrawInfo, SelectionState},
drawing_utils::widget_block,
Painter,
},
constants::*,
};
const SORT_MENU_WIDTH: u16 = 7;
@ -23,11 +23,11 @@ impl Painter {
/// - `widget_id` here represents the widget ID of the process widget
/// itself!
pub fn draw_process(
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
if let Some(proc_widget_state) = app_state.states.proc_state.widget_states.get(&widget_id) {
let search_height = if draw_border { 5 } else { 3 };
let is_basic = app_state.app_config_fields.use_basic_mode;
let search_height = if !is_basic { 5 } else { 3 };
let is_sort_open = proc_widget_state.is_sort_open;
let mut proc_draw_loc = draw_loc;
@ -38,13 +38,7 @@ impl Painter {
.split(draw_loc);
proc_draw_loc = processes_chunk[0];
self.draw_search_field(
f,
app_state,
processes_chunk[1],
draw_border,
widget_id + 1,
);
self.draw_search_field(f, app_state, processes_chunk[1], widget_id + 1);
}
if is_sort_open {
@ -110,8 +104,7 @@ impl Painter {
/// - `widget_id` represents the widget ID of the search box itself --- NOT
/// the process widget state that is stored.
fn draw_search_field(
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
fn build_query_span(
search_state: &AppSearchState, available_width: usize, is_on_widget: bool,
@ -157,16 +150,18 @@ impl Painter {
}
}
let is_basic = app_state.app_config_fields.use_basic_mode;
if let Some(proc_widget_state) = app_state
.states
.proc_state
.widget_states
.get_mut(&(widget_id - 1))
{
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let is_selected = widget_id == app_state.current_widget.widget_id;
let num_columns = usize::from(draw_loc.width);
const SEARCH_TITLE: &str = "> ";
let offset = if draw_border { 4 } else { 2 }; // width of 3 removed for >_|
let offset = 4;
let available_width = if num_columns > (offset + 3) {
num_columns - offset
} else {
@ -182,18 +177,18 @@ impl Painter {
let query_with_cursor = build_query_span(
&proc_widget_state.proc_search.search_state,
available_width,
is_on_widget,
self.colours.selected_text_style,
self.colours.text_style,
is_selected,
self.styles.selected_text_style,
self.styles.text_style,
);
let mut search_text = vec![Line::from({
let mut search_vec = vec![Span::styled(
SEARCH_TITLE,
if is_on_widget {
self.colours.table_header_style
if is_selected {
self.styles.table_header_style
} else {
self.colours.text_style
self.styles.text_style
},
)];
search_vec.extend(query_with_cursor);
@ -203,21 +198,21 @@ impl Painter {
// Text options shamelessly stolen from VS Code.
let case_style = if !proc_widget_state.proc_search.is_ignoring_case {
self.colours.selected_text_style
self.styles.selected_text_style
} else {
self.colours.text_style
self.styles.text_style
};
let whole_word_style = if proc_widget_state.proc_search.is_searching_whole_word {
self.colours.selected_text_style
self.styles.selected_text_style
} else {
self.colours.text_style
self.styles.text_style
};
let regex_style = if proc_widget_state.proc_search.is_searching_with_regex {
self.colours.selected_text_style
self.styles.selected_text_style
} else {
self.colours.text_style
self.styles.text_style
};
// TODO: [MOUSE] Mouse support for these in search
@ -245,54 +240,42 @@ impl Painter {
} else {
""
},
self.colours.invalid_query_style,
self.styles.invalid_query_style,
)));
search_text.push(option_text);
let current_border_style =
if proc_widget_state.proc_search.search_state.is_invalid_search {
self.colours.invalid_query_style
} else if is_on_widget {
self.colours.highlighted_border_style
self.styles.invalid_query_style
} else if is_selected {
self.styles.highlighted_border_style
} else {
self.colours.border_style
self.styles.border_style
};
let title = Span::styled(
if draw_border {
const TITLE_BASE: &str = " Esc to close ";
let repeat_num =
usize::from(draw_loc.width).saturating_sub(TITLE_BASE.chars().count() + 2);
format!("{} Esc to close ", "".repeat(repeat_num))
} else {
String::new()
},
current_border_style,
);
let process_search_block = {
let mut block = widget_block(is_basic, is_selected, self.styles.border_type)
.border_style(current_border_style);
let process_search_block = if draw_border {
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(current_border_style)
} else if is_on_widget {
Block::default()
.borders(SIDE_BORDERS)
.border_style(current_border_style)
} else {
Block::default().borders(Borders::NONE)
if !is_basic {
block = block.title_top(
Line::styled(" Esc to close ", current_border_style).right_aligned(),
)
}
block
};
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)])
.horizontal_margin(u16::from(!(is_on_widget || draw_border)))
.horizontal_margin(u16::from(is_basic && !is_selected))
.direction(Direction::Horizontal)
.split(draw_loc)[0];
f.render_widget(
Paragraph::new(search_text)
.block(process_search_block)
.style(self.colours.text_style)
.style(self.styles.text_style)
.alignment(Alignment::Left),
margined_draw_loc,
);

View File

@ -16,7 +16,7 @@ use std::{
};
use anyhow::{Context, Result};
use config::style::ColourPalette;
use config::style::Styles;
pub use config::Config;
pub(crate) use error::{OptionError, OptionResult};
use hashbrown::{HashMap, HashSet};
@ -144,7 +144,7 @@ fn create_config_at_path(path: &Path) -> anyhow::Result<Config> {
/// - If the user does NOT pass in a path explicitly, then just show a warning,
/// but continue. This is in case they do not want to write a default config file at
/// the XDG locations, for example.
pub fn get_or_create_config(config_path: Option<&Path>) -> anyhow::Result<Config> {
pub(crate) fn get_or_create_config(config_path: Option<&Path>) -> anyhow::Result<Config> {
let adjusted_config_path = get_config_path(config_path);
match &adjusted_config_path {
@ -196,9 +196,7 @@ pub fn get_or_create_config(config_path: Option<&Path>) -> anyhow::Result<Config
}
/// Initialize the app.
pub(crate) fn init_app(
args: BottomArgs, config: Config,
) -> Result<(App, BottomLayout, ColourPalette)> {
pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomLayout, Styles)> {
use BottomWidgetType::*;
// Since everything takes a reference, but we want to take ownership here to
@ -206,7 +204,7 @@ pub(crate) fn init_app(
let args = &args;
let config = &config;
let styling = ColourPalette::new(args, config)?;
let styling = Styles::new(args, config)?;
let (widget_layout, default_widget_id, default_widget_type_option) =
get_widget_layout(args, config)

View File

@ -1,6 +1,7 @@
//! Config options around styling.
mod battery;
mod borders;
mod cpu;
mod graphs;
mod memory;
@ -19,7 +20,7 @@ use memory::MemoryStyle;
use network::NetworkStyle;
use serde::{Deserialize, Serialize};
use tables::TableStyle;
use tui::style::Style;
use tui::{style::Style, widgets::BorderType};
use utils::{opt, set_colour, set_colour_list, set_style};
use widgets::WidgetStyle;
@ -92,45 +93,47 @@ pub(crate) struct StyleConfig {
pub(crate) widgets: Option<WidgetStyle>,
}
/// The actual internal representation of the configured colours,
/// as a "palette".
/// The actual internal representation of the configured styles.
#[derive(Debug)]
pub struct ColourPalette {
pub ram_style: Style,
pub struct Styles {
pub(crate) ram_style: Style,
#[cfg(not(target_os = "windows"))]
pub cache_style: Style,
pub swap_style: Style,
pub arc_style: Style,
pub gpu_colours: Vec<Style>,
pub rx_style: Style,
pub tx_style: Style,
pub total_rx_style: Style,
pub total_tx_style: Style,
pub all_cpu_colour: Style,
pub avg_cpu_colour: Style,
pub cpu_colour_styles: Vec<Style>,
pub border_style: Style,
pub highlighted_border_style: Style,
pub text_style: Style,
pub selected_text_style: Style,
pub table_header_style: Style,
pub widget_title_style: Style,
pub graph_style: Style,
pub graph_legend_style: Style,
pub high_battery: Style,
pub medium_battery: Style,
pub low_battery: Style,
pub invalid_query_style: Style,
pub disabled_text_style: Style,
pub(crate) cache_style: Style,
pub(crate) swap_style: Style,
#[cfg(feature = "zfs")]
pub(crate) arc_style: Style,
#[cfg(feature = "gpu")]
pub(crate) gpu_colours: Vec<Style>,
pub(crate) rx_style: Style,
pub(crate) tx_style: Style,
pub(crate) total_rx_style: Style,
pub(crate) total_tx_style: Style,
pub(crate) all_cpu_colour: Style,
pub(crate) avg_cpu_colour: Style,
pub(crate) cpu_colour_styles: Vec<Style>,
pub(crate) border_style: Style,
pub(crate) highlighted_border_style: Style,
pub(crate) text_style: Style,
pub(crate) selected_text_style: Style,
pub(crate) table_header_style: Style,
pub(crate) widget_title_style: Style,
pub(crate) graph_style: Style,
pub(crate) graph_legend_style: Style,
pub(crate) high_battery: Style,
pub(crate) medium_battery: Style,
pub(crate) low_battery: Style,
pub(crate) invalid_query_style: Style,
pub(crate) disabled_text_style: Style,
pub(crate) border_type: BorderType,
}
impl Default for ColourPalette {
impl Default for Styles {
fn default() -> Self {
Self::default_palette()
Self::default_style()
}
}
impl ColourPalette {
impl Styles {
pub fn new(args: &BottomArgs, config: &Config) -> anyhow::Result<Self> {
let mut palette = match &args.style.theme {
Some(theme) => Self::from_theme(theme)?,
@ -141,8 +144,8 @@ impl ColourPalette {
};
// Apply theme from config on top.
if let Some(style) = &config.styles {
palette.set_colours_from_palette(style)?;
if let Some(config_style) = &config.styles {
palette.set_styles_from_config(config_style)?;
}
Ok(palette)
@ -151,7 +154,7 @@ impl ColourPalette {
fn from_theme(theme: &str) -> anyhow::Result<Self> {
let lower_case = theme.to_lowercase();
match lower_case.as_str() {
"default" => Ok(Self::default_palette()),
"default" => Ok(Self::default_style()),
"default-light" => Ok(Self::default_light_mode()),
"gruvbox" => Ok(Self::gruvbox_palette()),
"gruvbox-light" => Ok(Self::gruvbox_light_palette()),
@ -164,7 +167,7 @@ impl ColourPalette {
}
}
fn set_colours_from_palette(&mut self, config: &StyleConfig) -> OptionResult<()> {
fn set_styles_from_config(&mut self, config: &StyleConfig) -> OptionResult<()> {
// CPU
set_colour!(self.avg_cpu_colour, config.cpu, avg_entry_color);
set_colour!(self.all_cpu_colour, config.cpu, all_entry_color);
@ -215,6 +218,12 @@ impl ColourPalette {
selected_border_color
);
if let Some(widgets) = &config.widgets {
if let Some(widget_borders) = widgets.widget_border_type {
self.border_type = widget_borders.into();
}
}
Ok(())
}
}
@ -224,20 +233,14 @@ mod test {
use tui::style::{Color, Style};
use super::ColourPalette;
use super::Styles;
use crate::options::config::style::utils::str_to_colour;
#[test]
fn default_selected_colour_works() {
let mut colours = ColourPalette::default();
let original_selected_text_colour = ColourPalette::default_palette()
.selected_text_style
.fg
.unwrap();
let original_selected_bg_colour = ColourPalette::default_palette()
.selected_text_style
.bg
.unwrap();
let mut colours = Styles::default();
let original_selected_text_colour = Styles::default_style().selected_text_style.fg.unwrap();
let original_selected_bg_colour = Styles::default_style().selected_text_style.bg.unwrap();
assert_eq!(
colours.selected_text_style,
@ -259,11 +262,11 @@ mod test {
#[test]
fn built_in_colour_schemes_work() {
ColourPalette::from_theme("default").unwrap();
ColourPalette::from_theme("default-light").unwrap();
ColourPalette::from_theme("gruvbox").unwrap();
ColourPalette::from_theme("gruvbox-light").unwrap();
ColourPalette::from_theme("nord").unwrap();
ColourPalette::from_theme("nord-light").unwrap();
Styles::from_theme("default").unwrap();
Styles::from_theme("default-light").unwrap();
Styles::from_theme("gruvbox").unwrap();
Styles::from_theme("gruvbox-light").unwrap();
Styles::from_theme("nord").unwrap();
Styles::from_theme("nord-light").unwrap();
}
}

View File

@ -0,0 +1,42 @@
use serde::{Deserialize, Serialize};
use tui::widgets::BorderType;
#[derive(Default, Clone, Copy, Debug, Serialize)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub(crate) enum WidgetBorderType {
#[default]
Default,
Rounded,
Double,
Thick,
}
impl<'de> Deserialize<'de> for WidgetBorderType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?.to_lowercase();
match value.as_str() {
"default" => Ok(WidgetBorderType::Default),
"rounded" => Ok(WidgetBorderType::Rounded),
"double" => Ok(WidgetBorderType::Double),
"thick" => Ok(WidgetBorderType::Thick),
_ => Err(serde::de::Error::custom(
"doesn't match any widget border type",
)),
}
}
}
impl From<WidgetBorderType> for BorderType {
fn from(value: WidgetBorderType) -> Self {
match value {
WidgetBorderType::Default => BorderType::Plain,
WidgetBorderType::Rounded => BorderType::Rounded,
WidgetBorderType::Double => BorderType::Double,
WidgetBorderType::Thick => BorderType::Thick,
}
}
}

View File

@ -1,10 +1,13 @@
use tui::style::{Color, Modifier, Style};
use tui::{
style::{Color, Modifier, Style},
widgets::BorderType,
};
use super::color;
use crate::options::config::style::ColourPalette;
use crate::options::config::style::Styles;
impl ColourPalette {
pub(crate) fn default_palette() -> Self {
impl Styles {
pub(crate) fn default_style() -> Self {
const FIRST_COLOUR: Color = Color::LightMagenta;
const SECOND_COLOUR: Color = Color::LightYellow;
const THIRD_COLOUR: Color = Color::LightCyan;
@ -22,7 +25,9 @@ impl ColourPalette {
#[cfg(not(target_os = "windows"))]
cache_style: color!(FIFTH_COLOUR),
swap_style: color!(SECOND_COLOUR),
#[cfg(feature = "zfs")]
arc_style: color!(THIRD_COLOUR),
#[cfg(feature = "gpu")]
gpu_colours: vec![
color!(FOURTH_COLOUR),
color!(Color::LightBlue),
@ -61,6 +66,7 @@ impl ColourPalette {
low_battery: color!(Color::Red),
invalid_query_style: color!(Color::Red),
disabled_text_style: color!(Color::DarkGray),
border_type: BorderType::Plain,
}
}
@ -70,7 +76,9 @@ impl ColourPalette {
#[cfg(not(target_os = "windows"))]
cache_style: color!(Color::LightRed),
swap_style: color!(Color::Red),
#[cfg(feature = "zfs")]
arc_style: color!(Color::LightBlue),
#[cfg(feature = "gpu")]
gpu_colours: vec![
color!(Color::LightGreen),
color!(Color::LightCyan),
@ -101,7 +109,7 @@ impl ColourPalette {
graph_style: color!(Color::Black),
graph_legend_style: color!(Color::Black),
disabled_text_style: color!(Color::Gray),
..Self::default_palette()
..Self::default_style()
}
}
}

View File

@ -1,16 +1,21 @@
use tui::style::{Color, Modifier};
use tui::{
style::{Color, Modifier},
widgets::BorderType,
};
use super::{color, hex};
use crate::options::config::style::{utils::convert_hex_to_color, ColourPalette};
use crate::options::config::style::{utils::convert_hex_to_color, Styles};
impl ColourPalette {
impl Styles {
pub(crate) fn gruvbox_palette() -> Self {
Self {
ram_style: hex!("#8ec07c"),
#[cfg(not(target_os = "windows"))]
cache_style: hex!("#b16286"),
swap_style: hex!("#fabd2f"),
#[cfg(feature = "zfs")]
arc_style: hex!("#689d6a"),
#[cfg(feature = "gpu")]
gpu_colours: vec![
hex!("#d79921"),
hex!("#458588"),
@ -61,6 +66,7 @@ impl ColourPalette {
low_battery: hex!("#fb4934"),
invalid_query_style: color!(Color::Red),
disabled_text_style: hex!("#665c54"),
border_type: BorderType::Plain,
}
}
@ -70,7 +76,9 @@ impl ColourPalette {
#[cfg(not(target_os = "windows"))]
cache_style: hex!("#d79921"),
swap_style: hex!("#cc241d"),
#[cfg(feature = "zfs")]
arc_style: hex!("#689d6a"),
#[cfg(feature = "gpu")]
gpu_colours: vec![
hex!("#9d0006"),
hex!("#98971a"),
@ -121,6 +129,7 @@ impl ColourPalette {
low_battery: hex!("#cc241d"),
invalid_query_style: color!(Color::Red),
disabled_text_style: hex!("#d5c4a1"),
border_type: BorderType::Plain,
}
}
}

View File

@ -1,16 +1,21 @@
use tui::style::{Color, Modifier};
use tui::{
style::{Color, Modifier},
widgets::BorderType,
};
use super::{color, hex};
use crate::options::config::style::{utils::convert_hex_to_color, ColourPalette};
use crate::options::config::style::{utils::convert_hex_to_color, Styles};
impl ColourPalette {
impl Styles {
pub(crate) fn nord_palette() -> Self {
Self {
ram_style: hex!("#88c0d0"),
#[cfg(not(target_os = "windows"))]
cache_style: hex!("#d8dee9"),
swap_style: hex!("#d08770"),
#[cfg(feature = "zfs")]
arc_style: hex!("#5e81ac"),
#[cfg(feature = "gpu")]
gpu_colours: vec![
hex!("#8fbcbb"),
hex!("#81a1c1"),
@ -49,6 +54,7 @@ impl ColourPalette {
low_battery: hex!("#bf616a"),
invalid_query_style: color!(Color::Red),
disabled_text_style: hex!("#4c566a"),
border_type: BorderType::Plain,
}
}
@ -58,7 +64,9 @@ impl ColourPalette {
#[cfg(not(target_os = "windows"))]
cache_style: hex!("#4c566a"),
swap_style: hex!("#d08770"),
#[cfg(feature = "zfs")]
arc_style: hex!("#5e81ac"),
#[cfg(feature = "gpu")]
gpu_colours: vec![
hex!("#8fbcbb"),
hex!("#88c0d0"),
@ -97,6 +105,7 @@ impl ColourPalette {
low_battery: hex!("#bf616a"),
invalid_query_style: color!(Color::Red),
disabled_text_style: hex!("#d8dee9"),
border_type: BorderType::Plain,
}
}
}

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use super::{ColorStr, TextStyleConfig};
use super::{borders::WidgetBorderType, ColorStr, TextStyleConfig};
/// General styling for generic widgets.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
@ -26,4 +26,7 @@ pub(crate) struct WidgetStyle {
/// Text styling for text when representing something that is disabled.
pub(crate) disabled_text: Option<TextStyleConfig>,
/// Widget borders type.
pub(crate) widget_border_type: Option<WidgetBorderType>,
}

View File

@ -14,7 +14,7 @@ use crate::{
},
data_collection::cpu::CpuDataType,
data_conversion::CpuWidgetData,
options::config::{cpu::CpuDefault, style::ColourPalette},
options::config::{cpu::CpuDefault, style::Styles},
};
pub enum CpuWidgetColumn {
@ -106,15 +106,14 @@ impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
#[inline(always)]
fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {
let style = match self {
CpuWidgetTableData::All => painter.colours.all_cpu_colour,
CpuWidgetTableData::All => painter.styles.all_cpu_colour,
CpuWidgetTableData::Entry {
data_type,
last_entry: _,
} => match data_type {
CpuDataType::Avg => painter.colours.avg_cpu_colour,
CpuDataType::Avg => painter.styles.avg_cpu_colour,
CpuDataType::Cpu(index) => {
painter.colours.cpu_colour_styles
[index % painter.colours.cpu_colour_styles.len()]
painter.styles.cpu_colour_styles[index % painter.styles.cpu_colour_styles.len()]
}
},
};
@ -142,7 +141,7 @@ pub struct CpuWidgetState {
impl CpuWidgetState {
pub(crate) fn new(
config: &AppConfigFields, default_selection: CpuDefault, current_display_time: u64,
autohide_timer: Option<Instant>, colours: &ColourPalette,
autohide_timer: Option<Instant>, colours: &Styles,
) -> Self {
const COLUMNS: [Column<CpuWidgetColumn>; 2] = [
Column::soft(CpuWidgetColumn::Cpu, Some(0.5)),

View File

@ -8,7 +8,7 @@ use crate::{
ColumnHeader, DataTableColumn, DataTableProps, DataTableStyling, DataToCell, SortColumn,
SortDataTable, SortDataTableProps, SortOrder, SortsRow,
},
options::config::style::ColourPalette,
options::config::style::Styles,
utils::{data_prefixes::get_decimal_bytes, general::sort_partial_fn},
};
@ -275,9 +275,7 @@ const fn default_disk_columns() -> [SortColumn<DiskColumn>; 8] {
}
impl DiskTableWidget {
pub fn new(
config: &AppConfigFields, palette: &ColourPalette, columns: Option<&[DiskColumn]>,
) -> Self {
pub fn new(config: &AppConfigFields, palette: &Styles, columns: Option<&[DiskColumn]>) -> Self {
let props = SortDataTableProps {
inner: DataTableProps {
title: Some(" Disks ".into()),

View File

@ -23,7 +23,7 @@ use crate::{
DataTableStyling, SortColumn, SortDataTable, SortDataTableProps, SortOrder, SortsRow,
},
data_collection::processes::{Pid, ProcessHarvest},
options::config::style::ColourPalette,
options::config::style::Styles,
};
/// ProcessSearchState only deals with process' search's current settings and
@ -160,7 +160,7 @@ pub struct ProcWidgetState {
}
impl ProcWidgetState {
fn new_sort_table(config: &AppConfigFields, palette: &ColourPalette) -> SortTable {
fn new_sort_table(config: &AppConfigFields, palette: &Styles) -> SortTable {
const COLUMNS: [Column<SortTableColumn>; 1] = [Column::hard(SortTableColumn, 7)];
let props = DataTableProps {
@ -177,7 +177,7 @@ impl ProcWidgetState {
}
fn new_process_table(
config: &AppConfigFields, colours: &ColourPalette, columns: Vec<SortColumn<ProcColumn>>,
config: &AppConfigFields, colours: &Styles, columns: Vec<SortColumn<ProcColumn>>,
default_index: usize, default_order: SortOrder,
) -> ProcessTable {
let inner_props = DataTableProps {
@ -200,7 +200,7 @@ impl ProcWidgetState {
pub fn new(
config: &AppConfigFields, mode: ProcWidgetMode, table_config: ProcTableConfig,
colours: &ColourPalette, config_columns: &Option<IndexSet<ProcWidgetColumn>>,
colours: &Styles, config_columns: &Option<IndexSet<ProcWidgetColumn>>,
) -> Self {
let process_search_state = {
let mut pss = ProcessSearchState::default();
@ -1130,7 +1130,7 @@ mod test {
fn init_state(table_config: ProcTableConfig, columns: &[ProcWidgetColumn]) -> ProcWidgetState {
let config = AppConfigFields::default();
let styling = ColourPalette::default();
let styling = Styles::default();
let columns = Some(columns.iter().cloned().collect());
ProcWidgetState::new(

View File

@ -340,7 +340,7 @@ impl DataToCell<ProcColumn> for ProcWidgetData {
#[inline(always)]
fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {
if self.disabled {
row.style(painter.colours.disabled_text_style)
row.style(painter.styles.disabled_text_style)
} else {
row
}

View File

@ -9,7 +9,7 @@ use crate::{
SortDataTable, SortDataTableProps, SortOrder, SortsRow,
},
data_collection::temperature::TemperatureType,
options::config::style::ColourPalette,
options::config::style::Styles,
utils::general::sort_partial_fn,
};
@ -100,7 +100,7 @@ pub struct TempWidgetState {
}
impl TempWidgetState {
pub(crate) fn new(config: &AppConfigFields, palette: &ColourPalette) -> Self {
pub(crate) fn new(config: &AppConfigFields, palette: &Styles) -> Self {
let columns = [
SortColumn::soft(TempWidgetColumn::Sensor, Some(0.8)),
SortColumn::soft(TempWidgetColumn::Temp, None).default_descending(),