mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-04-08 17:05:59 +02:00
refactor: move over battery widget
This commit is contained in:
parent
eddc9a16c7
commit
fa00dec146
@ -1,9 +1,24 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
cmp::{max, min},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use tui::{layout::Rect, widgets::Borders};
|
||||
use crossterm::event::{KeyCode, KeyEvent, MouseEvent};
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Paragraph, Tabs},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{data_farmer::DataCollection, Component, Widget},
|
||||
app::{
|
||||
data_farmer::DataCollection, does_bound_intersect_coordinate, event::WidgetEventResult,
|
||||
widgets::tui_widgets::PipeGauge, Component, Widget,
|
||||
},
|
||||
canvas::Painter,
|
||||
constants::TABLE_GAP_HEIGHT_LIMIT,
|
||||
data_conversion::{convert_battery_harvest, ConvertedBatteryData},
|
||||
options::layout_options::LayoutRule,
|
||||
};
|
||||
@ -33,28 +48,27 @@ impl BatteryState {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement battery widget.
|
||||
/// A table displaying battery information on a per-battery basis.
|
||||
pub struct BatteryTable {
|
||||
bounds: Rect,
|
||||
selected_index: usize,
|
||||
batteries: Vec<String>,
|
||||
battery_data: Vec<ConvertedBatteryData>,
|
||||
width: LayoutRule,
|
||||
height: LayoutRule,
|
||||
block_border: Borders,
|
||||
tab_bounds: Vec<Rect>,
|
||||
}
|
||||
|
||||
impl Default for BatteryTable {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
batteries: vec![],
|
||||
bounds: Default::default(),
|
||||
selected_index: 0,
|
||||
battery_data: Default::default(),
|
||||
width: LayoutRule::default(),
|
||||
height: LayoutRule::default(),
|
||||
block_border: Borders::ALL,
|
||||
tab_bounds: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,9 +91,16 @@ impl BatteryTable {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
/// Returns a reference to the battery names.
|
||||
pub fn batteries(&self) -> &[String] {
|
||||
&self.batteries
|
||||
fn increment_index(&mut self) {
|
||||
if self.selected_index + 1 < self.battery_data.len() {
|
||||
self.selected_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn decrement_index(&mut self) {
|
||||
if self.selected_index > 0 {
|
||||
self.selected_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the block border style.
|
||||
@ -100,6 +121,46 @@ impl Component for BatteryTable {
|
||||
fn set_bounds(&mut self, new_bounds: tui::layout::Rect) {
|
||||
self.bounds = new_bounds;
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
|
||||
if event.modifiers.is_empty() {
|
||||
match event.code {
|
||||
KeyCode::Left => {
|
||||
let current_index = self.selected_index;
|
||||
self.decrement_index();
|
||||
if current_index == self.selected_index {
|
||||
WidgetEventResult::NoRedraw
|
||||
} else {
|
||||
WidgetEventResult::Redraw
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
let current_index = self.selected_index;
|
||||
self.increment_index();
|
||||
if current_index == self.selected_index {
|
||||
WidgetEventResult::NoRedraw
|
||||
} else {
|
||||
WidgetEventResult::Redraw
|
||||
}
|
||||
}
|
||||
_ => WidgetEventResult::NoRedraw,
|
||||
}
|
||||
} else {
|
||||
WidgetEventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
|
||||
for (itx, bound) in self.tab_bounds.iter().enumerate() {
|
||||
if does_bound_intersect_coordinate(event.column, event.row, *bound) {
|
||||
if itx < self.battery_data.len() {
|
||||
self.selected_index = itx;
|
||||
return WidgetEventResult::Redraw;
|
||||
}
|
||||
}
|
||||
}
|
||||
WidgetEventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for BatteryTable {
|
||||
@ -109,6 +170,9 @@ impl Widget for BatteryTable {
|
||||
|
||||
fn update_data(&mut self, data_collection: &DataCollection) {
|
||||
self.battery_data = convert_battery_harvest(data_collection);
|
||||
if self.battery_data.len() <= self.selected_index {
|
||||
self.selected_index = self.battery_data.len().saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn width(&self) -> LayoutRule {
|
||||
@ -118,4 +182,163 @@ impl Widget for BatteryTable {
|
||||
fn height(&self) -> LayoutRule {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn draw<B: Backend>(
|
||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.border_style(if selected {
|
||||
painter.colours.highlighted_border_style
|
||||
} else {
|
||||
painter.colours.border_style
|
||||
})
|
||||
.borders(self.block_border.clone());
|
||||
|
||||
let inner_area = block.inner(area);
|
||||
const CONSTRAINTS: [Constraint; 2] = [Constraint::Length(1), Constraint::Min(0)];
|
||||
let split_area = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(CONSTRAINTS)
|
||||
.split(inner_area);
|
||||
let tab_area = Rect::new(
|
||||
split_area[0].x.saturating_sub(1),
|
||||
split_area[0].y,
|
||||
split_area[0].width,
|
||||
split_area[0].height,
|
||||
);
|
||||
let data_area = if inner_area.height >= TABLE_GAP_HEIGHT_LIMIT && split_area[1].height > 0 {
|
||||
Rect::new(
|
||||
split_area[1].x,
|
||||
split_area[1].y + 1,
|
||||
split_area[1].width,
|
||||
split_area[1].height - 1,
|
||||
)
|
||||
} else {
|
||||
split_area[1]
|
||||
};
|
||||
|
||||
if self.battery_data.is_empty() {
|
||||
f.render_widget(
|
||||
Paragraph::new("No batteries found").style(painter.colours.text_style),
|
||||
tab_area,
|
||||
);
|
||||
} else {
|
||||
let battery_tab_names = self
|
||||
.battery_data
|
||||
.iter()
|
||||
.map(|d| Spans::from(d.battery_name.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
let mut start_x_offset = tab_area.x + 1;
|
||||
self.tab_bounds = battery_tab_names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let length = name.width() as u16;
|
||||
let start = start_x_offset;
|
||||
start_x_offset += length;
|
||||
start_x_offset += 3;
|
||||
|
||||
Rect::new(start, tab_area.y, length, 1)
|
||||
})
|
||||
.collect();
|
||||
f.render_widget(
|
||||
Tabs::new(battery_tab_names)
|
||||
.divider(tui::symbols::line::VERTICAL)
|
||||
.style(painter.colours.text_style)
|
||||
.highlight_style(painter.colours.currently_selected_text_style)
|
||||
.select(self.selected_index),
|
||||
tab_area,
|
||||
);
|
||||
|
||||
if let Some(battery_details) = self.battery_data.get(self.selected_index) {
|
||||
let labels = vec![
|
||||
Spans::from(Span::styled("Charge %", painter.colours.text_style)),
|
||||
Spans::from(Span::styled("Consumption", painter.colours.text_style)),
|
||||
match &battery_details.charge_times {
|
||||
crate::data_conversion::BatteryDuration::Charging { .. } => {
|
||||
Spans::from(Span::styled("Time to full", painter.colours.text_style))
|
||||
}
|
||||
crate::data_conversion::BatteryDuration::Discharging { .. } => {
|
||||
Spans::from(Span::styled("Time to empty", painter.colours.text_style))
|
||||
}
|
||||
crate::data_conversion::BatteryDuration::Neither => Spans::from(
|
||||
Span::styled("Time to full/empty", painter.colours.text_style),
|
||||
),
|
||||
},
|
||||
Spans::from(Span::styled("Health %", painter.colours.text_style)),
|
||||
];
|
||||
|
||||
let data_constraints = if let Some(len) = labels.iter().map(|s| s.width()).max() {
|
||||
[
|
||||
Constraint::Length(min(
|
||||
max(len as u16 + 2, data_area.width / 2),
|
||||
data_area.width,
|
||||
)),
|
||||
Constraint::Min(0),
|
||||
]
|
||||
} else {
|
||||
[Constraint::Ratio(1, 2); 2]
|
||||
};
|
||||
const VALUE_CONSTRAINTS: [Constraint; 2] =
|
||||
[Constraint::Length(1), Constraint::Min(0)];
|
||||
let details_split_area = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(data_constraints)
|
||||
.split(data_area);
|
||||
let per_detail_area = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(VALUE_CONSTRAINTS)
|
||||
.split(details_split_area[1]);
|
||||
|
||||
f.render_widget(Paragraph::new(labels), details_split_area[0]);
|
||||
f.render_widget(
|
||||
PipeGauge::default()
|
||||
.end_label(format!(
|
||||
"{:3.0}%",
|
||||
battery_details.charge_percentage.round()
|
||||
))
|
||||
.ratio(battery_details.charge_percentage / 100.0)
|
||||
.style(if battery_details.charge_percentage < 10.0 {
|
||||
painter.colours.low_battery_colour
|
||||
} else if battery_details.charge_percentage < 50.0 {
|
||||
painter.colours.medium_battery_colour
|
||||
} else {
|
||||
painter.colours.high_battery_colour
|
||||
}),
|
||||
per_detail_area[0],
|
||||
);
|
||||
f.render_widget(
|
||||
Paragraph::new(vec![
|
||||
Spans::from(Span::styled(
|
||||
battery_details.watt_consumption.clone(),
|
||||
painter.colours.text_style,
|
||||
)),
|
||||
match &battery_details.charge_times {
|
||||
crate::data_conversion::BatteryDuration::Charging { short, long }
|
||||
| crate::data_conversion::BatteryDuration::Discharging {
|
||||
short,
|
||||
long,
|
||||
} => Spans::from(Span::styled(
|
||||
if (per_detail_area[1].width as usize) >= long.len() {
|
||||
long
|
||||
} else {
|
||||
short
|
||||
},
|
||||
painter.colours.text_style,
|
||||
)),
|
||||
crate::data_conversion::BatteryDuration::Neither => {
|
||||
Spans::from(Span::styled("N/A", painter.colours.text_style))
|
||||
}
|
||||
},
|
||||
Spans::from(Span::styled(
|
||||
battery_details.health.clone(),
|
||||
painter.colours.text_style,
|
||||
)),
|
||||
]),
|
||||
per_detail_area[1],
|
||||
);
|
||||
}
|
||||
}
|
||||
// Note the block must be rendered last, to cover up the tabs!
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +200,6 @@ impl Widget for Carousel {
|
||||
|
||||
fn selectable_type(&self) -> SelectableType {
|
||||
if let Some(node) = self.get_currently_selected() {
|
||||
debug!("node: {:?}", node);
|
||||
SelectableType::Redirect(node)
|
||||
} else {
|
||||
SelectableType::Unselectable
|
||||
|
@ -10,7 +10,7 @@ use tui::{
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
event::WidgetEventResult, sort_text_table::SimpleSortableColumn, time_graph::TimeGraphData,
|
||||
event::WidgetEventResult, text_table::SimpleColumn, time_graph::TimeGraphData,
|
||||
AppConfigFields, AppScrollWidgetState, CanvasTableWidthState, Component, DataCollection,
|
||||
TextTable, TimeGraph, Widget,
|
||||
},
|
||||
@ -79,7 +79,7 @@ pub enum CpuGraphLegendPosition {
|
||||
/// A widget designed to show CPU usage via a graph, along with a side legend in a table.
|
||||
pub struct CpuGraph {
|
||||
graph: TimeGraph,
|
||||
legend: TextTable<SimpleSortableColumn>,
|
||||
legend: TextTable<SimpleColumn>,
|
||||
legend_position: CpuGraphLegendPosition,
|
||||
showing_avg: bool,
|
||||
|
||||
@ -98,9 +98,10 @@ impl CpuGraph {
|
||||
pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
|
||||
let graph = TimeGraph::from_config(app_config_fields);
|
||||
let legend = TextTable::new(vec![
|
||||
SimpleSortableColumn::new_flex("CPU".into(), None, false, 0.5),
|
||||
SimpleSortableColumn::new_flex("Use%".into(), None, false, 0.5),
|
||||
]);
|
||||
SimpleColumn::new_flex("CPU".into(), 0.5),
|
||||
SimpleColumn::new_hard("Use%".into(), None),
|
||||
])
|
||||
.default_ltr(false);
|
||||
let legend_position = if app_config_fields.left_legend {
|
||||
CpuGraphLegendPosition::Left
|
||||
} else {
|
||||
|
@ -144,7 +144,7 @@ impl Widget for TempTable {
|
||||
} else {
|
||||
painter.colours.border_style
|
||||
})
|
||||
.borders(self.block_border.clone()); // TODO: Also do the scrolling indicator!
|
||||
.borders(self.block_border.clone());
|
||||
|
||||
self.table
|
||||
.draw_tui_table(painter, f, &self.display_data, block, area, selected);
|
||||
|
@ -86,10 +86,7 @@ impl<'a> Widget for PipeGauge<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
let ratio = self.ratio;
|
||||
let start_label = self
|
||||
.start_label
|
||||
.unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0)));
|
||||
let start_label = self.start_label.unwrap_or_else(move || Spans::from(""));
|
||||
|
||||
let (col, row) = buf.set_spans(
|
||||
gauge_area.left(),
|
||||
@ -131,8 +128,8 @@ impl<'a> Widget for PipeGauge<'a> {
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
}
|
||||
for col in end..gauge_end {
|
||||
buf.get_mut(col, row).set_symbol(" ");
|
||||
}
|
||||
// for col in end..gauge_end {
|
||||
// buf.get_mut(col, row).set_symbol(" ");
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
@ -131,15 +131,15 @@ pub fn draw_battery_display<B: Backend>(
|
||||
]),
|
||||
Row::new(vec!["Consumption", &battery_details.watt_consumption])
|
||||
.style(painter.colours.text_style),
|
||||
if let Some(duration_until_full) = &battery_details.duration_until_full {
|
||||
Row::new(vec!["Time to full", duration_until_full])
|
||||
.style(painter.colours.text_style)
|
||||
} else if let Some(duration_until_empty) = &battery_details.duration_until_empty {
|
||||
Row::new(vec!["Time to empty", duration_until_empty])
|
||||
.style(painter.colours.text_style)
|
||||
} else {
|
||||
Row::new(vec!["Time to full/empty", "N/A"]).style(painter.colours.text_style)
|
||||
},
|
||||
// if let Some(duration_until_full) = &battery_details.duration_until_full {
|
||||
// Row::new(vec!["Time to full", duration_until_full])
|
||||
// .style(painter.colours.text_style)
|
||||
// } else if let Some(duration_until_empty) = &battery_details.duration_until_empty {
|
||||
// Row::new(vec!["Time to empty", duration_until_empty])
|
||||
// .style(painter.colours.text_style)
|
||||
// } else {
|
||||
// Row::new(vec!["Time to full/empty", "N/A"]).style(painter.colours.text_style)
|
||||
// },
|
||||
Row::new(vec!["Health %", &battery_details.health])
|
||||
.style(painter.colours.text_style),
|
||||
];
|
||||
|
@ -17,13 +17,25 @@ use std::collections::{HashMap, VecDeque};
|
||||
/// Point is of time, data
|
||||
type Point = (f64, f64);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BatteryDuration {
|
||||
Charging { short: String, long: String },
|
||||
Discharging { short: String, long: String },
|
||||
Neither,
|
||||
}
|
||||
|
||||
impl Default for BatteryDuration {
|
||||
fn default() -> Self {
|
||||
Self::Neither
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ConvertedBatteryData {
|
||||
pub battery_name: String,
|
||||
pub charge_percentage: f64,
|
||||
pub watt_consumption: String,
|
||||
pub duration_until_full: Option<String>,
|
||||
pub duration_until_empty: Option<String>,
|
||||
pub charge_times: BatteryDuration,
|
||||
pub health: String,
|
||||
}
|
||||
|
||||
@ -1362,37 +1374,40 @@ pub fn convert_battery_harvest(current_data: &DataCollection) -> Vec<ConvertedBa
|
||||
battery_name: format!("Battery {}", itx),
|
||||
charge_percentage: battery_harvest.charge_percent,
|
||||
watt_consumption: format!("{:.2}W", battery_harvest.power_consumption_rate_watts),
|
||||
duration_until_empty: if let Some(secs_till_empty) = battery_harvest.secs_until_empty {
|
||||
charge_times: if let Some(secs_till_empty) = battery_harvest.secs_until_empty {
|
||||
let time = chrono::Duration::seconds(secs_till_empty);
|
||||
let num_minutes = time.num_minutes() - time.num_hours() * 60;
|
||||
let num_seconds = time.num_seconds() - time.num_minutes() * 60;
|
||||
Some(format!(
|
||||
"{} hour{}, {} minute{}, {} second{}",
|
||||
time.num_hours(),
|
||||
if time.num_hours() == 1 { "" } else { "s" },
|
||||
num_minutes,
|
||||
if num_minutes == 1 { "" } else { "s" },
|
||||
num_seconds,
|
||||
if num_seconds == 1 { "" } else { "s" },
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
duration_until_full: if let Some(secs_till_full) = battery_harvest.secs_until_full {
|
||||
BatteryDuration::Discharging {
|
||||
long: format!(
|
||||
"{} hour{}, {} minute{}, {} second{}",
|
||||
time.num_hours(),
|
||||
if time.num_hours() == 1 { "" } else { "s" },
|
||||
num_minutes,
|
||||
if num_minutes == 1 { "" } else { "s" },
|
||||
num_seconds,
|
||||
if num_seconds == 1 { "" } else { "s" },
|
||||
),
|
||||
short: format!("{}:{:02}:{:02}", time.num_hours(), num_minutes, num_seconds),
|
||||
}
|
||||
} else if let Some(secs_till_full) = battery_harvest.secs_until_full {
|
||||
let time = chrono::Duration::seconds(secs_till_full); // FIXME [DEP]: Can I get rid of chrono?
|
||||
let num_minutes = time.num_minutes() - time.num_hours() * 60;
|
||||
let num_seconds = time.num_seconds() - time.num_minutes() * 60;
|
||||
Some(format!(
|
||||
"{} hour{}, {} minute{}, {} second{}",
|
||||
time.num_hours(),
|
||||
if time.num_hours() == 1 { "" } else { "s" },
|
||||
num_minutes,
|
||||
if num_minutes == 1 { "" } else { "s" },
|
||||
num_seconds,
|
||||
if num_seconds == 1 { "" } else { "s" },
|
||||
))
|
||||
BatteryDuration::Charging {
|
||||
long: format!(
|
||||
"{} hour{}, {} minute{}, {} second{}",
|
||||
time.num_hours(),
|
||||
if time.num_hours() == 1 { "" } else { "s" },
|
||||
num_minutes,
|
||||
if num_minutes == 1 { "" } else { "s" },
|
||||
num_seconds,
|
||||
if num_seconds == 1 { "" } else { "s" },
|
||||
),
|
||||
short: format!("{}:{:02}:{:02}", time.num_hours(), num_minutes, num_seconds),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
BatteryDuration::Neither
|
||||
},
|
||||
health: format!("{:.2}%", battery_harvest.health_percent),
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user