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

View File

@ -81,7 +81,7 @@ crossterm = "0.28.1"
ctrlc = { version = "3.4.5", features = ["termination"] } ctrlc = { version = "3.4.5", features = ["termination"] }
dirs = "5.0.1" dirs = "5.0.1"
# Maybe consider https://github.com/rust-lang/rustc-hash for some cases too? # 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" humantime = "2.1.0"
indexmap = "2.6.0" indexmap = "2.6.0"
indoc = "2.0.5" 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. # These are just used for JSON schema generation.
schemars = { version = "0.8.21", optional = true } 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 } strum = { version = "0.26.3", features = ["derive"], optional = true }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]

View File

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

View File

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

View File

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

View File

@ -6,10 +6,11 @@ use tui::{
style::Style, style::Style,
symbols::Marker, symbols::Marker,
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Borders, GraphType}, widgets::{BorderType, GraphType},
Frame, Frame,
}; };
use unicode_segmentation::UnicodeSegmentation;
use crate::canvas::drawing_utils::widget_block;
use super::time_chart::{ use super::time_chart::{
Axis, Dataset, LegendPosition, Point, TimeChart, DEFAULT_LEGEND_CONSTRAINTS, Axis, Dataset, LegendPosition, Point, TimeChart, DEFAULT_LEGEND_CONSTRAINTS,
@ -42,9 +43,15 @@ pub struct TimeGraph<'a> {
/// The border style. /// The border style.
pub border_style: Style, pub border_style: Style,
/// The border type.
pub border_type: BorderType,
/// The graph title. /// The graph title.
pub title: Cow<'a, str>, pub title: Cow<'a, str>,
/// Whether this graph is selected.
pub is_selected: bool,
/// Whether this graph is expanded. /// Whether this graph is expanded.
pub is_expanded: bool, 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 /// 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. /// 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. // This is some ugly manual loop unswitching. Maybe unnecessary.
// TODO: Optimize this step. Cut out unneeded points. // TODO: Optimize this step. Cut out unneeded points.
let data = graph_data.iter().map(create_dataset).collect(); let data = graph_data.iter().map(create_dataset).collect();
let block = Block::default()
.title(self.generate_title(draw_loc)) let block = {
.borders(Borders::ALL) let mut b = widget_block(false, self.is_selected, self.border_type)
.border_style(self.border_style); .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( f.render_widget(
TimeChart::new(data) TimeChart::new(data)
@ -186,10 +178,10 @@ mod test {
use std::borrow::Cow; use std::borrow::Cow;
use tui::{ use tui::{
layout::Rect,
style::{Color, Style}, style::{Color, Style},
symbols::Marker, symbols::Marker,
text::{Line, Span}, text::Span,
widgets::BorderType,
}; };
use super::TimeGraph; use super::TimeGraph;
@ -210,6 +202,8 @@ mod test {
y_labels: &Y_LABELS, y_labels: &Y_LABELS,
graph_style: Style::default().fg(Color::Red), graph_style: Style::default().fg(Color::Red),
border_style: Style::default().fg(Color::Blue), border_style: Style::default().fg(Color::Blue),
border_type: BorderType::Plain,
is_selected: false,
is_expanded: false, is_expanded: false,
title_style: Style::default().fg(Color::Cyan), title_style: Style::default().fg(Color::Cyan),
legend_position: None, legend_position: None,
@ -252,26 +246,4 @@ mod test {
assert_eq!(y_axis.labels, actual.labels); assert_eq!(y_axis.labels, actual.labels);
assert_eq!(y_axis.style, actual.style); 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 left_name = left_table.get_pretty_name();
let right_name = right_table.get_pretty_name(); let right_name = right_table.get_pretty_name();
let num_spaces = let num_spaces =
usize::from(draw_loc.width).saturating_sub(6 + left_name.len() + right_name.len()); 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![ let left_arrow_text = vec![
Line::default(), Line::default(),
Line::from(Span::styled( Line::from(Span::styled(format!("{left_name}"), carousel_text_style)),
format!("{left_name}"),
self.colours.text_style,
)),
]; ];
let right_arrow_text = vec![ let right_arrow_text = vec![
Line::default(), Line::default(),
Line::from(Span::styled( Line::from(Span::styled(format!("{right_name}"), carousel_text_style)),
format!("{right_name}"),
self.colours.text_style,
)),
]; ];
let margined_draw_loc = Layout::default() let margined_draw_loc = Layout::default()

View File

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

View File

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

View File

@ -1,6 +1,11 @@
use std::{cmp::min, time::Instant}; 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. /// 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 { 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)] #[cfg(test)]
mod test { mod test {

View File

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

View File

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

View File

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

View File

@ -1,13 +1,11 @@
use tui::{ use tui::{
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
widgets::Block,
Frame, Frame,
}; };
use crate::{ use crate::{
app::App, app::App,
canvas::{components::pipe_gauge::PipeGauge, Painter}, canvas::{components::pipe_gauge::PipeGauge, drawing_utils::widget_block, Painter},
constants::*,
}; };
impl Painter { impl Painter {
@ -19,9 +17,8 @@ impl Painter {
if app_state.current_widget.widget_id == widget_id { if app_state.current_widget.widget_id == widget_id {
f.render_widget( f.render_widget(
Block::default() widget_block(true, true, self.styles.border_type)
.borders(SIDE_BORDERS) .border_style(self.styles.highlighted_border_style),
.border_style(self.colours.highlighted_border_style),
draw_loc, draw_loc,
); );
} }
@ -50,8 +47,8 @@ impl Painter {
.ratio(ram_percentage / 100.0) .ratio(ram_percentage / 100.0)
.start_label("RAM") .start_label("RAM")
.inner_label(memory_fraction_label) .inner_label(memory_fraction_label)
.label_style(self.colours.ram_style) .label_style(self.styles.ram_style)
.gauge_style(self.colours.ram_style), .gauge_style(self.styles.ram_style),
); );
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
@ -75,8 +72,8 @@ impl Painter {
.ratio(cache_percentage / 100.0) .ratio(cache_percentage / 100.0)
.start_label("CHE") .start_label("CHE")
.inner_label(cache_fraction_label) .inner_label(cache_fraction_label)
.label_style(self.colours.cache_style) .label_style(self.styles.cache_style)
.gauge_style(self.colours.cache_style), .gauge_style(self.styles.cache_style),
); );
} }
} }
@ -100,8 +97,8 @@ impl Painter {
.ratio(swap_percentage / 100.0) .ratio(swap_percentage / 100.0)
.start_label("SWP") .start_label("SWP")
.inner_label(swap_fraction_label) .inner_label(swap_fraction_label)
.label_style(self.colours.swap_style) .label_style(self.styles.swap_style)
.gauge_style(self.colours.swap_style), .gauge_style(self.styles.swap_style),
); );
} }
@ -124,8 +121,8 @@ impl Painter {
.ratio(arc_percentage / 100.0) .ratio(arc_percentage / 100.0)
.start_label("ARC") .start_label("ARC")
.inner_label(arc_fraction_label) .inner_label(arc_fraction_label)
.label_style(self.colours.arc_style) .label_style(self.styles.arc_style)
.gauge_style(self.colours.arc_style), .gauge_style(self.styles.arc_style),
); );
} }
} }
@ -133,7 +130,7 @@ impl Painter {
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
{ {
if let Some(gpu_data) = &app_state.converted_data.gpu_data { 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; let mut color_index = 0;
gpu_data.iter().for_each(|gpu_data_vec| { 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}"); let mem_label = format!("RAM:{label_percent}{label_frac}");
points.push(GraphData { points.push(GraphData {
points: &app_state.converted_data.mem_data, points: &app_state.converted_data.mem_data,
style: self.colours.ram_style, style: self.styles.ram_style,
name: Some(mem_label.into()), name: Some(mem_label.into()),
}); });
} }
@ -64,7 +64,7 @@ impl Painter {
let cache_label = format!("CHE:{label_percent}{label_frac}"); let cache_label = format!("CHE:{label_percent}{label_frac}");
points.push(GraphData { points.push(GraphData {
points: &app_state.converted_data.cache_data, points: &app_state.converted_data.cache_data,
style: self.colours.cache_style, style: self.styles.cache_style,
name: Some(cache_label.into()), name: Some(cache_label.into()),
}); });
} }
@ -72,7 +72,7 @@ impl Painter {
let swap_label = format!("SWP:{label_percent}{label_frac}"); let swap_label = format!("SWP:{label_percent}{label_frac}");
points.push(GraphData { points.push(GraphData {
points: &app_state.converted_data.swap_data, points: &app_state.converted_data.swap_data,
style: self.colours.swap_style, style: self.styles.swap_style,
name: Some(swap_label.into()), name: Some(swap_label.into()),
}); });
} }
@ -81,7 +81,7 @@ impl Painter {
let arc_label = format!("ARC:{label_percent}{label_frac}"); let arc_label = format!("ARC:{label_percent}{label_frac}");
points.push(GraphData { points.push(GraphData {
points: &app_state.converted_data.arc_data, points: &app_state.converted_data.arc_data,
style: self.colours.arc_style, style: self.styles.arc_style,
name: Some(arc_label.into()), name: Some(arc_label.into()),
}); });
} }
@ -89,7 +89,7 @@ impl Painter {
{ {
if let Some(gpu_data) = &app_state.converted_data.gpu_data { if let Some(gpu_data) = &app_state.converted_data.gpu_data {
let mut color_index = 0; 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| { gpu_data.iter().for_each(|gpu| {
let gpu_label = let gpu_label =
format!("{}:{}{}", gpu.name, gpu.mem_percent, gpu.mem_total); format!("{}:{}{}", gpu.name, gpu.mem_percent, gpu.mem_total);
@ -128,11 +128,13 @@ impl Painter {
hide_x_labels, hide_x_labels,
y_bounds: Y_BOUNDS, y_bounds: Y_BOUNDS,
y_labels: &Y_LABELS, y_labels: &Y_LABELS,
graph_style: self.colours.graph_style, graph_style: self.styles.graph_style,
border_style, border_style,
border_type: self.styles.border_type,
title: " Memory ".into(), title: " Memory ".into(),
is_selected: app_state.current_widget.widget_id == widget_id,
is_expanded: app_state.is_expanded, 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_position: app_state.app_config_fields.memory_legend_position,
legend_constraints: Some((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))), legend_constraints: Some((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))),
marker, marker,

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ use std::{
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use config::style::ColourPalette; use config::style::Styles;
pub use config::Config; pub use config::Config;
pub(crate) use error::{OptionError, OptionResult}; pub(crate) use error::{OptionError, OptionResult};
use hashbrown::{HashMap, HashSet}; 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, /// - 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 /// but continue. This is in case they do not want to write a default config file at
/// the XDG locations, for example. /// 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); let adjusted_config_path = get_config_path(config_path);
match &adjusted_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. /// Initialize the app.
pub(crate) fn init_app( pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomLayout, Styles)> {
args: BottomArgs, config: Config,
) -> Result<(App, BottomLayout, ColourPalette)> {
use BottomWidgetType::*; use BottomWidgetType::*;
// Since everything takes a reference, but we want to take ownership here to // 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 args = &args;
let config = &config; 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) = let (widget_layout, default_widget_id, default_widget_type_option) =
get_widget_layout(args, config) get_widget_layout(args, config)

View File

@ -1,6 +1,7 @@
//! Config options around styling. //! Config options around styling.
mod battery; mod battery;
mod borders;
mod cpu; mod cpu;
mod graphs; mod graphs;
mod memory; mod memory;
@ -19,7 +20,7 @@ use memory::MemoryStyle;
use network::NetworkStyle; use network::NetworkStyle;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tables::TableStyle; use tables::TableStyle;
use tui::style::Style; use tui::{style::Style, widgets::BorderType};
use utils::{opt, set_colour, set_colour_list, set_style}; use utils::{opt, set_colour, set_colour_list, set_style};
use widgets::WidgetStyle; use widgets::WidgetStyle;
@ -92,45 +93,47 @@ pub(crate) struct StyleConfig {
pub(crate) widgets: Option<WidgetStyle>, pub(crate) widgets: Option<WidgetStyle>,
} }
/// The actual internal representation of the configured colours, /// The actual internal representation of the configured styles.
/// as a "palette".
#[derive(Debug)] #[derive(Debug)]
pub struct ColourPalette { pub struct Styles {
pub ram_style: Style, pub(crate) ram_style: Style,
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
pub cache_style: Style, pub(crate) cache_style: Style,
pub swap_style: Style, pub(crate) swap_style: Style,
pub arc_style: Style, #[cfg(feature = "zfs")]
pub gpu_colours: Vec<Style>, pub(crate) arc_style: Style,
pub rx_style: Style, #[cfg(feature = "gpu")]
pub tx_style: Style, pub(crate) gpu_colours: Vec<Style>,
pub total_rx_style: Style, pub(crate) rx_style: Style,
pub total_tx_style: Style, pub(crate) tx_style: Style,
pub all_cpu_colour: Style, pub(crate) total_rx_style: Style,
pub avg_cpu_colour: Style, pub(crate) total_tx_style: Style,
pub cpu_colour_styles: Vec<Style>, pub(crate) all_cpu_colour: Style,
pub border_style: Style, pub(crate) avg_cpu_colour: Style,
pub highlighted_border_style: Style, pub(crate) cpu_colour_styles: Vec<Style>,
pub text_style: Style, pub(crate) border_style: Style,
pub selected_text_style: Style, pub(crate) highlighted_border_style: Style,
pub table_header_style: Style, pub(crate) text_style: Style,
pub widget_title_style: Style, pub(crate) selected_text_style: Style,
pub graph_style: Style, pub(crate) table_header_style: Style,
pub graph_legend_style: Style, pub(crate) widget_title_style: Style,
pub high_battery: Style, pub(crate) graph_style: Style,
pub medium_battery: Style, pub(crate) graph_legend_style: Style,
pub low_battery: Style, pub(crate) high_battery: Style,
pub invalid_query_style: Style, pub(crate) medium_battery: Style,
pub disabled_text_style: 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 { fn default() -> Self {
Self::default_palette() Self::default_style()
} }
} }
impl ColourPalette { impl Styles {
pub fn new(args: &BottomArgs, config: &Config) -> anyhow::Result<Self> { pub fn new(args: &BottomArgs, config: &Config) -> anyhow::Result<Self> {
let mut palette = match &args.style.theme { let mut palette = match &args.style.theme {
Some(theme) => Self::from_theme(theme)?, Some(theme) => Self::from_theme(theme)?,
@ -141,8 +144,8 @@ impl ColourPalette {
}; };
// Apply theme from config on top. // Apply theme from config on top.
if let Some(style) = &config.styles { if let Some(config_style) = &config.styles {
palette.set_colours_from_palette(style)?; palette.set_styles_from_config(config_style)?;
} }
Ok(palette) Ok(palette)
@ -151,7 +154,7 @@ impl ColourPalette {
fn from_theme(theme: &str) -> anyhow::Result<Self> { fn from_theme(theme: &str) -> anyhow::Result<Self> {
let lower_case = theme.to_lowercase(); let lower_case = theme.to_lowercase();
match lower_case.as_str() { match lower_case.as_str() {
"default" => Ok(Self::default_palette()), "default" => Ok(Self::default_style()),
"default-light" => Ok(Self::default_light_mode()), "default-light" => Ok(Self::default_light_mode()),
"gruvbox" => Ok(Self::gruvbox_palette()), "gruvbox" => Ok(Self::gruvbox_palette()),
"gruvbox-light" => Ok(Self::gruvbox_light_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 // CPU
set_colour!(self.avg_cpu_colour, config.cpu, avg_entry_color); set_colour!(self.avg_cpu_colour, config.cpu, avg_entry_color);
set_colour!(self.all_cpu_colour, config.cpu, all_entry_color); set_colour!(self.all_cpu_colour, config.cpu, all_entry_color);
@ -215,6 +218,12 @@ impl ColourPalette {
selected_border_color 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(()) Ok(())
} }
} }
@ -224,20 +233,14 @@ mod test {
use tui::style::{Color, Style}; use tui::style::{Color, Style};
use super::ColourPalette; use super::Styles;
use crate::options::config::style::utils::str_to_colour; use crate::options::config::style::utils::str_to_colour;
#[test] #[test]
fn default_selected_colour_works() { fn default_selected_colour_works() {
let mut colours = ColourPalette::default(); let mut colours = Styles::default();
let original_selected_text_colour = ColourPalette::default_palette() let original_selected_text_colour = Styles::default_style().selected_text_style.fg.unwrap();
.selected_text_style let original_selected_bg_colour = Styles::default_style().selected_text_style.bg.unwrap();
.fg
.unwrap();
let original_selected_bg_colour = ColourPalette::default_palette()
.selected_text_style
.bg
.unwrap();
assert_eq!( assert_eq!(
colours.selected_text_style, colours.selected_text_style,
@ -259,11 +262,11 @@ mod test {
#[test] #[test]
fn built_in_colour_schemes_work() { fn built_in_colour_schemes_work() {
ColourPalette::from_theme("default").unwrap(); Styles::from_theme("default").unwrap();
ColourPalette::from_theme("default-light").unwrap(); Styles::from_theme("default-light").unwrap();
ColourPalette::from_theme("gruvbox").unwrap(); Styles::from_theme("gruvbox").unwrap();
ColourPalette::from_theme("gruvbox-light").unwrap(); Styles::from_theme("gruvbox-light").unwrap();
ColourPalette::from_theme("nord").unwrap(); Styles::from_theme("nord").unwrap();
ColourPalette::from_theme("nord-light").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 super::color;
use crate::options::config::style::ColourPalette; use crate::options::config::style::Styles;
impl ColourPalette { impl Styles {
pub(crate) fn default_palette() -> Self { pub(crate) fn default_style() -> Self {
const FIRST_COLOUR: Color = Color::LightMagenta; const FIRST_COLOUR: Color = Color::LightMagenta;
const SECOND_COLOUR: Color = Color::LightYellow; const SECOND_COLOUR: Color = Color::LightYellow;
const THIRD_COLOUR: Color = Color::LightCyan; const THIRD_COLOUR: Color = Color::LightCyan;
@ -22,7 +25,9 @@ impl ColourPalette {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
cache_style: color!(FIFTH_COLOUR), cache_style: color!(FIFTH_COLOUR),
swap_style: color!(SECOND_COLOUR), swap_style: color!(SECOND_COLOUR),
#[cfg(feature = "zfs")]
arc_style: color!(THIRD_COLOUR), arc_style: color!(THIRD_COLOUR),
#[cfg(feature = "gpu")]
gpu_colours: vec![ gpu_colours: vec![
color!(FOURTH_COLOUR), color!(FOURTH_COLOUR),
color!(Color::LightBlue), color!(Color::LightBlue),
@ -61,6 +66,7 @@ impl ColourPalette {
low_battery: color!(Color::Red), low_battery: color!(Color::Red),
invalid_query_style: color!(Color::Red), invalid_query_style: color!(Color::Red),
disabled_text_style: color!(Color::DarkGray), disabled_text_style: color!(Color::DarkGray),
border_type: BorderType::Plain,
} }
} }
@ -70,7 +76,9 @@ impl ColourPalette {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
cache_style: color!(Color::LightRed), cache_style: color!(Color::LightRed),
swap_style: color!(Color::Red), swap_style: color!(Color::Red),
#[cfg(feature = "zfs")]
arc_style: color!(Color::LightBlue), arc_style: color!(Color::LightBlue),
#[cfg(feature = "gpu")]
gpu_colours: vec![ gpu_colours: vec![
color!(Color::LightGreen), color!(Color::LightGreen),
color!(Color::LightCyan), color!(Color::LightCyan),
@ -101,7 +109,7 @@ impl ColourPalette {
graph_style: color!(Color::Black), graph_style: color!(Color::Black),
graph_legend_style: color!(Color::Black), graph_legend_style: color!(Color::Black),
disabled_text_style: color!(Color::Gray), 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 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 { pub(crate) fn gruvbox_palette() -> Self {
Self { Self {
ram_style: hex!("#8ec07c"), ram_style: hex!("#8ec07c"),
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
cache_style: hex!("#b16286"), cache_style: hex!("#b16286"),
swap_style: hex!("#fabd2f"), swap_style: hex!("#fabd2f"),
#[cfg(feature = "zfs")]
arc_style: hex!("#689d6a"), arc_style: hex!("#689d6a"),
#[cfg(feature = "gpu")]
gpu_colours: vec![ gpu_colours: vec![
hex!("#d79921"), hex!("#d79921"),
hex!("#458588"), hex!("#458588"),
@ -61,6 +66,7 @@ impl ColourPalette {
low_battery: hex!("#fb4934"), low_battery: hex!("#fb4934"),
invalid_query_style: color!(Color::Red), invalid_query_style: color!(Color::Red),
disabled_text_style: hex!("#665c54"), disabled_text_style: hex!("#665c54"),
border_type: BorderType::Plain,
} }
} }
@ -70,7 +76,9 @@ impl ColourPalette {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
cache_style: hex!("#d79921"), cache_style: hex!("#d79921"),
swap_style: hex!("#cc241d"), swap_style: hex!("#cc241d"),
#[cfg(feature = "zfs")]
arc_style: hex!("#689d6a"), arc_style: hex!("#689d6a"),
#[cfg(feature = "gpu")]
gpu_colours: vec![ gpu_colours: vec![
hex!("#9d0006"), hex!("#9d0006"),
hex!("#98971a"), hex!("#98971a"),
@ -121,6 +129,7 @@ impl ColourPalette {
low_battery: hex!("#cc241d"), low_battery: hex!("#cc241d"),
invalid_query_style: color!(Color::Red), invalid_query_style: color!(Color::Red),
disabled_text_style: hex!("#d5c4a1"), 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 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 { pub(crate) fn nord_palette() -> Self {
Self { Self {
ram_style: hex!("#88c0d0"), ram_style: hex!("#88c0d0"),
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
cache_style: hex!("#d8dee9"), cache_style: hex!("#d8dee9"),
swap_style: hex!("#d08770"), swap_style: hex!("#d08770"),
#[cfg(feature = "zfs")]
arc_style: hex!("#5e81ac"), arc_style: hex!("#5e81ac"),
#[cfg(feature = "gpu")]
gpu_colours: vec![ gpu_colours: vec![
hex!("#8fbcbb"), hex!("#8fbcbb"),
hex!("#81a1c1"), hex!("#81a1c1"),
@ -49,6 +54,7 @@ impl ColourPalette {
low_battery: hex!("#bf616a"), low_battery: hex!("#bf616a"),
invalid_query_style: color!(Color::Red), invalid_query_style: color!(Color::Red),
disabled_text_style: hex!("#4c566a"), disabled_text_style: hex!("#4c566a"),
border_type: BorderType::Plain,
} }
} }
@ -58,7 +64,9 @@ impl ColourPalette {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
cache_style: hex!("#4c566a"), cache_style: hex!("#4c566a"),
swap_style: hex!("#d08770"), swap_style: hex!("#d08770"),
#[cfg(feature = "zfs")]
arc_style: hex!("#5e81ac"), arc_style: hex!("#5e81ac"),
#[cfg(feature = "gpu")]
gpu_colours: vec![ gpu_colours: vec![
hex!("#8fbcbb"), hex!("#8fbcbb"),
hex!("#88c0d0"), hex!("#88c0d0"),
@ -97,6 +105,7 @@ impl ColourPalette {
low_battery: hex!("#bf616a"), low_battery: hex!("#bf616a"),
invalid_query_style: color!(Color::Red), invalid_query_style: color!(Color::Red),
disabled_text_style: hex!("#d8dee9"), disabled_text_style: hex!("#d8dee9"),
border_type: BorderType::Plain,
} }
} }
} }

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{ColorStr, TextStyleConfig}; use super::{borders::WidgetBorderType, ColorStr, TextStyleConfig};
/// General styling for generic widgets. /// General styling for generic widgets.
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
@ -26,4 +26,7 @@ pub(crate) struct WidgetStyle {
/// Text styling for text when representing something that is disabled. /// Text styling for text when representing something that is disabled.
pub(crate) disabled_text: Option<TextStyleConfig>, 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_collection::cpu::CpuDataType,
data_conversion::CpuWidgetData, data_conversion::CpuWidgetData,
options::config::{cpu::CpuDefault, style::ColourPalette}, options::config::{cpu::CpuDefault, style::Styles},
}; };
pub enum CpuWidgetColumn { pub enum CpuWidgetColumn {
@ -106,15 +106,14 @@ impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
#[inline(always)] #[inline(always)]
fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> { fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {
let style = match self { let style = match self {
CpuWidgetTableData::All => painter.colours.all_cpu_colour, CpuWidgetTableData::All => painter.styles.all_cpu_colour,
CpuWidgetTableData::Entry { CpuWidgetTableData::Entry {
data_type, data_type,
last_entry: _, last_entry: _,
} => match data_type { } => match data_type {
CpuDataType::Avg => painter.colours.avg_cpu_colour, CpuDataType::Avg => painter.styles.avg_cpu_colour,
CpuDataType::Cpu(index) => { CpuDataType::Cpu(index) => {
painter.colours.cpu_colour_styles painter.styles.cpu_colour_styles[index % painter.styles.cpu_colour_styles.len()]
[index % painter.colours.cpu_colour_styles.len()]
} }
}, },
}; };
@ -142,7 +141,7 @@ pub struct CpuWidgetState {
impl CpuWidgetState { impl CpuWidgetState {
pub(crate) fn new( pub(crate) fn new(
config: &AppConfigFields, default_selection: CpuDefault, current_display_time: u64, config: &AppConfigFields, default_selection: CpuDefault, current_display_time: u64,
autohide_timer: Option<Instant>, colours: &ColourPalette, autohide_timer: Option<Instant>, colours: &Styles,
) -> Self { ) -> Self {
const COLUMNS: [Column<CpuWidgetColumn>; 2] = [ const COLUMNS: [Column<CpuWidgetColumn>; 2] = [
Column::soft(CpuWidgetColumn::Cpu, Some(0.5)), Column::soft(CpuWidgetColumn::Cpu, Some(0.5)),

View File

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

View File

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

View File

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

View File

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