Basic temp

This commit is contained in:
ClementTsang 2021-12-28 03:52:34 -05:00
parent 18bce9f0a0
commit a78edc88c0
23 changed files with 338 additions and 118 deletions

View File

@ -29,6 +29,7 @@ use frozen_state::FrozenState;
use crate::{
canvas::Painter,
constants,
data_conversion::ConvertedData,
tuine::{Application, Element, Flex, Status, ViewContext},
units::data_units::DataUnit,
Pid,
@ -129,7 +130,13 @@ impl Default for CurrentScreen {
pub enum AppMessages {
Update(Box<data_harvester::Data>),
OpenHelp,
KillProcess { to_kill: Vec<Pid> },
ConfirmKillProcess {
to_kill: Vec<Pid>,
},
KillProcess {
to_kill: Vec<Pid>,
signal: Option<i32>,
},
ToggleFreeze,
Reset,
Clean,
@ -209,27 +216,34 @@ impl AppState {
impl Application for AppState {
type Message = AppMessages;
fn update(&mut self, message: Self::Message) {
fn update(&mut self, message: Self::Message) -> bool {
match message {
AppMessages::Update(new_data) => {
self.data_collection.eat_data(new_data);
true
}
AppMessages::OpenHelp => {
self.set_current_screen(CurrentScreen::Help);
true
}
AppMessages::KillProcess { to_kill } => {}
AppMessages::ConfirmKillProcess { to_kill } => true,
AppMessages::KillProcess { to_kill, signal } => true,
AppMessages::ToggleFreeze => {
self.frozen_state.toggle(&self.data_collection);
true
}
AppMessages::Clean => {
self.data_collection
.clean_data(constants::STALE_MAX_MILLISECONDS);
false
}
AppMessages::Quit => {
self.terminator.store(true, SeqCst);
false
}
AppMessages::Reset => {
// FIXME: Reset
true
}
}
}
@ -243,10 +257,21 @@ impl Application for AppState {
use crate::tuine::StatefulComponent;
use crate::tuine::{TempTable, TextTable, TextTableProps};
let data = match &self.frozen_state {
FrozenState::NotFrozen => &self.data_collection,
FrozenState::Frozen(frozen_data_collection) => &frozen_data_collection,
};
let mut converted_data = ConvertedData::default();
Flex::column()
.with_flex_child(
Flex::row_with_children(vec![
FlexElement::new(TempTable::build(ctx)),
FlexElement::new(TempTable::build(
ctx,
&self.painter,
converted_data.temp_table(data, self.app_config_fields.temperature_type),
)),
FlexElement::new(TextTable::build(
ctx,
TextTableProps::new(vec!["D", "E", "F"]),

View File

@ -175,6 +175,7 @@ impl Default for DataCollection {
}
}
// TODO: Just rip this out, store only stringified data...?
impl DataCollection {
pub fn reset(&mut self) {
self.timed_data_vec = Default::default();

View File

@ -23,7 +23,7 @@ pub struct TempHarvest {
pub temperature: f32,
}
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug)]
pub enum TemperatureType {
Celsius,
Kelvin,

View File

@ -7,6 +7,47 @@ use crate::{app::AxisScaling, units::data_units::DataUnit};
use std::borrow::Cow;
/// Stores converted data, and caches results.
#[derive(Default)]
pub struct ConvertedData {
temp_table: Option<Vec<Vec<Cow<'static, str>>>>,
}
impl ConvertedData {
pub fn temp_table(
&mut self, data: &DataCollection, temp_type: TemperatureType,
) -> Vec<Vec<Cow<'static, str>>> {
match &self.temp_table {
Some(temp_table) => temp_table.clone(),
None => {
let temp_table = if data.temp_harvest.is_empty() {
vec![vec!["No Sensors Found".into(), "".into()]]
} else {
let unit = match temp_type {
data_harvester::temperature::TemperatureType::Celsius => "°C",
data_harvester::temperature::TemperatureType::Kelvin => "K",
data_harvester::temperature::TemperatureType::Fahrenheit => "°F",
};
data.temp_harvest
.iter()
.map(|temp_harvest| {
let val = temp_harvest.temperature.ceil().to_string();
vec![
temp_harvest.name.clone().into(),
format!("{}{}", val, unit).into(),
]
})
.collect()
};
self.temp_table = Some(temp_table.clone());
temp_table
}
}
}
}
/// Point is of time, data
type Point = (f64, f64);

View File

@ -14,8 +14,8 @@ pub type CrosstermBackend = tui::backend::CrosstermBackend<std::io::Stdout>;
pub trait Application: Sized {
type Message: Debug;
/// Determines how to handle a given message.
fn update(&mut self, message: Self::Message);
/// Determines how to handle a given message, and returns `true` if this update should trigger a redraw.
fn update(&mut self, message: Self::Message) -> bool;
/// Returns whether to stop the application. Defaults to
/// always returning false.

View File

@ -9,8 +9,7 @@ use crate::tuine::{
/// A set of styles for a [`Block`].
#[derive(Clone, Debug, Default)]
pub struct StyleSheet {
text: Style,
border: Style,
pub border: Style,
}
/// A [`Block`] is a widget that draws a border around a child [`Component`], as well as optional
@ -47,6 +46,11 @@ where
self
}
pub fn style(mut self, style: StyleSheet) -> Self {
self.style_sheet = style;
self
}
fn inner_rect(&self, original: Rect) -> Rect {
let mut inner = original;

View File

@ -5,12 +5,14 @@ mod table_scroll_state;
use self::table_scroll_state::ScrollState;
pub mod data_row;
use crossterm::event::KeyCode;
pub use data_row::DataRow;
pub mod data_cell;
pub use data_cell::DataCell;
pub mod sort_type;
pub use sort_type::SortType;
pub mod props;
@ -35,9 +37,9 @@ use crate::{
/// A set of styles for a [`TextTable`].
#[derive(Clone, Debug, Default)]
pub struct StyleSheet {
text: Style,
selected_text: Style,
table_header: Style,
pub text: Style,
pub selected_text: Style,
pub table_header: Style,
}
#[derive(PartialEq, Default)]
@ -108,6 +110,71 @@ impl<Message> TextTable<Message> {
self.column_widths = column_widths;
}
fn update_sort_column(&mut self, state: &mut TextTableState, x: u16) -> Status {
match state.sort {
SortType::Unsortable => Status::Ignored,
SortType::Ascending(column) | SortType::Descending(column) => {
let mut cursor = 0;
for (selected_column, width) in self.column_widths.iter().enumerate() {
if x >= cursor && x <= cursor + width {
match state.sort {
SortType::Ascending(_) => {
if selected_column == column {
// FIXME: This should handle default sorting orders...
state.sort = SortType::Descending(selected_column);
} else {
state.sort = SortType::Ascending(selected_column);
}
}
SortType::Descending(_) => {
if selected_column == column {
// FIXME: This should handle default sorting orders...
state.sort = SortType::Ascending(selected_column);
} else {
state.sort = SortType::Descending(selected_column);
}
}
SortType::Unsortable => unreachable!(), // Should be impossible by above check.
}
return Status::Captured;
} else {
cursor += width;
}
}
Status::Ignored
}
}
}
pub fn on_page_up(&mut self, state: &mut TextTableState, rect: Rect) -> Status {
let height = rect.height.saturating_sub(self.table_gap + 1);
state.scroll.move_up(height.into())
}
pub fn on_page_down(&mut self, state: &mut TextTableState, rect: Rect) -> Status {
let height = rect.height.saturating_sub(self.table_gap + 1);
state.scroll.move_down(height.into())
}
pub fn scroll_down(
&mut self, state: &mut TextTableState, messages: &mut Vec<Message>,
) -> Status {
let status = state.scroll.move_down(1);
if let Some(on_select) = &self.on_select {
messages.push(on_select(state.scroll.current_index()));
}
status
}
pub fn scroll_up(&mut self, state: &mut TextTableState, messages: &mut Vec<Message>) -> Status {
let status = state.scroll.move_up(1);
if let Some(on_select) = &self.on_select {
messages.push(on_select(state.scroll.current_index()));
}
status
}
}
impl<Message> StatefulComponent<Message> for TextTable<Message> {
@ -190,9 +257,31 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
};
// Now build up our headers...
let header = Row::new(self.columns.iter().map(|column| column.name.clone()))
.style(self.style_sheet.table_header)
.bottom_margin(self.table_gap);
let header = match state.sort {
SortType::Unsortable => Row::new(self.columns.iter().map(|column| column.name.clone())),
SortType::Ascending(sort_column) => {
Row::new(self.columns.iter().enumerate().map(|(index, column)| {
const UP_ARROW: &str = "";
if index == sort_column {
format!("{}{}", column.name, UP_ARROW).into()
} else {
column.name.clone()
}
}))
}
SortType::Descending(sort_column) => {
Row::new(self.columns.iter().enumerate().map(|(index, column)| {
const DOWN_ARROW: &str = "";
if index == sort_column {
format!("{}{}", column.name, DOWN_ARROW).into()
} else {
column.name.clone()
}
}))
}
}
.style(self.style_sheet.table_header)
.bottom_margin(self.table_gap);
let mut table = Table::new(data_slice)
.header(header)
@ -219,6 +308,10 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
Event::Keyboard(key_event) => {
if key_event.modifiers.is_empty() {
match key_event.code {
KeyCode::PageUp => self.on_page_up(state, rect),
KeyCode::PageDown => self.on_page_down(state, rect),
KeyCode::Up => self.scroll_up(state, messages),
KeyCode::Down => self.scroll_down(state, messages),
_ => Status::Ignored,
}
} else {
@ -232,52 +325,7 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
let y = mouse_event.row - rect.top();
if y == 0 {
let x = mouse_event.column - rect.left();
match state.sort {
SortType::Unsortable => Status::Ignored,
SortType::Ascending(column) | SortType::Descending(column) => {
let mut cursor = 0;
for (selected_column, width) in
self.column_widths.iter().enumerate()
{
let end = cursor + width;
if x >= cursor && x <= end {
match state.sort {
SortType::Ascending(_) => {
if selected_column == column {
// FIXME: This should handle default sorting orders...
state.sort = SortType::Descending(
selected_column,
);
} else {
state.sort = SortType::Ascending(
selected_column,
);
}
}
SortType::Descending(_) => {
if selected_column == column {
// FIXME: This should handle default sorting orders...
state.sort = SortType::Ascending(
selected_column,
);
} else {
state.sort = SortType::Descending(
selected_column,
);
}
}
SortType::Unsortable => unreachable!(), // Should be impossible by above check.
}
return Status::Captured;
} else {
cursor += width;
}
}
Status::Ignored
}
}
self.update_sort_column(state, x)
} else if y > self.table_gap {
let visual_index = usize::from(y - self.table_gap);
match state.scroll.set_visual_index(visual_index) {
@ -297,20 +345,8 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
Status::Ignored
}
}
MouseEventKind::ScrollDown => {
let status = state.scroll.move_down(1);
if let Some(on_select) = &self.on_select {
messages.push(on_select(state.scroll.current_index()));
}
status
}
MouseEventKind::ScrollUp => {
let status = state.scroll.move_up(1);
if let Some(on_select) = &self.on_select {
messages.push(on_select(state.scroll.current_index()));
}
status
}
MouseEventKind::ScrollDown => self.scroll_down(state, messages),
MouseEventKind::ScrollUp => self.scroll_up(state, messages),
_ => Status::Ignored,
}
} else {

View File

@ -94,6 +94,12 @@ impl<Message> TextTableProps<Message> {
self
}
/// Sets the style for the entry.
pub fn style(mut self, style: StyleSheet) -> Self {
self.style_sheet = style;
self
}
pub(crate) fn try_sort_data(&mut self, sort_type: SortType) {
use std::cmp::Ordering;

View File

@ -1,4 +1,4 @@
use crate::tuine::{State, ViewContext};
use crate::tuine::{State, StateContext, ViewContext};
use super::TmpComponent;

View File

View File

View File

View File

View File

View File

@ -1,2 +1,32 @@
pub mod simple_table;
pub use simple_table::*;
pub mod cpu_graph;
pub use cpu_graph::*;
pub mod disk_table;
pub use disk_table::*;
pub mod mem_graph;
pub use mem_graph::*;
pub mod net_graph;
pub use net_graph::*;
pub mod process_table;
pub use process_table::*;
pub mod temp_table;
pub use temp_table::*;
pub mod battery_table;
pub use battery_table::*;
pub mod cpu_simple;
pub use cpu_simple::*;
pub mod mem_simple;
pub use mem_simple::*;
pub mod net_simple;
pub use net_simple::*;

View File

View File

View File

@ -0,0 +1,72 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tui::style::Style;
use crate::tuine::{
self, block,
text_table::{self, DataRow, SortType, TextTableProps},
Block, Event, Shortcut, StatefulComponent, Status, TextTable, TmpComponent, ViewContext,
};
/// A set of styles for a [`SimpleTable`].
#[derive(Default)]
pub struct StyleSheet {
pub text: Style,
pub selected_text: Style,
pub table_header: Style,
pub border: Style,
}
/// A [`SimpleTable`] is a wrapper around a [`TextTable`] with basic shortcut support already added for:
/// - Skipping to the start/end of the table
/// - Scrolling up/down by a page
/// - Configurable sorting options
pub struct SimpleTable<Message> {
inner: Block<Message, Shortcut<Message, TextTable<Message>>>,
}
impl<Message> SimpleTable<Message> {
#[track_caller]
pub fn build<C: Into<std::borrow::Cow<'static, str>>, R: Into<DataRow>>(
ctx: &mut ViewContext<'_>, style: StyleSheet, columns: Vec<C>, data: Vec<R>,
) -> Self {
let shortcut = Shortcut::with_child(TextTable::build(
ctx,
TextTableProps::new(columns)
.rows(data)
.default_sort(SortType::Ascending(1))
.style(text_table::StyleSheet {
text: style.text,
selected_text: style.selected_text,
table_header: style.table_header,
}),
));
Self {
inner: Block::with_child(shortcut).style(block::StyleSheet {
border: style.border,
}),
}
}
}
impl<Message> TmpComponent<Message> for SimpleTable<Message> {
fn draw<Backend>(
&mut self, state_ctx: &mut tuine::StateContext<'_>, draw_ctx: &tuine::DrawContext<'_>,
frame: &mut tui::Frame<'_, Backend>,
) where
Backend: tui::backend::Backend,
{
self.inner.draw(state_ctx, draw_ctx, frame);
}
fn on_event(
&mut self, state_ctx: &mut tuine::StateContext<'_>, draw_ctx: &tuine::DrawContext<'_>,
event: tuine::Event, messages: &mut Vec<Message>,
) -> tuine::Status {
self.inner.on_event(state_ctx, draw_ctx, event, messages)
}
fn layout(&self, bounds: tuine::Bounds, node: &mut tuine::LayoutNode) -> tuine::Size {
self.inner.layout(bounds, node)
}
}

View File

@ -1,35 +1,41 @@
use crate::tuine::{
text_table::{DataRow, SortType, TextTableProps},
Block, Shortcut, StatefulComponent, TextTable, TmpComponent, ViewContext,
use crate::{
canvas::Painter,
tuine::{
Bounds, DataRow, DrawContext, LayoutNode, SimpleTable, Size, StateContext, Status,
TmpComponent, ViewContext,
},
};
/// A [`TempTable`] is a text table that is meant to display temperature data.
use super::simple_table;
/// A [`TempTable`] is a table displaying temperature data.
///
/// It wraps a [`SimpleTable`], with set columns and manages extracting data and styling.
pub struct TempTable<Message> {
inner: Block<Message, Shortcut<Message, TextTable<Message>>>,
inner: SimpleTable<Message>,
}
impl<Message> TempTable<Message> {
#[track_caller]
pub fn build(ctx: &mut ViewContext<'_>) -> Self {
pub fn build<R: Into<DataRow>>(
ctx: &mut ViewContext<'_>, painter: &Painter, data: Vec<R>,
) -> Self {
let style = simple_table::StyleSheet {
text: painter.colours.text_style,
selected_text: painter.colours.currently_selected_text_style,
table_header: painter.colours.table_header_style,
border: painter.colours.border_style,
};
Self {
inner: Block::with_child(Shortcut::with_child(TextTable::build(
ctx,
TextTableProps::new(vec!["Sensor", "Temp"])
.rows(vec![
DataRow::default().cell("A").cell(2),
DataRow::default().cell("B").cell(3),
DataRow::default().cell("C").cell(1),
])
.default_sort(SortType::Ascending(1)),
))),
inner: SimpleTable::build(ctx, style, vec!["Sensor", "Temp"], data),
}
}
}
impl<Message> TmpComponent<Message> for TempTable<Message> {
fn draw<Backend>(
&mut self, state_ctx: &mut crate::tuine::StateContext<'_>,
draw_ctx: &crate::tuine::DrawContext<'_>, frame: &mut tui::Frame<'_, Backend>,
&mut self, state_ctx: &mut StateContext<'_>, draw_ctx: &DrawContext<'_>,
frame: &mut tui::Frame<'_, Backend>,
) where
Backend: tui::backend::Backend,
{
@ -37,16 +43,13 @@ impl<Message> TmpComponent<Message> for TempTable<Message> {
}
fn on_event(
&mut self, state_ctx: &mut crate::tuine::StateContext<'_>,
draw_ctx: &crate::tuine::DrawContext<'_>, event: crate::tuine::Event,
messages: &mut Vec<Message>,
) -> crate::tuine::Status {
&mut self, state_ctx: &mut StateContext<'_>, draw_ctx: &DrawContext<'_>,
event: crate::tuine::Event, messages: &mut Vec<Message>,
) -> Status {
self.inner.on_event(state_ctx, draw_ctx, event, messages)
}
fn layout(
&self, bounds: crate::tuine::Bounds, node: &mut crate::tuine::LayoutNode,
) -> crate::tuine::Size {
fn layout(&self, bounds: Bounds, node: &mut LayoutNode) -> Size {
self.inner.layout(bounds, node)
}
}

View File

@ -3,7 +3,7 @@ use tui::Frame;
use super::{
Block, Bounds, Carousel, Container, DrawContext, Empty, Event, Flex, LayoutNode, Shortcut,
Size, StateContext, Status, TempTable, TextTable, TmpComponent,
SimpleTable, Size, StateContext, Status, TempTable, TextTable, TmpComponent,
};
/// An [`Element`] is an instantiated [`Component`].
@ -19,5 +19,6 @@ where
Shortcut(Shortcut<Message, C>),
TextTable(TextTable<Message>),
Empty,
SimpleTable(SimpleTable<Message>),
TempTable(TempTable<Message>),
}

View File

@ -41,27 +41,22 @@ where
if let Ok(event) = receiver.recv() {
match event {
RuntimeEvent::UserInterface(event) => {
match on_event(
if on_event(
&mut application,
&mut user_interface,
&mut app_data,
&mut layout,
event,
) {
Status::Captured => {
// Hmm... is this really needed? Or is it fine to redraw once then do the termination check?
if application.is_terminated() {
break;
}
user_interface = new_user_interface(&mut application, &mut app_data);
draw(&mut user_interface, terminal, &mut app_data, &mut layout)?;
}
Status::Ignored => {}
user_interface = new_user_interface(&mut application, &mut app_data);
draw(&mut user_interface, terminal, &mut app_data, &mut layout)?;
}
}
RuntimeEvent::Custom(message) => {
application.update(message);
if application.update(message) {
user_interface = new_user_interface(&mut application, &mut app_data);
draw(&mut user_interface, terminal, &mut app_data, &mut layout)?;
}
}
RuntimeEvent::Resize {
width: _,
@ -86,7 +81,7 @@ where
fn on_event<A>(
application: &mut A, user_interface: &mut Element<A::Message>, app_data: &mut AppData,
layout: &mut LayoutNode, event: Event,
) -> Status
) -> bool
where
A: Application + 'static,
{
@ -103,12 +98,18 @@ where
Status::Ignored => application.global_event_handler(event, &mut messages),
};
let mut should_redraw = match event_handled {
Status::Captured => true,
Status::Ignored => false,
};
for msg in messages {
debug!("Message: {:?}", msg); // FIXME: Remove this debug line!
application.update(msg);
let msg_result = application.update(msg);
should_redraw = should_redraw || msg_result;
}
event_handled
should_redraw
}
/// Creates a new [`Element`] representing the root of the user interface.