mirror of
https://github.com/ClementTsang/bottom
synced 2024-12-11 21:22:38 +00:00
commit
22cdc005bc
25 changed files with 5814 additions and 5032 deletions
12
README.md
12
README.md
|
@ -30,6 +30,8 @@ Features of bottom include:
|
||||||
|
|
||||||
- Maximizing of widgets of interest to take up the entire window.
|
- Maximizing of widgets of interest to take up the entire window.
|
||||||
|
|
||||||
|
- Basic mode
|
||||||
|
|
||||||
More details about each widget and compatibility can be found [here](./docs/widgets.md).
|
More details about each widget and compatibility can be found [here](./docs/widgets.md).
|
||||||
|
|
||||||
## Config files
|
## Config files
|
||||||
|
@ -75,7 +77,7 @@ sudo dpkg -i bottom_0.2.2_amd64.deb
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
You can get release versions via [Chocolatey](https://chocolatey.org/packages/bottom/):
|
You can get release versions via [Chocolatey](https://chocolatey.org/packages/bottom/) (note it may take a while to be available due to moderation/review):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
choco install bottom --version=0.2.1
|
choco install bottom --version=0.2.1
|
||||||
|
@ -86,10 +88,10 @@ choco install bottom --version=0.2.1
|
||||||
You can get release versions using Homebrew:
|
You can get release versions using Homebrew:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ brew tap clementtsang/bottom
|
brew tap clementtsang/bottom
|
||||||
$ brew install bottom
|
brew install bottom
|
||||||
# Or
|
# Or
|
||||||
$ brew install clementtsang/bottom/bottom
|
brew install clementtsang/bottom/bottom
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
@ -134,6 +136,8 @@ Run using `btm`.
|
||||||
|
|
||||||
- `-C`, `--config` takes in a file path leading to a TOML file. If doesn't exist, creates one.
|
- `-C`, `--config` takes in a file path leading to a TOML file. If doesn't exist, creates one.
|
||||||
|
|
||||||
|
- `-b`, `--basic` will enable basic mode, removing all graphs from the main interface and condensing data.
|
||||||
|
|
||||||
### Keybindings
|
### Keybindings
|
||||||
|
|
||||||
#### General
|
#### General
|
||||||
|
|
|
@ -3,7 +3,6 @@ max_width = 100
|
||||||
newline_style = "Unix"
|
newline_style = "Unix"
|
||||||
reorder_imports = true
|
reorder_imports = true
|
||||||
fn_args_layout = "Compressed"
|
fn_args_layout = "Compressed"
|
||||||
hard_tabs = true
|
|
||||||
merge_derives = true
|
merge_derives = true
|
||||||
reorder_modules = true
|
reorder_modules = true
|
||||||
tab_spaces = 4
|
tab_spaces = 4
|
||||||
|
|
|
@ -34,6 +34,9 @@
|
||||||
#default_widget = "network_default"
|
#default_widget = "network_default"
|
||||||
#default_widget = "process_default"
|
#default_widget = "process_default"
|
||||||
|
|
||||||
|
# Basic mode
|
||||||
|
#basic = true
|
||||||
|
|
||||||
|
|
||||||
# These are all the components that support custom theming. Currently, it only
|
# These are all the components that support custom theming. Currently, it only
|
||||||
# supports taking in a string representing a hex colour. Note that colour support
|
# supports taking in a string representing a hex colour. Note that colour support
|
||||||
|
|
2472
src/app.rs
2472
src/app.rs
File diff suppressed because it is too large
Load diff
|
@ -23,14 +23,14 @@ pub type JoinedDataPoints = (Value, Vec<(TimeOffset, Value)>);
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct TimedData {
|
pub struct TimedData {
|
||||||
pub rx_data: JoinedDataPoints,
|
pub rx_data: JoinedDataPoints,
|
||||||
pub tx_data: JoinedDataPoints,
|
pub tx_data: JoinedDataPoints,
|
||||||
pub cpu_data: Vec<JoinedDataPoints>,
|
pub cpu_data: Vec<JoinedDataPoints>,
|
||||||
pub mem_data: JoinedDataPoints,
|
pub mem_data: JoinedDataPoints,
|
||||||
pub swap_data: JoinedDataPoints,
|
pub swap_data: JoinedDataPoints,
|
||||||
// Unused for now
|
// Unused for now
|
||||||
// pub io_data : JoinedDataPoints
|
// pub io_data : JoinedDataPoints
|
||||||
// pub temp_data: JoinedDataPoints,
|
// pub temp_data: JoinedDataPoints,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AppCollection represents the pooled data stored within the main app
|
/// AppCollection represents the pooled data stored within the main app
|
||||||
|
@ -44,253 +44,253 @@ pub struct TimedData {
|
||||||
/// not the data collector.
|
/// not the data collector.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DataCollection {
|
pub struct DataCollection {
|
||||||
pub current_instant: Instant,
|
pub current_instant: Instant,
|
||||||
pub timed_data_vec: Vec<(Instant, TimedData)>,
|
pub timed_data_vec: Vec<(Instant, TimedData)>,
|
||||||
pub network_harvest: network::NetworkHarvest,
|
pub network_harvest: network::NetworkHarvest,
|
||||||
pub memory_harvest: mem::MemHarvest,
|
pub memory_harvest: mem::MemHarvest,
|
||||||
pub swap_harvest: mem::MemHarvest,
|
pub swap_harvest: mem::MemHarvest,
|
||||||
pub cpu_harvest: cpu::CPUHarvest,
|
pub cpu_harvest: cpu::CPUHarvest,
|
||||||
pub process_harvest: Vec<processes::ProcessHarvest>,
|
pub process_harvest: Vec<processes::ProcessHarvest>,
|
||||||
pub disk_harvest: Vec<disks::DiskHarvest>,
|
pub disk_harvest: Vec<disks::DiskHarvest>,
|
||||||
pub io_harvest: disks::IOHarvest,
|
pub io_harvest: disks::IOHarvest,
|
||||||
pub io_labels: Vec<(u64, u64)>,
|
pub io_labels: Vec<(u64, u64)>,
|
||||||
io_prev: Vec<(u64, u64)>,
|
io_prev: Vec<(u64, u64)>,
|
||||||
pub temp_harvest: Vec<temperature::TempHarvest>,
|
pub temp_harvest: Vec<temperature::TempHarvest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DataCollection {
|
impl Default for DataCollection {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
DataCollection {
|
DataCollection {
|
||||||
current_instant: Instant::now(),
|
current_instant: Instant::now(),
|
||||||
timed_data_vec: Vec::default(),
|
timed_data_vec: Vec::default(),
|
||||||
network_harvest: network::NetworkHarvest::default(),
|
network_harvest: network::NetworkHarvest::default(),
|
||||||
memory_harvest: mem::MemHarvest::default(),
|
memory_harvest: mem::MemHarvest::default(),
|
||||||
swap_harvest: mem::MemHarvest::default(),
|
swap_harvest: mem::MemHarvest::default(),
|
||||||
cpu_harvest: cpu::CPUHarvest::default(),
|
cpu_harvest: cpu::CPUHarvest::default(),
|
||||||
process_harvest: Vec::default(),
|
process_harvest: Vec::default(),
|
||||||
disk_harvest: Vec::default(),
|
disk_harvest: Vec::default(),
|
||||||
io_harvest: disks::IOHarvest::default(),
|
io_harvest: disks::IOHarvest::default(),
|
||||||
io_labels: Vec::default(),
|
io_labels: Vec::default(),
|
||||||
io_prev: Vec::default(),
|
io_prev: Vec::default(),
|
||||||
temp_harvest: Vec::default(),
|
temp_harvest: Vec::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DataCollection {
|
impl DataCollection {
|
||||||
pub fn clean_data(&mut self, max_time_millis: u128) {
|
pub fn clean_data(&mut self, max_time_millis: u128) {
|
||||||
let current_time = Instant::now();
|
let current_time = Instant::now();
|
||||||
|
|
||||||
let mut remove_index = 0;
|
let mut remove_index = 0;
|
||||||
for entry in &self.timed_data_vec {
|
for entry in &self.timed_data_vec {
|
||||||
if current_time.duration_since(entry.0).as_millis() >= max_time_millis {
|
if current_time.duration_since(entry.0).as_millis() >= max_time_millis {
|
||||||
remove_index += 1;
|
remove_index += 1;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.timed_data_vec.drain(0..remove_index);
|
self.timed_data_vec.drain(0..remove_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn eat_data(&mut self, harvested_data: &Data) {
|
pub fn eat_data(&mut self, harvested_data: &Data) {
|
||||||
let harvested_time = harvested_data.last_collection_time;
|
let harvested_time = harvested_data.last_collection_time;
|
||||||
let mut new_entry = TimedData::default();
|
let mut new_entry = TimedData::default();
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
self.eat_network(&harvested_data, harvested_time, &mut new_entry);
|
self.eat_network(&harvested_data, harvested_time, &mut new_entry);
|
||||||
|
|
||||||
// Memory and Swap
|
// Memory and Swap
|
||||||
self.eat_memory_and_swap(&harvested_data, harvested_time, &mut new_entry);
|
self.eat_memory_and_swap(&harvested_data, harvested_time, &mut new_entry);
|
||||||
|
|
||||||
// CPU
|
// CPU
|
||||||
self.eat_cpu(&harvested_data, harvested_time, &mut new_entry);
|
self.eat_cpu(&harvested_data, harvested_time, &mut new_entry);
|
||||||
|
|
||||||
// Temp
|
// Temp
|
||||||
self.eat_temp(&harvested_data);
|
self.eat_temp(&harvested_data);
|
||||||
|
|
||||||
// Disks
|
// Disks
|
||||||
self.eat_disks(&harvested_data, harvested_time);
|
self.eat_disks(&harvested_data, harvested_time);
|
||||||
|
|
||||||
// Processes
|
// Processes
|
||||||
self.eat_proc(&harvested_data);
|
self.eat_proc(&harvested_data);
|
||||||
|
|
||||||
// And we're done eating. Update time and push the new entry!
|
// And we're done eating. Update time and push the new entry!
|
||||||
self.current_instant = harvested_time;
|
self.current_instant = harvested_time;
|
||||||
self.timed_data_vec.push((harvested_time, new_entry));
|
self.timed_data_vec.push((harvested_time, new_entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eat_memory_and_swap(
|
fn eat_memory_and_swap(
|
||||||
&mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData,
|
&mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData,
|
||||||
) {
|
) {
|
||||||
// Memory
|
// Memory
|
||||||
let mem_percent = harvested_data.memory.mem_used_in_mb as f64
|
let mem_percent = harvested_data.memory.mem_used_in_mb as f64
|
||||||
/ harvested_data.memory.mem_total_in_mb as f64
|
/ harvested_data.memory.mem_total_in_mb as f64
|
||||||
* 100.0;
|
* 100.0;
|
||||||
let mem_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() {
|
let mem_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() {
|
||||||
generate_joining_points(*time, last_pt.mem_data.0, harvested_time, mem_percent)
|
generate_joining_points(*time, last_pt.mem_data.0, harvested_time, mem_percent)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
let mem_pt = (mem_percent, mem_joining_pts);
|
let mem_pt = (mem_percent, mem_joining_pts);
|
||||||
new_entry.mem_data = mem_pt;
|
new_entry.mem_data = mem_pt;
|
||||||
|
|
||||||
// Swap
|
// Swap
|
||||||
if harvested_data.swap.mem_total_in_mb > 0 {
|
if harvested_data.swap.mem_total_in_mb > 0 {
|
||||||
let swap_percent = harvested_data.swap.mem_used_in_mb as f64
|
let swap_percent = harvested_data.swap.mem_used_in_mb as f64
|
||||||
/ harvested_data.swap.mem_total_in_mb as f64
|
/ harvested_data.swap.mem_total_in_mb as f64
|
||||||
* 100.0;
|
* 100.0;
|
||||||
let swap_joining_pt = if let Some((time, last_pt)) = self.timed_data_vec.last() {
|
let swap_joining_pt = if let Some((time, last_pt)) = self.timed_data_vec.last() {
|
||||||
generate_joining_points(*time, last_pt.swap_data.0, harvested_time, swap_percent)
|
generate_joining_points(*time, last_pt.swap_data.0, harvested_time, swap_percent)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
let swap_pt = (swap_percent, swap_joining_pt);
|
let swap_pt = (swap_percent, swap_joining_pt);
|
||||||
new_entry.swap_data = swap_pt;
|
new_entry.swap_data = swap_pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In addition copy over latest data for easy reference
|
// In addition copy over latest data for easy reference
|
||||||
self.memory_harvest = harvested_data.memory.clone();
|
self.memory_harvest = harvested_data.memory.clone();
|
||||||
self.swap_harvest = harvested_data.swap.clone();
|
self.swap_harvest = harvested_data.swap.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eat_network(
|
fn eat_network(
|
||||||
&mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData,
|
&mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData,
|
||||||
) {
|
) {
|
||||||
// RX
|
// RX
|
||||||
let logged_rx_val = if harvested_data.network.rx as f64 > 0.0 {
|
let logged_rx_val = if harvested_data.network.rx as f64 > 0.0 {
|
||||||
(harvested_data.network.rx as f64).log(2.0)
|
(harvested_data.network.rx as f64).log(2.0)
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let rx_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() {
|
let rx_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() {
|
||||||
generate_joining_points(*time, last_pt.rx_data.0, harvested_time, logged_rx_val)
|
generate_joining_points(*time, last_pt.rx_data.0, harvested_time, logged_rx_val)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
let rx_pt = (logged_rx_val, rx_joining_pts);
|
let rx_pt = (logged_rx_val, rx_joining_pts);
|
||||||
new_entry.rx_data = rx_pt;
|
new_entry.rx_data = rx_pt;
|
||||||
|
|
||||||
// TX
|
// TX
|
||||||
let logged_tx_val = if harvested_data.network.tx as f64 > 0.0 {
|
let logged_tx_val = if harvested_data.network.tx as f64 > 0.0 {
|
||||||
(harvested_data.network.tx as f64).log(2.0)
|
(harvested_data.network.tx as f64).log(2.0)
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let tx_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() {
|
let tx_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() {
|
||||||
generate_joining_points(*time, last_pt.tx_data.0, harvested_time, logged_tx_val)
|
generate_joining_points(*time, last_pt.tx_data.0, harvested_time, logged_tx_val)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
let tx_pt = (logged_tx_val, tx_joining_pts);
|
let tx_pt = (logged_tx_val, tx_joining_pts);
|
||||||
new_entry.tx_data = tx_pt;
|
new_entry.tx_data = tx_pt;
|
||||||
|
|
||||||
// In addition copy over latest data for easy reference
|
// In addition copy over latest data for easy reference
|
||||||
self.network_harvest = harvested_data.network.clone();
|
self.network_harvest = harvested_data.network.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eat_cpu(
|
fn eat_cpu(
|
||||||
&mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData,
|
&mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData,
|
||||||
) {
|
) {
|
||||||
// Note this only pre-calculates the data points - the names will be
|
// Note this only pre-calculates the data points - the names will be
|
||||||
// within the local copy of cpu_harvest. Since it's all sequential
|
// within the local copy of cpu_harvest. Since it's all sequential
|
||||||
// it probably doesn't matter anyways.
|
// it probably doesn't matter anyways.
|
||||||
for (itx, cpu) in harvested_data.cpu.iter().enumerate() {
|
for (itx, cpu) in harvested_data.cpu.iter().enumerate() {
|
||||||
let cpu_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() {
|
let cpu_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() {
|
||||||
generate_joining_points(
|
generate_joining_points(
|
||||||
*time,
|
*time,
|
||||||
last_pt.cpu_data[itx].0,
|
last_pt.cpu_data[itx].0,
|
||||||
harvested_time,
|
harvested_time,
|
||||||
cpu.cpu_usage,
|
cpu.cpu_usage,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let cpu_pt = (cpu.cpu_usage, cpu_joining_pts);
|
let cpu_pt = (cpu.cpu_usage, cpu_joining_pts);
|
||||||
new_entry.cpu_data.push(cpu_pt);
|
new_entry.cpu_data.push(cpu_pt);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cpu_harvest = harvested_data.cpu.clone();
|
self.cpu_harvest = harvested_data.cpu.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eat_temp(&mut self, harvested_data: &Data) {
|
fn eat_temp(&mut self, harvested_data: &Data) {
|
||||||
// TODO: [PO] To implement
|
// TODO: [PO] To implement
|
||||||
self.temp_harvest = harvested_data.temperature_sensors.clone();
|
self.temp_harvest = harvested_data.temperature_sensors.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eat_disks(&mut self, harvested_data: &Data, harvested_time: Instant) {
|
fn eat_disks(&mut self, harvested_data: &Data, harvested_time: Instant) {
|
||||||
// TODO: [PO] To implement
|
// TODO: [PO] To implement
|
||||||
|
|
||||||
let time_since_last_harvest = harvested_time
|
let time_since_last_harvest = harvested_time
|
||||||
.duration_since(self.current_instant)
|
.duration_since(self.current_instant)
|
||||||
.as_secs_f64();
|
.as_secs_f64();
|
||||||
|
|
||||||
for (itx, device) in harvested_data.disks.iter().enumerate() {
|
for (itx, device) in harvested_data.disks.iter().enumerate() {
|
||||||
if let Some(trim) = device.name.split('/').last() {
|
if let Some(trim) = device.name.split('/').last() {
|
||||||
let io_device = harvested_data.io.get(trim);
|
let io_device = harvested_data.io.get(trim);
|
||||||
if let Some(io) = io_device {
|
if let Some(io) = io_device {
|
||||||
let io_r_pt = io.read_bytes;
|
let io_r_pt = io.read_bytes;
|
||||||
let io_w_pt = io.write_bytes;
|
let io_w_pt = io.write_bytes;
|
||||||
|
|
||||||
if self.io_labels.len() <= itx {
|
if self.io_labels.len() <= itx {
|
||||||
self.io_prev.push((io_r_pt, io_w_pt));
|
self.io_prev.push((io_r_pt, io_w_pt));
|
||||||
self.io_labels.push((0, 0));
|
self.io_labels.push((0, 0));
|
||||||
} else {
|
} else {
|
||||||
let r_rate = ((io_r_pt - self.io_prev[itx].0) as f64
|
let r_rate = ((io_r_pt - self.io_prev[itx].0) as f64
|
||||||
/ time_since_last_harvest)
|
/ time_since_last_harvest)
|
||||||
.round() as u64;
|
.round() as u64;
|
||||||
let w_rate = ((io_w_pt - self.io_prev[itx].1) as f64
|
let w_rate = ((io_w_pt - self.io_prev[itx].1) as f64
|
||||||
/ time_since_last_harvest)
|
/ time_since_last_harvest)
|
||||||
.round() as u64;
|
.round() as u64;
|
||||||
|
|
||||||
self.io_labels[itx] = (r_rate, w_rate);
|
self.io_labels[itx] = (r_rate, w_rate);
|
||||||
self.io_prev[itx] = (io_r_pt, io_w_pt);
|
self.io_prev[itx] = (io_r_pt, io_w_pt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.disk_harvest = harvested_data.disks.clone();
|
self.disk_harvest = harvested_data.disks.clone();
|
||||||
self.io_harvest = harvested_data.io.clone();
|
self.io_harvest = harvested_data.io.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eat_proc(&mut self, harvested_data: &Data) {
|
fn eat_proc(&mut self, harvested_data: &Data) {
|
||||||
self.process_harvest = harvested_data.list_of_processes.clone();
|
self.process_harvest = harvested_data.list_of_processes.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_joining_points(
|
pub fn generate_joining_points(
|
||||||
start_x: Instant, start_y: f64, end_x: Instant, end_y: f64,
|
start_x: Instant, start_y: f64, end_x: Instant, end_y: f64,
|
||||||
) -> Vec<(TimeOffset, Value)> {
|
) -> Vec<(TimeOffset, Value)> {
|
||||||
let mut points: Vec<(TimeOffset, Value)> = Vec::new();
|
let mut points: Vec<(TimeOffset, Value)> = Vec::new();
|
||||||
|
|
||||||
// Convert time floats first:
|
// Convert time floats first:
|
||||||
let tmp_time_diff = (end_x).duration_since(start_x).as_millis() as f64;
|
let tmp_time_diff = (end_x).duration_since(start_x).as_millis() as f64;
|
||||||
let time_difference = if tmp_time_diff == 0.0 {
|
let time_difference = if tmp_time_diff == 0.0 {
|
||||||
0.001
|
0.001
|
||||||
} else {
|
} else {
|
||||||
tmp_time_diff
|
tmp_time_diff
|
||||||
};
|
};
|
||||||
let value_difference = end_y - start_y;
|
let value_difference = end_y - start_y;
|
||||||
|
|
||||||
// Let's generate... about this many points!
|
// Let's generate... about this many points!
|
||||||
let num_points = std::cmp::min(
|
let num_points = std::cmp::min(
|
||||||
std::cmp::max(
|
std::cmp::max(
|
||||||
(value_difference.abs() / time_difference * 2000.0) as u64,
|
(value_difference.abs() / time_difference * 2000.0) as u64,
|
||||||
50,
|
50,
|
||||||
),
|
),
|
||||||
2000,
|
2000,
|
||||||
);
|
);
|
||||||
|
|
||||||
for itx in (0..num_points).step_by(2) {
|
for itx in (0..num_points).step_by(2) {
|
||||||
points.push((
|
points.push((
|
||||||
time_difference - (itx as f64 / num_points as f64 * time_difference),
|
time_difference - (itx as f64 / num_points as f64 * time_difference),
|
||||||
start_y + (itx as f64 / num_points as f64 * value_difference),
|
start_y + (itx as f64 / num_points as f64 * value_difference),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
points
|
points
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,176 +13,176 @@ pub mod temperature;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
pub cpu: cpu::CPUHarvest,
|
pub cpu: cpu::CPUHarvest,
|
||||||
pub memory: mem::MemHarvest,
|
pub memory: mem::MemHarvest,
|
||||||
pub swap: mem::MemHarvest,
|
pub swap: mem::MemHarvest,
|
||||||
pub temperature_sensors: Vec<temperature::TempHarvest>,
|
pub temperature_sensors: Vec<temperature::TempHarvest>,
|
||||||
pub network: network::NetworkHarvest,
|
pub network: network::NetworkHarvest,
|
||||||
pub list_of_processes: Vec<processes::ProcessHarvest>,
|
pub list_of_processes: Vec<processes::ProcessHarvest>,
|
||||||
pub disks: Vec<disks::DiskHarvest>,
|
pub disks: Vec<disks::DiskHarvest>,
|
||||||
pub io: disks::IOHarvest,
|
pub io: disks::IOHarvest,
|
||||||
pub last_collection_time: Instant,
|
pub last_collection_time: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Data {
|
impl Default for Data {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Data {
|
Data {
|
||||||
cpu: cpu::CPUHarvest::default(),
|
cpu: cpu::CPUHarvest::default(),
|
||||||
memory: mem::MemHarvest::default(),
|
memory: mem::MemHarvest::default(),
|
||||||
swap: mem::MemHarvest::default(),
|
swap: mem::MemHarvest::default(),
|
||||||
temperature_sensors: Vec::default(),
|
temperature_sensors: Vec::default(),
|
||||||
list_of_processes: Vec::default(),
|
list_of_processes: Vec::default(),
|
||||||
disks: Vec::default(),
|
disks: Vec::default(),
|
||||||
io: disks::IOHarvest::default(),
|
io: disks::IOHarvest::default(),
|
||||||
network: network::NetworkHarvest::default(),
|
network: network::NetworkHarvest::default(),
|
||||||
last_collection_time: Instant::now(),
|
last_collection_time: Instant::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Data {
|
impl Data {
|
||||||
pub fn first_run_cleanup(&mut self) {
|
pub fn first_run_cleanup(&mut self) {
|
||||||
self.io = disks::IOHarvest::default();
|
self.io = disks::IOHarvest::default();
|
||||||
self.temperature_sensors = Vec::new();
|
self.temperature_sensors = Vec::new();
|
||||||
self.list_of_processes = Vec::new();
|
self.list_of_processes = Vec::new();
|
||||||
self.disks = Vec::new();
|
self.disks = Vec::new();
|
||||||
|
|
||||||
self.network.first_run_cleanup();
|
self.network.first_run_cleanup();
|
||||||
self.memory = mem::MemHarvest::default();
|
self.memory = mem::MemHarvest::default();
|
||||||
self.swap = mem::MemHarvest::default();
|
self.swap = mem::MemHarvest::default();
|
||||||
self.cpu = cpu::CPUHarvest::default();
|
self.cpu = cpu::CPUHarvest::default();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DataState {
|
pub struct DataState {
|
||||||
pub data: Data,
|
pub data: Data,
|
||||||
sys: System,
|
sys: System,
|
||||||
prev_pid_stats: HashMap<String, (f64, Instant)>,
|
prev_pid_stats: HashMap<String, (f64, Instant)>,
|
||||||
prev_idle: f64,
|
prev_idle: f64,
|
||||||
prev_non_idle: f64,
|
prev_non_idle: f64,
|
||||||
mem_total_kb: u64,
|
mem_total_kb: u64,
|
||||||
temperature_type: temperature::TemperatureType,
|
temperature_type: temperature::TemperatureType,
|
||||||
use_current_cpu_total: bool,
|
use_current_cpu_total: bool,
|
||||||
last_collection_time: Instant,
|
last_collection_time: Instant,
|
||||||
total_rx: u64,
|
total_rx: u64,
|
||||||
total_tx: u64,
|
total_tx: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DataState {
|
impl Default for DataState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
DataState {
|
DataState {
|
||||||
data: Data::default(),
|
data: Data::default(),
|
||||||
sys: System::new_all(),
|
sys: System::new_all(),
|
||||||
prev_pid_stats: HashMap::new(),
|
prev_pid_stats: HashMap::new(),
|
||||||
prev_idle: 0_f64,
|
prev_idle: 0_f64,
|
||||||
prev_non_idle: 0_f64,
|
prev_non_idle: 0_f64,
|
||||||
mem_total_kb: 0,
|
mem_total_kb: 0,
|
||||||
temperature_type: temperature::TemperatureType::Celsius,
|
temperature_type: temperature::TemperatureType::Celsius,
|
||||||
use_current_cpu_total: false,
|
use_current_cpu_total: false,
|
||||||
last_collection_time: Instant::now(),
|
last_collection_time: Instant::now(),
|
||||||
total_rx: 0,
|
total_rx: 0,
|
||||||
total_tx: 0,
|
total_tx: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DataState {
|
impl DataState {
|
||||||
pub fn set_temperature_type(&mut self, temperature_type: temperature::TemperatureType) {
|
pub fn set_temperature_type(&mut self, temperature_type: temperature::TemperatureType) {
|
||||||
self.temperature_type = temperature_type;
|
self.temperature_type = temperature_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_use_current_cpu_total(&mut self, use_current_cpu_total: bool) {
|
pub fn set_use_current_cpu_total(&mut self, use_current_cpu_total: bool) {
|
||||||
self.use_current_cpu_total = use_current_cpu_total;
|
self.use_current_cpu_total = use_current_cpu_total;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&mut self) {
|
pub fn init(&mut self) {
|
||||||
self.mem_total_kb = self.sys.get_total_memory();
|
self.mem_total_kb = self.sys.get_total_memory();
|
||||||
futures::executor::block_on(self.update_data());
|
futures::executor::block_on(self.update_data());
|
||||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||||
self.data.first_run_cleanup();
|
self.data.first_run_cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_data(&mut self) {
|
pub async fn update_data(&mut self) {
|
||||||
self.sys.refresh_system();
|
self.sys.refresh_system();
|
||||||
|
|
||||||
if cfg!(not(target_os = "linux")) {
|
if cfg!(not(target_os = "linux")) {
|
||||||
self.sys.refresh_processes();
|
self.sys.refresh_processes();
|
||||||
self.sys.refresh_components();
|
self.sys.refresh_components();
|
||||||
}
|
}
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
self.sys.refresh_networks();
|
self.sys.refresh_networks();
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_instant = std::time::Instant::now();
|
let current_instant = std::time::Instant::now();
|
||||||
|
|
||||||
// CPU
|
// CPU
|
||||||
self.data.cpu = cpu::get_cpu_data_list(&self.sys);
|
self.data.cpu = cpu::get_cpu_data_list(&self.sys);
|
||||||
|
|
||||||
// Processes. This is the longest part of the harvesting process... changing this might be
|
// Processes. This is the longest part of the harvesting process... changing this might be
|
||||||
// good in the future. What was tried already:
|
// good in the future. What was tried already:
|
||||||
// * Splitting the internal part into multiple scoped threads (dropped by ~.01 seconds, but upped usage)
|
// * Splitting the internal part into multiple scoped threads (dropped by ~.01 seconds, but upped usage)
|
||||||
if let Ok(process_list) = processes::get_sorted_processes_list(
|
if let Ok(process_list) = processes::get_sorted_processes_list(
|
||||||
&self.sys,
|
&self.sys,
|
||||||
&mut self.prev_idle,
|
&mut self.prev_idle,
|
||||||
&mut self.prev_non_idle,
|
&mut self.prev_non_idle,
|
||||||
&mut self.prev_pid_stats,
|
&mut self.prev_pid_stats,
|
||||||
self.use_current_cpu_total,
|
self.use_current_cpu_total,
|
||||||
self.mem_total_kb,
|
self.mem_total_kb,
|
||||||
current_instant,
|
current_instant,
|
||||||
) {
|
) {
|
||||||
self.data.list_of_processes = process_list;
|
self.data.list_of_processes = process_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ASYNC
|
// ASYNC
|
||||||
let network_data_fut = network::get_network_data(
|
let network_data_fut = network::get_network_data(
|
||||||
&self.sys,
|
&self.sys,
|
||||||
self.last_collection_time,
|
self.last_collection_time,
|
||||||
&mut self.total_rx,
|
&mut self.total_rx,
|
||||||
&mut self.total_tx,
|
&mut self.total_tx,
|
||||||
current_instant,
|
current_instant,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mem_data_fut = mem::get_mem_data_list();
|
let mem_data_fut = mem::get_mem_data_list();
|
||||||
let swap_data_fut = mem::get_swap_data_list();
|
let swap_data_fut = mem::get_swap_data_list();
|
||||||
let disk_data_fut = disks::get_disk_usage_list();
|
let disk_data_fut = disks::get_disk_usage_list();
|
||||||
let disk_io_usage_fut = disks::get_io_usage_list(false);
|
let disk_io_usage_fut = disks::get_io_usage_list(false);
|
||||||
let temp_data_fut = temperature::get_temperature_data(&self.sys, &self.temperature_type);
|
let temp_data_fut = temperature::get_temperature_data(&self.sys, &self.temperature_type);
|
||||||
|
|
||||||
let (net_data, mem_res, swap_res, disk_res, io_res, temp_res) = join!(
|
let (net_data, mem_res, swap_res, disk_res, io_res, temp_res) = join!(
|
||||||
network_data_fut,
|
network_data_fut,
|
||||||
mem_data_fut,
|
mem_data_fut,
|
||||||
swap_data_fut,
|
swap_data_fut,
|
||||||
disk_data_fut,
|
disk_data_fut,
|
||||||
disk_io_usage_fut,
|
disk_io_usage_fut,
|
||||||
temp_data_fut
|
temp_data_fut
|
||||||
);
|
);
|
||||||
|
|
||||||
// After async
|
// After async
|
||||||
self.data.network = net_data;
|
self.data.network = net_data;
|
||||||
self.total_rx = self.data.network.total_rx;
|
self.total_rx = self.data.network.total_rx;
|
||||||
self.total_tx = self.data.network.total_tx;
|
self.total_tx = self.data.network.total_tx;
|
||||||
|
|
||||||
if let Ok(memory) = mem_res {
|
if let Ok(memory) = mem_res {
|
||||||
self.data.memory = memory;
|
self.data.memory = memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(swap) = swap_res {
|
if let Ok(swap) = swap_res {
|
||||||
self.data.swap = swap;
|
self.data.swap = swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(disks) = disk_res {
|
if let Ok(disks) = disk_res {
|
||||||
self.data.disks = disks;
|
self.data.disks = disks;
|
||||||
}
|
}
|
||||||
if let Ok(io) = io_res {
|
if let Ok(io) = io_res {
|
||||||
self.data.io = io;
|
self.data.io = io;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(temp) = temp_res {
|
if let Ok(temp) = temp_res {
|
||||||
self.data.temperature_sensors = temp;
|
self.data.temperature_sensors = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update time
|
// Update time
|
||||||
self.data.last_collection_time = current_instant;
|
self.data.last_collection_time = current_instant;
|
||||||
self.last_collection_time = current_instant;
|
self.last_collection_time = current_instant;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,26 +2,26 @@ use sysinfo::{ProcessorExt, System, SystemExt};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct CPUData {
|
pub struct CPUData {
|
||||||
pub cpu_name: String,
|
pub cpu_name: String,
|
||||||
pub cpu_usage: f64,
|
pub cpu_usage: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type CPUHarvest = Vec<CPUData>;
|
pub type CPUHarvest = Vec<CPUData>;
|
||||||
|
|
||||||
pub fn get_cpu_data_list(sys: &System) -> CPUHarvest {
|
pub fn get_cpu_data_list(sys: &System) -> CPUHarvest {
|
||||||
let cpu_data = sys.get_processors();
|
let cpu_data = sys.get_processors();
|
||||||
let avg_cpu_usage = sys.get_global_processor_info().get_cpu_usage();
|
let avg_cpu_usage = sys.get_global_processor_info().get_cpu_usage();
|
||||||
let mut cpu_vec = vec![CPUData {
|
let mut cpu_vec = vec![CPUData {
|
||||||
cpu_name: "AVG".to_string(),
|
cpu_name: "AVG".to_string(),
|
||||||
cpu_usage: avg_cpu_usage as f64,
|
cpu_usage: avg_cpu_usage as f64,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
for cpu in cpu_data {
|
for cpu in cpu_data {
|
||||||
cpu_vec.push(CPUData {
|
cpu_vec.push(CPUData {
|
||||||
cpu_name: cpu.get_name().to_uppercase(),
|
cpu_name: cpu.get_name().to_uppercase(),
|
||||||
cpu_usage: f64::from(cpu.get_cpu_usage()),
|
cpu_usage: f64::from(cpu.get_cpu_usage()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cpu_vec
|
cpu_vec
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,83 +3,83 @@ use heim::units::information;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct DiskHarvest {
|
pub struct DiskHarvest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub mount_point: String,
|
pub mount_point: String,
|
||||||
pub free_space: u64,
|
pub free_space: u64,
|
||||||
pub used_space: u64,
|
pub used_space: u64,
|
||||||
pub total_space: u64,
|
pub total_space: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct IOData {
|
pub struct IOData {
|
||||||
pub read_bytes: u64,
|
pub read_bytes: u64,
|
||||||
pub write_bytes: u64,
|
pub write_bytes: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type IOHarvest = std::collections::HashMap<String, IOData>;
|
pub type IOHarvest = std::collections::HashMap<String, IOData>;
|
||||||
|
|
||||||
pub async fn get_io_usage_list(get_physical: bool) -> crate::utils::error::Result<IOHarvest> {
|
pub async fn get_io_usage_list(get_physical: bool) -> crate::utils::error::Result<IOHarvest> {
|
||||||
let mut io_hash: std::collections::HashMap<String, IOData> = std::collections::HashMap::new();
|
let mut io_hash: std::collections::HashMap<String, IOData> = std::collections::HashMap::new();
|
||||||
if get_physical {
|
if get_physical {
|
||||||
let mut physical_counter_stream = heim::disk::io_counters_physical();
|
let mut physical_counter_stream = heim::disk::io_counters_physical();
|
||||||
while let Some(io) = physical_counter_stream.next().await {
|
while let Some(io) = physical_counter_stream.next().await {
|
||||||
let io = io?;
|
let io = io?;
|
||||||
let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable");
|
let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable");
|
||||||
io_hash.insert(
|
io_hash.insert(
|
||||||
mount_point.to_string(),
|
mount_point.to_string(),
|
||||||
IOData {
|
IOData {
|
||||||
read_bytes: io.read_bytes().get::<information::megabyte>(),
|
read_bytes: io.read_bytes().get::<information::megabyte>(),
|
||||||
write_bytes: io.write_bytes().get::<information::megabyte>(),
|
write_bytes: io.write_bytes().get::<information::megabyte>(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut counter_stream = heim::disk::io_counters();
|
let mut counter_stream = heim::disk::io_counters();
|
||||||
while let Some(io) = counter_stream.next().await {
|
while let Some(io) = counter_stream.next().await {
|
||||||
let io = io?;
|
let io = io?;
|
||||||
let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable");
|
let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable");
|
||||||
io_hash.insert(
|
io_hash.insert(
|
||||||
mount_point.to_string(),
|
mount_point.to_string(),
|
||||||
IOData {
|
IOData {
|
||||||
read_bytes: io.read_bytes().get::<information::byte>(),
|
read_bytes: io.read_bytes().get::<information::byte>(),
|
||||||
write_bytes: io.write_bytes().get::<information::byte>(),
|
write_bytes: io.write_bytes().get::<information::byte>(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(io_hash)
|
Ok(io_hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_disk_usage_list() -> crate::utils::error::Result<Vec<DiskHarvest>> {
|
pub async fn get_disk_usage_list() -> crate::utils::error::Result<Vec<DiskHarvest>> {
|
||||||
let mut vec_disks: Vec<DiskHarvest> = Vec::new();
|
let mut vec_disks: Vec<DiskHarvest> = Vec::new();
|
||||||
let mut partitions_stream = heim::disk::partitions_physical();
|
let mut partitions_stream = heim::disk::partitions_physical();
|
||||||
|
|
||||||
while let Some(part) = partitions_stream.next().await {
|
while let Some(part) = partitions_stream.next().await {
|
||||||
if let Ok(part) = part {
|
if let Ok(part) = part {
|
||||||
let partition = part;
|
let partition = part;
|
||||||
let usage = heim::disk::usage(partition.mount_point().to_path_buf()).await?;
|
let usage = heim::disk::usage(partition.mount_point().to_path_buf()).await?;
|
||||||
|
|
||||||
vec_disks.push(DiskHarvest {
|
vec_disks.push(DiskHarvest {
|
||||||
free_space: usage.free().get::<information::byte>(),
|
free_space: usage.free().get::<information::byte>(),
|
||||||
used_space: usage.used().get::<information::byte>(),
|
used_space: usage.used().get::<information::byte>(),
|
||||||
total_space: usage.total().get::<information::byte>(),
|
total_space: usage.total().get::<information::byte>(),
|
||||||
mount_point: (partition
|
mount_point: (partition
|
||||||
.mount_point()
|
.mount_point()
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap_or("Name Unavailable"))
|
.unwrap_or("Name Unavailable"))
|
||||||
.to_string(),
|
.to_string(),
|
||||||
name: (partition
|
name: (partition
|
||||||
.device()
|
.device()
|
||||||
.unwrap_or_else(|| std::ffi::OsStr::new("Name Unavailable"))
|
.unwrap_or_else(|| std::ffi::OsStr::new("Name Unavailable"))
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap_or("Name Unavailable"))
|
.unwrap_or("Name Unavailable"))
|
||||||
.to_string(),
|
.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vec_disks.sort_by(|a, b| a.name.cmp(&b.name));
|
vec_disks.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
||||||
Ok(vec_disks)
|
Ok(vec_disks)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,34 +2,34 @@ use heim::units::information;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MemHarvest {
|
pub struct MemHarvest {
|
||||||
pub mem_total_in_mb: u64,
|
pub mem_total_in_mb: u64,
|
||||||
pub mem_used_in_mb: u64,
|
pub mem_used_in_mb: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MemHarvest {
|
impl Default for MemHarvest {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
MemHarvest {
|
MemHarvest {
|
||||||
mem_total_in_mb: 0,
|
mem_total_in_mb: 0,
|
||||||
mem_used_in_mb: 0,
|
mem_used_in_mb: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_mem_data_list() -> crate::utils::error::Result<MemHarvest> {
|
pub async fn get_mem_data_list() -> crate::utils::error::Result<MemHarvest> {
|
||||||
let memory = heim::memory::memory().await?;
|
let memory = heim::memory::memory().await?;
|
||||||
|
|
||||||
Ok(MemHarvest {
|
Ok(MemHarvest {
|
||||||
mem_total_in_mb: memory.total().get::<information::megabyte>(),
|
mem_total_in_mb: memory.total().get::<information::megabyte>(),
|
||||||
mem_used_in_mb: memory.total().get::<information::megabyte>()
|
mem_used_in_mb: memory.total().get::<information::megabyte>()
|
||||||
- memory.available().get::<information::megabyte>(),
|
- memory.available().get::<information::megabyte>(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_swap_data_list() -> crate::utils::error::Result<MemHarvest> {
|
pub async fn get_swap_data_list() -> crate::utils::error::Result<MemHarvest> {
|
||||||
let memory = heim::memory::swap().await?;
|
let memory = heim::memory::swap().await?;
|
||||||
|
|
||||||
Ok(MemHarvest {
|
Ok(MemHarvest {
|
||||||
mem_total_in_mb: memory.total().get::<information::megabyte>(),
|
mem_total_in_mb: memory.total().get::<information::megabyte>(),
|
||||||
mem_used_in_mb: memory.used().get::<information::megabyte>(),
|
mem_used_in_mb: memory.used().get::<information::megabyte>(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,53 +7,53 @@ use sysinfo::{NetworkExt, System, SystemExt};
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct NetworkHarvest {
|
pub struct NetworkHarvest {
|
||||||
pub rx: u64,
|
pub rx: u64,
|
||||||
pub tx: u64,
|
pub tx: u64,
|
||||||
pub total_rx: u64,
|
pub total_rx: u64,
|
||||||
pub total_tx: u64,
|
pub total_tx: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetworkHarvest {
|
impl NetworkHarvest {
|
||||||
pub fn first_run_cleanup(&mut self) {
|
pub fn first_run_cleanup(&mut self) {
|
||||||
self.rx = 0;
|
self.rx = 0;
|
||||||
self.tx = 0;
|
self.tx = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_network_data(
|
pub async fn get_network_data(
|
||||||
sys: &System, prev_net_access_time: Instant, prev_net_rx: &mut u64, prev_net_tx: &mut u64,
|
sys: &System, prev_net_access_time: Instant, prev_net_rx: &mut u64, prev_net_tx: &mut u64,
|
||||||
curr_time: Instant,
|
curr_time: Instant,
|
||||||
) -> NetworkHarvest {
|
) -> NetworkHarvest {
|
||||||
let mut io_data = net::io_counters();
|
let mut io_data = net::io_counters();
|
||||||
let mut total_rx: u64 = 0;
|
let mut total_rx: u64 = 0;
|
||||||
let mut total_tx: u64 = 0;
|
let mut total_tx: u64 = 0;
|
||||||
|
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
let networks = sys.get_networks();
|
let networks = sys.get_networks();
|
||||||
for (_, network) in networks {
|
for (_, network) in networks {
|
||||||
total_rx += network.get_total_income();
|
total_rx += network.get_total_income();
|
||||||
total_tx += network.get_total_outcome();
|
total_tx += network.get_total_outcome();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
while let Some(io) = io_data.next().await {
|
while let Some(io) = io_data.next().await {
|
||||||
if let Ok(io) = io {
|
if let Ok(io) = io {
|
||||||
total_rx += io.bytes_recv().get::<byte>();
|
total_rx += io.bytes_recv().get::<byte>();
|
||||||
total_tx += io.bytes_sent().get::<byte>();
|
total_tx += io.bytes_sent().get::<byte>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let elapsed_time = curr_time.duration_since(prev_net_access_time).as_secs_f64();
|
let elapsed_time = curr_time.duration_since(prev_net_access_time).as_secs_f64();
|
||||||
|
|
||||||
let rx = ((total_rx - *prev_net_rx) as f64 / elapsed_time) as u64;
|
let rx = ((total_rx - *prev_net_rx) as f64 / elapsed_time) as u64;
|
||||||
let tx = ((total_tx - *prev_net_tx) as f64 / elapsed_time) as u64;
|
let tx = ((total_tx - *prev_net_tx) as f64 / elapsed_time) as u64;
|
||||||
|
|
||||||
*prev_net_rx = total_rx;
|
*prev_net_rx = total_rx;
|
||||||
*prev_net_tx = total_tx;
|
*prev_net_tx = total_tx;
|
||||||
NetworkHarvest {
|
NetworkHarvest {
|
||||||
rx,
|
rx,
|
||||||
tx,
|
tx,
|
||||||
total_rx,
|
total_rx,
|
||||||
total_tx,
|
total_tx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::{hash_map::RandomState, HashMap},
|
collections::{hash_map::RandomState, HashMap},
|
||||||
process::Command,
|
process::Command,
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
use sysinfo::{ProcessExt, ProcessorExt, System, SystemExt};
|
use sysinfo::{ProcessExt, ProcessorExt, System, SystemExt};
|
||||||
|
@ -10,277 +10,277 @@ use crate::utils::error;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ProcessSorting {
|
pub enum ProcessSorting {
|
||||||
CPU,
|
CPU,
|
||||||
MEM,
|
MEM,
|
||||||
PID,
|
PID,
|
||||||
NAME,
|
NAME,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProcessSorting {
|
impl Default for ProcessSorting {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ProcessSorting::CPU
|
ProcessSorting::CPU
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ProcessHarvest {
|
pub struct ProcessHarvest {
|
||||||
pub pid: u32,
|
pub pid: u32,
|
||||||
pub cpu_usage_percent: f64,
|
pub cpu_usage_percent: f64,
|
||||||
pub mem_usage_percent: f64,
|
pub mem_usage_percent: f64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cpu_usage_calculation(
|
fn cpu_usage_calculation(
|
||||||
prev_idle: &mut f64, prev_non_idle: &mut f64,
|
prev_idle: &mut f64, prev_non_idle: &mut f64,
|
||||||
) -> error::Result<(f64, f64)> {
|
) -> error::Result<(f64, f64)> {
|
||||||
// From SO answer: https://stackoverflow.com/a/23376195
|
// From SO answer: https://stackoverflow.com/a/23376195
|
||||||
let mut path = std::path::PathBuf::new();
|
let mut path = std::path::PathBuf::new();
|
||||||
path.push("/proc");
|
path.push("/proc");
|
||||||
path.push("stat");
|
path.push("stat");
|
||||||
|
|
||||||
let stat_results = std::fs::read_to_string(path)?;
|
let stat_results = std::fs::read_to_string(path)?;
|
||||||
let first_line: &str;
|
let first_line: &str;
|
||||||
|
|
||||||
let split_results = stat_results.split('\n').collect::<Vec<&str>>();
|
let split_results = stat_results.split('\n').collect::<Vec<&str>>();
|
||||||
if split_results.is_empty() {
|
if split_results.is_empty() {
|
||||||
return Err(error::BottomError::InvalidIO(format!(
|
return Err(error::BottomError::InvalidIO(format!(
|
||||||
"Unable to properly split the stat results; saw {} values, expected at least 1 value.",
|
"Unable to properly split the stat results; saw {} values, expected at least 1 value.",
|
||||||
split_results.len()
|
split_results.len()
|
||||||
)));
|
)));
|
||||||
} else {
|
} else {
|
||||||
first_line = split_results[0];
|
first_line = split_results[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
let val = first_line.split_whitespace().collect::<Vec<&str>>();
|
let val = first_line.split_whitespace().collect::<Vec<&str>>();
|
||||||
|
|
||||||
// SC in case that the parsing will fail due to length:
|
// SC in case that the parsing will fail due to length:
|
||||||
if val.len() <= 10 {
|
if val.len() <= 10 {
|
||||||
return Err(error::BottomError::InvalidIO(format!(
|
return Err(error::BottomError::InvalidIO(format!(
|
||||||
"CPU parsing will fail due to too short of a return value; saw {} values, expected 10 values.",
|
"CPU parsing will fail due to too short of a return value; saw {} values, expected 10 values.",
|
||||||
val.len()
|
val.len()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let user: f64 = val[1].parse::<_>().unwrap_or(0_f64);
|
let user: f64 = val[1].parse::<_>().unwrap_or(0_f64);
|
||||||
let nice: f64 = val[2].parse::<_>().unwrap_or(0_f64);
|
let nice: f64 = val[2].parse::<_>().unwrap_or(0_f64);
|
||||||
let system: f64 = val[3].parse::<_>().unwrap_or(0_f64);
|
let system: f64 = val[3].parse::<_>().unwrap_or(0_f64);
|
||||||
let idle: f64 = val[4].parse::<_>().unwrap_or(0_f64);
|
let idle: f64 = val[4].parse::<_>().unwrap_or(0_f64);
|
||||||
let iowait: f64 = val[5].parse::<_>().unwrap_or(0_f64);
|
let iowait: f64 = val[5].parse::<_>().unwrap_or(0_f64);
|
||||||
let irq: f64 = val[6].parse::<_>().unwrap_or(0_f64);
|
let irq: f64 = val[6].parse::<_>().unwrap_or(0_f64);
|
||||||
let softirq: f64 = val[7].parse::<_>().unwrap_or(0_f64);
|
let softirq: f64 = val[7].parse::<_>().unwrap_or(0_f64);
|
||||||
let steal: f64 = val[8].parse::<_>().unwrap_or(0_f64);
|
let steal: f64 = val[8].parse::<_>().unwrap_or(0_f64);
|
||||||
let guest: f64 = val[9].parse::<_>().unwrap_or(0_f64);
|
let guest: f64 = val[9].parse::<_>().unwrap_or(0_f64);
|
||||||
|
|
||||||
let idle = idle + iowait;
|
let idle = idle + iowait;
|
||||||
let non_idle = user + nice + system + irq + softirq + steal + guest;
|
let non_idle = user + nice + system + irq + softirq + steal + guest;
|
||||||
|
|
||||||
let total = idle + non_idle;
|
let total = idle + non_idle;
|
||||||
let prev_total = *prev_idle + *prev_non_idle;
|
let prev_total = *prev_idle + *prev_non_idle;
|
||||||
|
|
||||||
let total_delta: f64 = total - prev_total;
|
let total_delta: f64 = total - prev_total;
|
||||||
let idle_delta: f64 = idle - *prev_idle;
|
let idle_delta: f64 = idle - *prev_idle;
|
||||||
|
|
||||||
//debug!("Vangelis function: CPU PERCENT: {}", (total_delta - idle_delta) / total_delta * 100_f64);
|
//debug!("Vangelis function: CPU PERCENT: {}", (total_delta - idle_delta) / total_delta * 100_f64);
|
||||||
|
|
||||||
*prev_idle = idle;
|
*prev_idle = idle;
|
||||||
*prev_non_idle = non_idle;
|
*prev_non_idle = non_idle;
|
||||||
|
|
||||||
let result = if total_delta - idle_delta != 0_f64 {
|
let result = if total_delta - idle_delta != 0_f64 {
|
||||||
total_delta - idle_delta
|
total_delta - idle_delta
|
||||||
} else {
|
} else {
|
||||||
1_f64
|
1_f64
|
||||||
};
|
};
|
||||||
|
|
||||||
let cpu_percentage = if total_delta != 0_f64 {
|
let cpu_percentage = if total_delta != 0_f64 {
|
||||||
result / total_delta
|
result / total_delta
|
||||||
} else {
|
} else {
|
||||||
0_f64
|
0_f64
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((result, cpu_percentage))
|
Ok((result, cpu_percentage))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_process_cpu_stats(pid: u32) -> std::io::Result<f64> {
|
fn get_process_cpu_stats(pid: u32) -> std::io::Result<f64> {
|
||||||
let mut path = std::path::PathBuf::new();
|
let mut path = std::path::PathBuf::new();
|
||||||
path.push("/proc");
|
path.push("/proc");
|
||||||
path.push(&pid.to_string());
|
path.push(&pid.to_string());
|
||||||
path.push("stat");
|
path.push("stat");
|
||||||
|
|
||||||
let stat_results = std::fs::read_to_string(path)?;
|
let stat_results = std::fs::read_to_string(path)?;
|
||||||
let val = stat_results.split_whitespace().collect::<Vec<&str>>();
|
let val = stat_results.split_whitespace().collect::<Vec<&str>>();
|
||||||
let utime = val[13].parse::<f64>().unwrap_or(0_f64);
|
let utime = val[13].parse::<f64>().unwrap_or(0_f64);
|
||||||
let stime = val[14].parse::<f64>().unwrap_or(0_f64);
|
let stime = val[14].parse::<f64>().unwrap_or(0_f64);
|
||||||
|
|
||||||
//debug!("PID: {}, utime: {}, stime: {}", pid, utime, stime);
|
//debug!("PID: {}, utime: {}, stime: {}", pid, utime, stime);
|
||||||
|
|
||||||
Ok(utime + stime) // This seems to match top...
|
Ok(utime + stime) // This seems to match top...
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Note that cpu_fraction should be represented WITHOUT the \times 100 factor!
|
/// Note that cpu_fraction should be represented WITHOUT the \times 100 factor!
|
||||||
fn linux_cpu_usage<S: core::hash::BuildHasher>(
|
fn linux_cpu_usage<S: core::hash::BuildHasher>(
|
||||||
pid: u32, cpu_usage: f64, cpu_fraction: f64,
|
pid: u32, cpu_usage: f64, cpu_fraction: f64,
|
||||||
prev_pid_stats: &HashMap<String, (f64, Instant), S>,
|
prev_pid_stats: &HashMap<String, (f64, Instant), S>,
|
||||||
new_pid_stats: &mut HashMap<String, (f64, Instant), S>, use_current_cpu_total: bool,
|
new_pid_stats: &mut HashMap<String, (f64, Instant), S>, use_current_cpu_total: bool,
|
||||||
curr_time: Instant,
|
curr_time: Instant,
|
||||||
) -> std::io::Result<f64> {
|
) -> std::io::Result<f64> {
|
||||||
// Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556
|
// Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556
|
||||||
let before_proc_val: f64 = if prev_pid_stats.contains_key(&pid.to_string()) {
|
let before_proc_val: f64 = if prev_pid_stats.contains_key(&pid.to_string()) {
|
||||||
prev_pid_stats
|
prev_pid_stats
|
||||||
.get(&pid.to_string())
|
.get(&pid.to_string())
|
||||||
.unwrap_or(&(0_f64, curr_time))
|
.unwrap_or(&(0_f64, curr_time))
|
||||||
.0
|
.0
|
||||||
} else {
|
} else {
|
||||||
0_f64
|
0_f64
|
||||||
};
|
};
|
||||||
let after_proc_val = get_process_cpu_stats(pid)?;
|
let after_proc_val = get_process_cpu_stats(pid)?;
|
||||||
|
|
||||||
/*debug!(
|
/*debug!(
|
||||||
"PID - {} - Before: {}, After: {}, CPU: {}, Percentage: {}",
|
"PID - {} - Before: {}, After: {}, CPU: {}, Percentage: {}",
|
||||||
pid,
|
pid,
|
||||||
before_proc_val,
|
before_proc_val,
|
||||||
after_proc_val,
|
after_proc_val,
|
||||||
cpu_usage,
|
cpu_usage,
|
||||||
(after_proc_val - before_proc_val) / cpu_usage * 100_f64
|
(after_proc_val - before_proc_val) / cpu_usage * 100_f64
|
||||||
);*/
|
);*/
|
||||||
|
|
||||||
new_pid_stats.insert(pid.to_string(), (after_proc_val, curr_time));
|
new_pid_stats.insert(pid.to_string(), (after_proc_val, curr_time));
|
||||||
|
|
||||||
if use_current_cpu_total {
|
if use_current_cpu_total {
|
||||||
Ok((after_proc_val - before_proc_val) / cpu_usage * 100_f64)
|
Ok((after_proc_val - before_proc_val) / cpu_usage * 100_f64)
|
||||||
} else {
|
} else {
|
||||||
Ok((after_proc_val - before_proc_val) / cpu_usage * 100_f64 * cpu_fraction)
|
Ok((after_proc_val - before_proc_val) / cpu_usage * 100_f64 * cpu_fraction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_ps<S: core::hash::BuildHasher>(
|
fn convert_ps<S: core::hash::BuildHasher>(
|
||||||
process: &str, cpu_usage: f64, cpu_fraction: f64,
|
process: &str, cpu_usage: f64, cpu_fraction: f64,
|
||||||
prev_pid_stats: &HashMap<String, (f64, Instant), S>,
|
prev_pid_stats: &HashMap<String, (f64, Instant), S>,
|
||||||
new_pid_stats: &mut HashMap<String, (f64, Instant), S>, use_current_cpu_total: bool,
|
new_pid_stats: &mut HashMap<String, (f64, Instant), S>, use_current_cpu_total: bool,
|
||||||
curr_time: Instant,
|
curr_time: Instant,
|
||||||
) -> std::io::Result<ProcessHarvest> {
|
) -> std::io::Result<ProcessHarvest> {
|
||||||
if process.trim().to_string().is_empty() {
|
if process.trim().to_string().is_empty() {
|
||||||
return Ok(ProcessHarvest {
|
return Ok(ProcessHarvest {
|
||||||
pid: 0,
|
pid: 0,
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
mem_usage_percent: 0.0,
|
mem_usage_percent: 0.0,
|
||||||
cpu_usage_percent: 0.0,
|
cpu_usage_percent: 0.0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let pid = (&process[..11])
|
let pid = (&process[..11])
|
||||||
.trim()
|
.trim()
|
||||||
.to_string()
|
.to_string()
|
||||||
.parse::<u32>()
|
.parse::<u32>()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let name = (&process[11..61]).trim().to_string();
|
let name = (&process[11..61]).trim().to_string();
|
||||||
let mem_usage_percent = (&process[62..])
|
let mem_usage_percent = (&process[62..])
|
||||||
.trim()
|
.trim()
|
||||||
.to_string()
|
.to_string()
|
||||||
.parse::<f64>()
|
.parse::<f64>()
|
||||||
.unwrap_or(0_f64);
|
.unwrap_or(0_f64);
|
||||||
|
|
||||||
let cpu_usage_percent = linux_cpu_usage(
|
let cpu_usage_percent = linux_cpu_usage(
|
||||||
pid,
|
pid,
|
||||||
cpu_usage,
|
cpu_usage,
|
||||||
cpu_fraction,
|
cpu_fraction,
|
||||||
prev_pid_stats,
|
prev_pid_stats,
|
||||||
new_pid_stats,
|
new_pid_stats,
|
||||||
use_current_cpu_total,
|
use_current_cpu_total,
|
||||||
curr_time,
|
curr_time,
|
||||||
)?;
|
)?;
|
||||||
Ok(ProcessHarvest {
|
Ok(ProcessHarvest {
|
||||||
pid,
|
pid,
|
||||||
name,
|
name,
|
||||||
mem_usage_percent,
|
mem_usage_percent,
|
||||||
cpu_usage_percent,
|
cpu_usage_percent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_sorted_processes_list(
|
pub fn get_sorted_processes_list(
|
||||||
sys: &System, prev_idle: &mut f64, prev_non_idle: &mut f64,
|
sys: &System, prev_idle: &mut f64, prev_non_idle: &mut f64,
|
||||||
prev_pid_stats: &mut HashMap<String, (f64, Instant), RandomState>, use_current_cpu_total: bool,
|
prev_pid_stats: &mut HashMap<String, (f64, Instant), RandomState>, use_current_cpu_total: bool,
|
||||||
mem_total_kb: u64, curr_time: Instant,
|
mem_total_kb: u64, curr_time: Instant,
|
||||||
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
|
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
|
||||||
let mut process_vector: Vec<ProcessHarvest> = Vec::new();
|
let mut process_vector: Vec<ProcessHarvest> = Vec::new();
|
||||||
|
|
||||||
if cfg!(target_os = "linux") {
|
if cfg!(target_os = "linux") {
|
||||||
let ps_result = Command::new("ps")
|
let ps_result = Command::new("ps")
|
||||||
.args(&["-axo", "pid:10,comm:50,%mem:5", "--noheader"])
|
.args(&["-axo", "pid:10,comm:50,%mem:5", "--noheader"])
|
||||||
.output()?;
|
.output()?;
|
||||||
let ps_stdout = String::from_utf8_lossy(&ps_result.stdout);
|
let ps_stdout = String::from_utf8_lossy(&ps_result.stdout);
|
||||||
let split_string = ps_stdout.split('\n');
|
let split_string = ps_stdout.split('\n');
|
||||||
let cpu_calc = cpu_usage_calculation(prev_idle, prev_non_idle);
|
let cpu_calc = cpu_usage_calculation(prev_idle, prev_non_idle);
|
||||||
if let Ok((cpu_usage, cpu_fraction)) = cpu_calc {
|
if let Ok((cpu_usage, cpu_fraction)) = cpu_calc {
|
||||||
let process_stream = split_string.collect::<Vec<&str>>();
|
let process_stream = split_string.collect::<Vec<&str>>();
|
||||||
|
|
||||||
let mut new_pid_stats: HashMap<String, (f64, Instant), RandomState> = HashMap::new();
|
let mut new_pid_stats: HashMap<String, (f64, Instant), RandomState> = HashMap::new();
|
||||||
|
|
||||||
for process in process_stream {
|
for process in process_stream {
|
||||||
if let Ok(process_object) = convert_ps(
|
if let Ok(process_object) = convert_ps(
|
||||||
process,
|
process,
|
||||||
cpu_usage,
|
cpu_usage,
|
||||||
cpu_fraction,
|
cpu_fraction,
|
||||||
&prev_pid_stats,
|
&prev_pid_stats,
|
||||||
&mut new_pid_stats,
|
&mut new_pid_stats,
|
||||||
use_current_cpu_total,
|
use_current_cpu_total,
|
||||||
curr_time,
|
curr_time,
|
||||||
) {
|
) {
|
||||||
if !process_object.name.is_empty() {
|
if !process_object.name.is_empty() {
|
||||||
process_vector.push(process_object);
|
process_vector.push(process_object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*prev_pid_stats = new_pid_stats;
|
*prev_pid_stats = new_pid_stats;
|
||||||
} else {
|
} else {
|
||||||
error!("Unable to properly parse CPU data in Linux.");
|
error!("Unable to properly parse CPU data in Linux.");
|
||||||
error!("Result: {:?}", cpu_calc.err());
|
error!("Result: {:?}", cpu_calc.err());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let process_hashmap = sys.get_processes();
|
let process_hashmap = sys.get_processes();
|
||||||
let cpu_usage = sys.get_global_processor_info().get_cpu_usage() as f64 / 100.0;
|
let cpu_usage = sys.get_global_processor_info().get_cpu_usage() as f64 / 100.0;
|
||||||
let num_cpus = sys.get_processors().len() as f64;
|
let num_cpus = sys.get_processors().len() as f64;
|
||||||
for process_val in process_hashmap.values() {
|
for process_val in process_hashmap.values() {
|
||||||
let name = if process_val.name().is_empty() {
|
let name = if process_val.name().is_empty() {
|
||||||
let process_cmd = process_val.cmd();
|
let process_cmd = process_val.cmd();
|
||||||
if process_cmd.len() > 1 {
|
if process_cmd.len() > 1 {
|
||||||
process_cmd[0].clone()
|
process_cmd[0].clone()
|
||||||
} else {
|
} else {
|
||||||
let process_exe = process_val.exe().file_stem();
|
let process_exe = process_val.exe().file_stem();
|
||||||
if let Some(exe) = process_exe {
|
if let Some(exe) = process_exe {
|
||||||
let process_exe_opt = exe.to_str();
|
let process_exe_opt = exe.to_str();
|
||||||
if let Some(exe_name) = process_exe_opt {
|
if let Some(exe_name) = process_exe_opt {
|
||||||
exe_name.to_string()
|
exe_name.to_string()
|
||||||
} else {
|
} else {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
process_val.name().to_string()
|
process_val.name().to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let pcu = if cfg!(target_os = "windows") {
|
let pcu = if cfg!(target_os = "windows") {
|
||||||
process_val.cpu_usage() as f64
|
process_val.cpu_usage() as f64
|
||||||
} else {
|
} else {
|
||||||
process_val.cpu_usage() as f64 / num_cpus
|
process_val.cpu_usage() as f64 / num_cpus
|
||||||
};
|
};
|
||||||
let process_cpu_usage = if use_current_cpu_total {
|
let process_cpu_usage = if use_current_cpu_total {
|
||||||
pcu / cpu_usage
|
pcu / cpu_usage
|
||||||
} else {
|
} else {
|
||||||
pcu
|
pcu
|
||||||
};
|
};
|
||||||
|
|
||||||
process_vector.push(ProcessHarvest {
|
process_vector.push(ProcessHarvest {
|
||||||
pid: process_val.pid() as u32,
|
pid: process_val.pid() as u32,
|
||||||
name,
|
name,
|
||||||
mem_usage_percent: process_val.memory() as f64 * 100.0 / mem_total_kb as f64,
|
mem_usage_percent: process_val.memory() as f64 * 100.0 / mem_total_kb as f64,
|
||||||
cpu_usage_percent: process_cpu_usage,
|
cpu_usage_percent: process_cpu_usage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(process_vector)
|
Ok(process_vector)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,93 +6,93 @@ use sysinfo::{ComponentExt, System, SystemExt};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct TempHarvest {
|
pub struct TempHarvest {
|
||||||
pub component_name: String,
|
pub component_name: String,
|
||||||
pub temperature: f32,
|
pub temperature: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum TemperatureType {
|
pub enum TemperatureType {
|
||||||
Celsius,
|
Celsius,
|
||||||
Kelvin,
|
Kelvin,
|
||||||
Fahrenheit,
|
Fahrenheit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TemperatureType {
|
impl Default for TemperatureType {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
TemperatureType::Celsius
|
TemperatureType::Celsius
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_temperature_data(
|
pub async fn get_temperature_data(
|
||||||
sys: &System, temp_type: &TemperatureType,
|
sys: &System, temp_type: &TemperatureType,
|
||||||
) -> crate::utils::error::Result<Vec<TempHarvest>> {
|
) -> crate::utils::error::Result<Vec<TempHarvest>> {
|
||||||
let mut temperature_vec: Vec<TempHarvest> = Vec::new();
|
let mut temperature_vec: Vec<TempHarvest> = Vec::new();
|
||||||
|
|
||||||
if cfg!(target_os = "linux") {
|
if cfg!(target_os = "linux") {
|
||||||
let mut sensor_data = heim::sensors::temperatures();
|
let mut sensor_data = heim::sensors::temperatures();
|
||||||
while let Some(sensor) = sensor_data.next().await {
|
while let Some(sensor) = sensor_data.next().await {
|
||||||
if let Ok(sensor) = sensor {
|
if let Ok(sensor) = sensor {
|
||||||
temperature_vec.push(TempHarvest {
|
temperature_vec.push(TempHarvest {
|
||||||
component_name: sensor.unit().to_string(),
|
component_name: sensor.unit().to_string(),
|
||||||
temperature: match temp_type {
|
temperature: match temp_type {
|
||||||
TemperatureType::Celsius => sensor
|
TemperatureType::Celsius => sensor
|
||||||
.current()
|
.current()
|
||||||
.get::<thermodynamic_temperature::degree_celsius>(
|
.get::<thermodynamic_temperature::degree_celsius>(
|
||||||
),
|
),
|
||||||
TemperatureType::Kelvin => {
|
TemperatureType::Kelvin => {
|
||||||
sensor.current().get::<thermodynamic_temperature::kelvin>()
|
sensor.current().get::<thermodynamic_temperature::kelvin>()
|
||||||
}
|
}
|
||||||
TemperatureType::Fahrenheit => sensor
|
TemperatureType::Fahrenheit => sensor
|
||||||
.current()
|
.current()
|
||||||
.get::<thermodynamic_temperature::degree_fahrenheit>(
|
.get::<thermodynamic_temperature::degree_fahrenheit>(
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let sensor_data = sys.get_components();
|
let sensor_data = sys.get_components();
|
||||||
for component in sensor_data {
|
for component in sensor_data {
|
||||||
temperature_vec.push(TempHarvest {
|
temperature_vec.push(TempHarvest {
|
||||||
component_name: component.get_label().to_string(),
|
component_name: component.get_label().to_string(),
|
||||||
temperature: match temp_type {
|
temperature: match temp_type {
|
||||||
TemperatureType::Celsius => component.get_temperature(),
|
TemperatureType::Celsius => component.get_temperature(),
|
||||||
TemperatureType::Kelvin => {
|
TemperatureType::Kelvin => {
|
||||||
convert_celsius_to_kelvin(component.get_temperature())
|
convert_celsius_to_kelvin(component.get_temperature())
|
||||||
}
|
}
|
||||||
TemperatureType::Fahrenheit => {
|
TemperatureType::Fahrenheit => {
|
||||||
convert_celsius_to_fahrenheit(component.get_temperature())
|
convert_celsius_to_fahrenheit(component.get_temperature())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// By default, sort temperature, then by alphabetically!
|
// By default, sort temperature, then by alphabetically!
|
||||||
|
|
||||||
// Note we sort in reverse here; we want greater temps to be higher priority.
|
// Note we sort in reverse here; we want greater temps to be higher priority.
|
||||||
temperature_vec.sort_by(|a, b| match a.temperature.partial_cmp(&b.temperature) {
|
temperature_vec.sort_by(|a, b| match a.temperature.partial_cmp(&b.temperature) {
|
||||||
Some(x) => match x {
|
Some(x) => match x {
|
||||||
Ordering::Less => Ordering::Greater,
|
Ordering::Less => Ordering::Greater,
|
||||||
Ordering::Greater => Ordering::Less,
|
Ordering::Greater => Ordering::Less,
|
||||||
Ordering::Equal => Ordering::Equal,
|
Ordering::Equal => Ordering::Equal,
|
||||||
},
|
},
|
||||||
None => Ordering::Equal,
|
None => Ordering::Equal,
|
||||||
});
|
});
|
||||||
|
|
||||||
temperature_vec.sort_by(|a, b| {
|
temperature_vec.sort_by(|a, b| {
|
||||||
a.component_name
|
a.component_name
|
||||||
.partial_cmp(&b.component_name)
|
.partial_cmp(&b.component_name)
|
||||||
.unwrap_or(Ordering::Equal)
|
.unwrap_or(Ordering::Equal)
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(temperature_vec)
|
Ok(temperature_vec)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_celsius_to_kelvin(celsius: f32) -> f32 {
|
fn convert_celsius_to_kelvin(celsius: f32) -> f32 {
|
||||||
celsius + 273.15
|
celsius + 273.15
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_celsius_to_fahrenheit(celsius: f32) -> f32 {
|
fn convert_celsius_to_fahrenheit(celsius: f32) -> f32 {
|
||||||
(celsius * (9.0 / 5.0)) + 32.0
|
(celsius * (9.0 / 5.0)) + 32.0
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,11 @@ use std::process::Command;
|
||||||
// Copied from SO: https://stackoverflow.com/a/55231715
|
// Copied from SO: https://stackoverflow.com/a/55231715
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use winapi::{
|
use winapi::{
|
||||||
shared::{minwindef::DWORD, ntdef::HANDLE},
|
shared::{minwindef::DWORD, ntdef::HANDLE},
|
||||||
um::{
|
um::{
|
||||||
processthreadsapi::{OpenProcess, TerminateProcess},
|
processthreadsapi::{OpenProcess, TerminateProcess},
|
||||||
winnt::{PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE},
|
winnt::{PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// This file is meant to house (OS specific) implementations on how to kill processes.
|
/// This file is meant to house (OS specific) implementations on how to kill processes.
|
||||||
|
@ -18,36 +18,36 @@ struct Process(HANDLE);
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
impl Process {
|
impl Process {
|
||||||
fn open(pid: DWORD) -> Result<Process, String> {
|
fn open(pid: DWORD) -> Result<Process, String> {
|
||||||
let pc = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, 0, pid) };
|
let pc = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, 0, pid) };
|
||||||
if pc.is_null() {
|
if pc.is_null() {
|
||||||
return Err("!OpenProcess".to_string());
|
return Err("!OpenProcess".to_string());
|
||||||
}
|
}
|
||||||
Ok(Process(pc))
|
Ok(Process(pc))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kill(self) -> Result<(), String> {
|
fn kill(self) -> Result<(), String> {
|
||||||
unsafe { TerminateProcess(self.0, 1) };
|
unsafe { TerminateProcess(self.0, 1) };
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kills a process, given a PID.
|
/// Kills a process, given a PID.
|
||||||
pub fn kill_process_given_pid(pid: u32) -> crate::utils::error::Result<()> {
|
pub fn kill_process_given_pid(pid: u32) -> crate::utils::error::Result<()> {
|
||||||
if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
|
if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
|
||||||
Command::new("kill").arg(pid.to_string()).output()?;
|
Command::new("kill").arg(pid.to_string()).output()?;
|
||||||
} else if cfg!(target_os = "windows") {
|
} else if cfg!(target_os = "windows") {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
let process = Process::open(pid as DWORD)?;
|
let process = Process::open(pid as DWORD)?;
|
||||||
process.kill()?;
|
process.kill()?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(BottomError::GenericError(
|
return Err(BottomError::GenericError(
|
||||||
"Sorry, support operating systems outside the main three are not implemented yet!"
|
"Sorry, support operating systems outside the main three are not implemented yet!"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
3303
src/canvas.rs
3303
src/canvas.rs
File diff suppressed because it is too large
Load diff
|
@ -7,147 +7,147 @@ use crate::{constants::*, utils::error};
|
||||||
mod colour_utils;
|
mod colour_utils;
|
||||||
|
|
||||||
pub struct CanvasColours {
|
pub struct CanvasColours {
|
||||||
pub currently_selected_text_colour: Color,
|
pub currently_selected_text_colour: Color,
|
||||||
pub currently_selected_bg_colour: Color,
|
pub currently_selected_bg_colour: Color,
|
||||||
pub currently_selected_text_style: Style,
|
pub currently_selected_text_style: Style,
|
||||||
pub table_header_style: Style,
|
pub table_header_style: Style,
|
||||||
pub ram_style: Style,
|
pub ram_style: Style,
|
||||||
pub swap_style: Style,
|
pub swap_style: Style,
|
||||||
pub rx_style: Style,
|
pub rx_style: Style,
|
||||||
pub tx_style: Style,
|
pub tx_style: Style,
|
||||||
pub rx_total_style: Style,
|
pub total_rx_style: Style,
|
||||||
pub tx_total_style: Style,
|
pub total_tx_style: Style,
|
||||||
pub avg_colour_style: Style,
|
pub avg_colour_style: Style,
|
||||||
pub cpu_colour_styles: Vec<Style>,
|
pub cpu_colour_styles: Vec<Style>,
|
||||||
pub border_style: Style,
|
pub border_style: Style,
|
||||||
pub highlighted_border_style: Style,
|
pub highlighted_border_style: Style,
|
||||||
pub text_style: Style,
|
pub text_style: Style,
|
||||||
pub widget_title_style: Style,
|
pub widget_title_style: Style,
|
||||||
pub graph_style: Style,
|
pub graph_style: Style,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CanvasColours {
|
impl Default for CanvasColours {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let text_colour = Color::Gray;
|
let text_colour = Color::Gray;
|
||||||
|
|
||||||
CanvasColours {
|
CanvasColours {
|
||||||
currently_selected_text_colour: Color::Black,
|
currently_selected_text_colour: Color::Black,
|
||||||
currently_selected_bg_colour: Color::Cyan,
|
currently_selected_bg_colour: Color::Cyan,
|
||||||
currently_selected_text_style: Style::default().fg(Color::Black).bg(Color::Cyan),
|
currently_selected_text_style: Style::default().fg(Color::Black).bg(Color::Cyan),
|
||||||
table_header_style: Style::default().fg(Color::LightBlue),
|
table_header_style: Style::default().fg(Color::LightBlue),
|
||||||
ram_style: Style::default().fg(STANDARD_FIRST_COLOUR),
|
ram_style: Style::default().fg(STANDARD_FIRST_COLOUR),
|
||||||
swap_style: Style::default().fg(STANDARD_SECOND_COLOUR),
|
swap_style: Style::default().fg(STANDARD_SECOND_COLOUR),
|
||||||
rx_style: Style::default().fg(STANDARD_FIRST_COLOUR),
|
rx_style: Style::default().fg(STANDARD_FIRST_COLOUR),
|
||||||
tx_style: Style::default().fg(STANDARD_SECOND_COLOUR),
|
tx_style: Style::default().fg(STANDARD_SECOND_COLOUR),
|
||||||
rx_total_style: Style::default().fg(STANDARD_THIRD_COLOUR),
|
total_rx_style: Style::default().fg(STANDARD_THIRD_COLOUR),
|
||||||
tx_total_style: Style::default().fg(STANDARD_FOURTH_COLOUR),
|
total_tx_style: Style::default().fg(STANDARD_FOURTH_COLOUR),
|
||||||
avg_colour_style: Style::default().fg(AVG_COLOUR),
|
avg_colour_style: Style::default().fg(AVG_COLOUR),
|
||||||
cpu_colour_styles: Vec::new(),
|
cpu_colour_styles: Vec::new(),
|
||||||
border_style: Style::default().fg(text_colour),
|
border_style: Style::default().fg(text_colour),
|
||||||
highlighted_border_style: Style::default().fg(Color::LightBlue),
|
highlighted_border_style: Style::default().fg(Color::LightBlue),
|
||||||
text_style: Style::default().fg(text_colour),
|
text_style: Style::default().fg(text_colour),
|
||||||
widget_title_style: Style::default().fg(text_colour),
|
widget_title_style: Style::default().fg(text_colour),
|
||||||
graph_style: Style::default().fg(text_colour),
|
graph_style: Style::default().fg(text_colour),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CanvasColours {
|
impl CanvasColours {
|
||||||
pub fn set_text_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_text_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.text_style = get_style_from_config(colour)?;
|
self.text_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_border_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_border_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.border_style = get_style_from_config(colour)?;
|
self.border_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_highlighted_border_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_highlighted_border_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.highlighted_border_style = get_style_from_config(colour)?;
|
self.highlighted_border_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_table_header_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_table_header_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.table_header_style = get_style_from_config(colour)?.modifier(Modifier::BOLD);
|
self.table_header_style = get_style_from_config(colour)?.modifier(Modifier::BOLD);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_ram_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_ram_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.ram_style = get_style_from_config(colour)?;
|
self.ram_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_swap_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_swap_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.swap_style = get_style_from_config(colour)?;
|
self.swap_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_rx_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_rx_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.rx_style = get_style_from_config(colour)?;
|
self.rx_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_tx_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_tx_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.tx_style = get_style_from_config(colour)?;
|
self.tx_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_rx_total_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_rx_total_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.rx_total_style = get_style_from_config(colour)?;
|
self.total_rx_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_tx_total_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_tx_total_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.tx_total_style = get_style_from_config(colour)?;
|
self.total_tx_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_avg_cpu_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_avg_cpu_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.avg_colour_style = get_style_from_config(colour)?;
|
self.avg_colour_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_cpu_colours(&mut self, colours: &[String]) -> error::Result<()> {
|
pub fn set_cpu_colours(&mut self, colours: &[String]) -> error::Result<()> {
|
||||||
let max_amount = std::cmp::min(colours.len(), NUM_COLOURS as usize);
|
let max_amount = std::cmp::min(colours.len(), NUM_COLOURS as usize);
|
||||||
for (itx, colour) in colours.iter().enumerate() {
|
for (itx, colour) in colours.iter().enumerate() {
|
||||||
if itx >= max_amount {
|
if itx >= max_amount {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
self.cpu_colour_styles.push(get_style_from_config(colour)?);
|
self.cpu_colour_styles.push(get_style_from_config(colour)?);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_remaining_cpu_colours(&mut self) {
|
pub fn generate_remaining_cpu_colours(&mut self) {
|
||||||
let remaining_num_colours = NUM_COLOURS - self.cpu_colour_styles.len() as i32;
|
let remaining_num_colours = NUM_COLOURS - self.cpu_colour_styles.len() as i32;
|
||||||
self.cpu_colour_styles
|
self.cpu_colour_styles
|
||||||
.extend(gen_n_styles(remaining_num_colours));
|
.extend(gen_n_styles(remaining_num_colours));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_scroll_entry_text_color(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_scroll_entry_text_color(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.currently_selected_text_colour = get_colour_from_config(colour)?;
|
self.currently_selected_text_colour = get_colour_from_config(colour)?;
|
||||||
self.currently_selected_text_style = Style::default()
|
self.currently_selected_text_style = Style::default()
|
||||||
.fg(self.currently_selected_text_colour)
|
.fg(self.currently_selected_text_colour)
|
||||||
.bg(self.currently_selected_bg_colour);
|
.bg(self.currently_selected_bg_colour);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_scroll_entry_bg_color(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_scroll_entry_bg_color(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.currently_selected_bg_colour = get_colour_from_config(colour)?;
|
self.currently_selected_bg_colour = get_colour_from_config(colour)?;
|
||||||
self.currently_selected_text_style = Style::default()
|
self.currently_selected_text_style = Style::default()
|
||||||
.fg(self.currently_selected_text_colour)
|
.fg(self.currently_selected_text_colour)
|
||||||
.bg(self.currently_selected_bg_colour);
|
.bg(self.currently_selected_bg_colour);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_widget_title_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_widget_title_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.widget_title_style = get_style_from_config(colour)?;
|
self.widget_title_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_graph_colour(&mut self, colour: &str) -> error::Result<()> {
|
pub fn set_graph_colour(&mut self, colour: &str) -> error::Result<()> {
|
||||||
self.graph_style = get_style_from_config(colour)?;
|
self.graph_style = get_style_from_config(colour)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@ use tui::style::{Color, Style};
|
||||||
|
|
||||||
use crate::utils::{error, gen_util::*};
|
use crate::utils::{error, gen_util::*};
|
||||||
|
|
||||||
const GOLDEN_RATIO: f32 = 0.618_034; // Approx, good enough for use (also Clippy gets mad if it's too long)
|
const GOLDEN_RATIO: f32 = 0.618_034;
|
||||||
|
// Approx, good enough for use (also Clippy gets mad if it's too long)
|
||||||
pub const STANDARD_FIRST_COLOUR: Color = Color::LightMagenta;
|
pub const STANDARD_FIRST_COLOUR: Color = Color::LightMagenta;
|
||||||
pub const STANDARD_SECOND_COLOUR: Color = Color::LightYellow;
|
pub const STANDARD_SECOND_COLOUR: Color = Color::LightYellow;
|
||||||
pub const STANDARD_THIRD_COLOUR: Color = Color::LightCyan;
|
pub const STANDARD_THIRD_COLOUR: Color = Color::LightCyan;
|
||||||
|
@ -12,181 +13,181 @@ pub const STANDARD_FOURTH_COLOUR: Color = Color::LightGreen;
|
||||||
pub const AVG_COLOUR: Color = Color::Red;
|
pub const AVG_COLOUR: Color = Color::Red;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref COLOR_NAME_LOOKUP_TABLE: HashMap<&'static str, Color> = [
|
static ref COLOR_NAME_LOOKUP_TABLE: HashMap<&'static str, Color> = [
|
||||||
("reset", Color::Reset),
|
("reset", Color::Reset),
|
||||||
("black", Color::Black),
|
("black", Color::Black),
|
||||||
("red", Color::Red),
|
("red", Color::Red),
|
||||||
("green", Color::Green),
|
("green", Color::Green),
|
||||||
("yellow", Color::Yellow),
|
("yellow", Color::Yellow),
|
||||||
("blue", Color::Blue),
|
("blue", Color::Blue),
|
||||||
("magenta", Color::Magenta),
|
("magenta", Color::Magenta),
|
||||||
("cyan", Color::Cyan),
|
("cyan", Color::Cyan),
|
||||||
("gray", Color::Gray),
|
("gray", Color::Gray),
|
||||||
("darkgray", Color::DarkGray),
|
("darkgray", Color::DarkGray),
|
||||||
("lightred", Color::LightRed),
|
("lightred", Color::LightRed),
|
||||||
("lightgreen", Color::LightGreen),
|
("lightgreen", Color::LightGreen),
|
||||||
("lightyellow", Color::LightYellow),
|
("lightyellow", Color::LightYellow),
|
||||||
("lightblue", Color::LightBlue),
|
("lightblue", Color::LightBlue),
|
||||||
("lightmagenta", Color::LightMagenta),
|
("lightmagenta", Color::LightMagenta),
|
||||||
("lightcyan", Color::LightCyan),
|
("lightcyan", Color::LightCyan),
|
||||||
("white", Color::White)
|
("white", Color::White)
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates random colours. Strategy found from
|
/// Generates random colours. Strategy found from
|
||||||
/// https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
|
/// https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
|
||||||
pub fn gen_n_styles(num_to_gen: i32) -> Vec<Style> {
|
pub fn gen_n_styles(num_to_gen: i32) -> Vec<Style> {
|
||||||
fn gen_hsv(h: f32) -> f32 {
|
fn gen_hsv(h: f32) -> f32 {
|
||||||
let new_val = h + GOLDEN_RATIO;
|
let new_val = h + GOLDEN_RATIO;
|
||||||
if new_val > 1.0 {
|
if new_val > 1.0 {
|
||||||
new_val.fract()
|
new_val.fract()
|
||||||
} else {
|
} else {
|
||||||
new_val
|
new_val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// This takes in an h, s, and v value of range [0, 1]
|
/// This takes in an h, s, and v value of range [0, 1]
|
||||||
/// For explanation of what this does, see
|
/// For explanation of what this does, see
|
||||||
/// https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative
|
/// https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative
|
||||||
fn hsv_to_rgb(hue: f32, saturation: f32, value: f32) -> (u8, u8, u8) {
|
fn hsv_to_rgb(hue: f32, saturation: f32, value: f32) -> (u8, u8, u8) {
|
||||||
fn hsv_helper(num: u32, hu: f32, sat: f32, val: f32) -> f32 {
|
fn hsv_helper(num: u32, hu: f32, sat: f32, val: f32) -> f32 {
|
||||||
let k = (num as f32 + hu * 6.0) % 6.0;
|
let k = (num as f32 + hu * 6.0) % 6.0;
|
||||||
val - val * sat * float_max(float_min(k, float_min(4.1 - k, 1.1)), 0.0)
|
val - val * sat * float_max(float_min(k, float_min(4.1 - k, 1.1)), 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
(
|
(
|
||||||
(hsv_helper(5, hue, saturation, value) * 255.0) as u8,
|
(hsv_helper(5, hue, saturation, value) * 255.0) as u8,
|
||||||
(hsv_helper(3, hue, saturation, value) * 255.0) as u8,
|
(hsv_helper(3, hue, saturation, value) * 255.0) as u8,
|
||||||
(hsv_helper(1, hue, saturation, value) * 255.0) as u8,
|
(hsv_helper(1, hue, saturation, value) * 255.0) as u8,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate colours
|
// Generate colours
|
||||||
// Why do we need so many colours? Because macOS default terminal
|
// Why do we need so many colours? Because macOS default terminal
|
||||||
// throws a tantrum if you don't give it supported colours, but so
|
// throws a tantrum if you don't give it supported colours, but so
|
||||||
// does PowerShell with some colours (Magenta and Yellow)!
|
// does PowerShell with some colours (Magenta and Yellow)!
|
||||||
let mut colour_vec: Vec<Style> = vec![
|
let mut colour_vec: Vec<Style> = vec![
|
||||||
Style::default().fg(STANDARD_FIRST_COLOUR),
|
Style::default().fg(STANDARD_FIRST_COLOUR),
|
||||||
Style::default().fg(STANDARD_SECOND_COLOUR),
|
Style::default().fg(STANDARD_SECOND_COLOUR),
|
||||||
Style::default().fg(STANDARD_THIRD_COLOUR),
|
Style::default().fg(STANDARD_THIRD_COLOUR),
|
||||||
Style::default().fg(STANDARD_FOURTH_COLOUR),
|
Style::default().fg(STANDARD_FOURTH_COLOUR),
|
||||||
Style::default().fg(Color::LightBlue),
|
Style::default().fg(Color::LightBlue),
|
||||||
Style::default().fg(Color::LightRed),
|
Style::default().fg(Color::LightRed),
|
||||||
Style::default().fg(Color::Cyan),
|
Style::default().fg(Color::Cyan),
|
||||||
Style::default().fg(Color::Green),
|
Style::default().fg(Color::Green),
|
||||||
Style::default().fg(Color::Blue),
|
Style::default().fg(Color::Blue),
|
||||||
Style::default().fg(Color::Red),
|
Style::default().fg(Color::Red),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut h: f32 = 0.4; // We don't need random colours... right?
|
let mut h: f32 = 0.4; // We don't need random colours... right?
|
||||||
for _i in 0..(num_to_gen - 10) {
|
for _i in 0..(num_to_gen - 10) {
|
||||||
h = gen_hsv(h);
|
h = gen_hsv(h);
|
||||||
let result = hsv_to_rgb(h, 0.5, 0.95);
|
let result = hsv_to_rgb(h, 0.5, 0.95);
|
||||||
colour_vec.push(Style::default().fg(Color::Rgb(result.0, result.1, result.2)));
|
colour_vec.push(Style::default().fg(Color::Rgb(result.0, result.1, result.2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
colour_vec
|
colour_vec
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_hex_to_color(hex: &str) -> error::Result<Color> {
|
pub fn convert_hex_to_color(hex: &str) -> error::Result<Color> {
|
||||||
fn convert_hex_to_rgb(hex: &str) -> error::Result<(u8, u8, u8)> {
|
fn convert_hex_to_rgb(hex: &str) -> error::Result<(u8, u8, u8)> {
|
||||||
if hex.len() == 7 && &hex[0..1] == "#" {
|
if hex.len() == 7 && &hex[0..1] == "#" {
|
||||||
let r = u8::from_str_radix(&hex[1..3], 16)?;
|
let r = u8::from_str_radix(&hex[1..3], 16)?;
|
||||||
let g = u8::from_str_radix(&hex[3..5], 16)?;
|
let g = u8::from_str_radix(&hex[3..5], 16)?;
|
||||||
let b = u8::from_str_radix(&hex[5..7], 16)?;
|
let b = u8::from_str_radix(&hex[5..7], 16)?;
|
||||||
|
|
||||||
return Ok((r, g, b));
|
return Ok((r, g, b));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(error::BottomError::GenericError(format!(
|
Err(error::BottomError::GenericError(format!(
|
||||||
"Colour hex {} is not of valid length. It must be a 7 character string of the form \"#112233\".",
|
"Colour hex {} is not of valid length. It must be a 7 character string of the form \"#112233\".",
|
||||||
hex
|
hex
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
let rgb = convert_hex_to_rgb(hex)?;
|
let rgb = convert_hex_to_rgb(hex)?;
|
||||||
Ok(Color::Rgb(rgb.0, rgb.1, rgb.2))
|
Ok(Color::Rgb(rgb.0, rgb.1, rgb.2))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_style_from_config(input_val: &str) -> error::Result<Style> {
|
pub fn get_style_from_config(input_val: &str) -> error::Result<Style> {
|
||||||
if input_val.len() > 1 {
|
if input_val.len() > 1 {
|
||||||
if &input_val[0..1] == "#" {
|
if &input_val[0..1] == "#" {
|
||||||
get_style_from_hex(input_val)
|
get_style_from_hex(input_val)
|
||||||
} else if input_val.contains(',') {
|
} else if input_val.contains(',') {
|
||||||
get_style_from_rgb(input_val)
|
get_style_from_rgb(input_val)
|
||||||
} else {
|
} else {
|
||||||
get_style_from_color_name(input_val)
|
get_style_from_color_name(input_val)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(error::BottomError::GenericError(format!(
|
Err(error::BottomError::GenericError(format!(
|
||||||
"Colour input {} is not valid.",
|
"Colour input {} is not valid.",
|
||||||
input_val
|
input_val
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_colour_from_config(input_val: &str) -> error::Result<Color> {
|
pub fn get_colour_from_config(input_val: &str) -> error::Result<Color> {
|
||||||
if input_val.len() > 1 {
|
if input_val.len() > 1 {
|
||||||
if &input_val[0..1] == "#" {
|
if &input_val[0..1] == "#" {
|
||||||
convert_hex_to_color(input_val)
|
convert_hex_to_color(input_val)
|
||||||
} else if input_val.contains(',') {
|
} else if input_val.contains(',') {
|
||||||
convert_rgb_to_color(input_val)
|
convert_rgb_to_color(input_val)
|
||||||
} else {
|
} else {
|
||||||
convert_name_to_color(input_val)
|
convert_name_to_color(input_val)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(error::BottomError::GenericError(format!(
|
Err(error::BottomError::GenericError(format!(
|
||||||
"Colour input {} is not valid.",
|
"Colour input {} is not valid.",
|
||||||
input_val
|
input_val
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_style_from_hex(hex: &str) -> error::Result<Style> {
|
pub fn get_style_from_hex(hex: &str) -> error::Result<Style> {
|
||||||
Ok(Style::default().fg(convert_hex_to_color(hex)?))
|
Ok(Style::default().fg(convert_hex_to_color(hex)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_rgb_to_color(rgb_str: &str) -> error::Result<Color> {
|
fn convert_rgb_to_color(rgb_str: &str) -> error::Result<Color> {
|
||||||
let rgb_list = rgb_str.split(',');
|
let rgb_list = rgb_str.split(',');
|
||||||
let rgb = rgb_list
|
let rgb = rgb_list
|
||||||
.filter_map(|val| {
|
.filter_map(|val| {
|
||||||
if let Ok(res) = val.to_string().trim().parse::<u8>() {
|
if let Ok(res) = val.to_string().trim().parse::<u8>() {
|
||||||
Some(res)
|
Some(res)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if rgb.len() == 3 {
|
if rgb.len() == 3 {
|
||||||
Ok(Color::Rgb(rgb[0], rgb[1], rgb[2]))
|
Ok(Color::Rgb(rgb[0], rgb[1], rgb[2]))
|
||||||
} else {
|
} else {
|
||||||
Err(error::BottomError::GenericError(format!(
|
Err(error::BottomError::GenericError(format!(
|
||||||
"RGB colour {} is not of valid length. It must be a comma separated value with 3 integers from 0 to 255, like \"255, 0, 155\".",
|
"RGB colour {} is not of valid length. It must be a comma separated value with 3 integers from 0 to 255, like \"255, 0, 155\".",
|
||||||
rgb_str
|
rgb_str
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_style_from_rgb(rgb_str: &str) -> error::Result<Style> {
|
pub fn get_style_from_rgb(rgb_str: &str) -> error::Result<Style> {
|
||||||
Ok(Style::default().fg(convert_rgb_to_color(rgb_str)?))
|
Ok(Style::default().fg(convert_rgb_to_color(rgb_str)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_name_to_color(color_name: &str) -> error::Result<Color> {
|
fn convert_name_to_color(color_name: &str) -> error::Result<Color> {
|
||||||
let color = COLOR_NAME_LOOKUP_TABLE.get(color_name.to_lowercase().as_str());
|
let color = COLOR_NAME_LOOKUP_TABLE.get(color_name.to_lowercase().as_str());
|
||||||
if let Some(color) = color {
|
if let Some(color) = color {
|
||||||
return Ok(*color);
|
return Ok(*color);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(error::BottomError::GenericError(format!(
|
Err(error::BottomError::GenericError(format!(
|
||||||
"Color {} is not a supported config colour. bottom supports the following named colours as strings: \
|
"Color {} is not a supported config colour. bottom supports the following named colours as strings: \
|
||||||
Reset, Black, Red, Green, Yellow, Blue, Magenta, Cyan, Gray, DarkGray, LightRed, LightGreen, \
|
Reset, Black, Red, Green, Yellow, Blue, Magenta, Cyan, Gray, DarkGray, LightRed, LightGreen, \
|
||||||
LightYellow, LightBlue, LightMagenta, LightCyan, White",
|
LightYellow, LightBlue, LightMagenta, LightCyan, White",
|
||||||
color_name
|
color_name
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_style_from_color_name(color_name: &str) -> error::Result<Style> {
|
pub fn get_style_from_color_name(color_name: &str) -> error::Result<Style> {
|
||||||
Ok(Style::default().fg(convert_name_to_color(color_name)?))
|
Ok(Style::default().fg(convert_name_to_color(color_name)?))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,145 +8,154 @@ use crate::app;
|
||||||
/// `width thresholds` and `desired_widths_ratio` should be the same length.
|
/// `width thresholds` and `desired_widths_ratio` should be the same length.
|
||||||
/// Otherwise bad things happen.
|
/// Otherwise bad things happen.
|
||||||
pub fn get_variable_intrinsic_widths(
|
pub fn get_variable_intrinsic_widths(
|
||||||
total_width: u16, desired_widths_ratio: &[f64], width_thresholds: &[usize],
|
total_width: u16, desired_widths_ratio: &[f64], width_thresholds: &[usize],
|
||||||
) -> (Vec<u16>, usize) {
|
) -> (Vec<u16>, usize) {
|
||||||
let num_widths = desired_widths_ratio.len();
|
let num_widths = desired_widths_ratio.len();
|
||||||
let mut resulting_widths: Vec<u16> = vec![0; num_widths];
|
let mut resulting_widths: Vec<u16> = vec![0; num_widths];
|
||||||
let mut last_index = 0;
|
let mut last_index = 0;
|
||||||
|
|
||||||
let mut remaining_width = (total_width - (num_widths as u16 - 1)) as i32; // Required for spaces...
|
let mut remaining_width = (total_width - (num_widths as u16 - 1)) as i32; // Required for spaces...
|
||||||
let desired_widths = desired_widths_ratio
|
let desired_widths = desired_widths_ratio
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&desired_width_ratio| (desired_width_ratio * total_width as f64) as i32)
|
.map(|&desired_width_ratio| (desired_width_ratio * total_width as f64) as i32)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for (itx, desired_width) in desired_widths.into_iter().enumerate() {
|
for (itx, desired_width) in desired_widths.into_iter().enumerate() {
|
||||||
resulting_widths[itx] = if desired_width < width_thresholds[itx] as i32 {
|
resulting_widths[itx] = if desired_width < width_thresholds[itx] as i32 {
|
||||||
// Try to take threshold, else, 0
|
// Try to take threshold, else, 0
|
||||||
if remaining_width < width_thresholds[itx] as i32 {
|
if remaining_width < width_thresholds[itx] as i32 {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
remaining_width -= width_thresholds[itx] as i32;
|
remaining_width -= width_thresholds[itx] as i32;
|
||||||
width_thresholds[itx] as u16
|
width_thresholds[itx] as u16
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Take as large as possible
|
// Take as large as possible
|
||||||
if remaining_width < desired_width {
|
if remaining_width < desired_width {
|
||||||
// Check the biggest chunk possible
|
// Check the biggest chunk possible
|
||||||
if remaining_width < width_thresholds[itx] as i32 {
|
if remaining_width < width_thresholds[itx] as i32 {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
let temp_width = remaining_width;
|
let temp_width = remaining_width;
|
||||||
remaining_width = 0;
|
remaining_width = 0;
|
||||||
temp_width as u16
|
temp_width as u16
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
remaining_width -= desired_width;
|
remaining_width -= desired_width;
|
||||||
desired_width as u16
|
desired_width as u16
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if resulting_widths[itx] == 0 {
|
if resulting_widths[itx] == 0 {
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
last_index += 1;
|
last_index += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple redistribution tactic - if there's any space left, split it evenly amongst all members
|
// Simple redistribution tactic - if there's any space left, split it evenly amongst all members
|
||||||
if last_index < num_widths {
|
if last_index < num_widths && last_index != 0 {
|
||||||
let for_all_widths = (remaining_width / last_index as i32) as u16;
|
let for_all_widths = (remaining_width / last_index as i32) as u16;
|
||||||
let mut remainder = remaining_width % last_index as i32;
|
let mut remainder = remaining_width % last_index as i32;
|
||||||
|
|
||||||
for resulting_width in &mut resulting_widths {
|
for resulting_width in &mut resulting_widths {
|
||||||
*resulting_width += for_all_widths;
|
*resulting_width += for_all_widths;
|
||||||
if remainder > 0 {
|
if remainder > 0 {
|
||||||
*resulting_width += 1;
|
*resulting_width += 1;
|
||||||
remainder -= 1;
|
remainder -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(resulting_widths, last_index)
|
(resulting_widths, last_index)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, unused_variables)]
|
#[allow(dead_code, unused_variables)]
|
||||||
pub fn get_search_start_position(
|
pub fn get_search_start_position(
|
||||||
num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize,
|
num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize,
|
||||||
current_cursor_position: usize, is_resized: bool,
|
current_cursor_position: usize, is_resized: bool,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
if is_resized {
|
if is_resized {
|
||||||
*cursor_bar = 0;
|
*cursor_bar = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
match cursor_direction {
|
match cursor_direction {
|
||||||
app::CursorDirection::RIGHT => {
|
app::CursorDirection::RIGHT => {
|
||||||
if current_cursor_position < *cursor_bar + num_columns {
|
if current_cursor_position < *cursor_bar + num_columns {
|
||||||
// If, using previous_scrolled_position, we can see the element
|
// If, using previous_scrolled_position, we can see the element
|
||||||
// (so within that and + num_rows) just reuse the current previously scrolled position
|
// (so within that and + num_rows) just reuse the current previously scrolled position
|
||||||
*cursor_bar
|
*cursor_bar
|
||||||
} else if current_cursor_position >= num_columns {
|
} else if current_cursor_position >= num_columns {
|
||||||
// Else if the current position past the last element visible in the list, omit
|
// Else if the current position past the last element visible in the list, omit
|
||||||
// until we can see that element
|
// until we can see that element
|
||||||
*cursor_bar = current_cursor_position - num_columns;
|
*cursor_bar = current_cursor_position - num_columns;
|
||||||
*cursor_bar
|
*cursor_bar
|
||||||
} else {
|
} else {
|
||||||
// Else, if it is not past the last element visible, do not omit anything
|
// Else, if it is not past the last element visible, do not omit anything
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app::CursorDirection::LEFT => {
|
app::CursorDirection::LEFT => {
|
||||||
if current_cursor_position <= *cursor_bar {
|
if current_cursor_position <= *cursor_bar {
|
||||||
// If it's past the first element, then show from that element downwards
|
// If it's past the first element, then show from that element downwards
|
||||||
*cursor_bar = current_cursor_position;
|
*cursor_bar = current_cursor_position;
|
||||||
*cursor_bar
|
*cursor_bar
|
||||||
} else if current_cursor_position >= *cursor_bar + num_columns {
|
} else if current_cursor_position >= *cursor_bar + num_columns {
|
||||||
*cursor_bar = current_cursor_position - num_columns;
|
*cursor_bar = current_cursor_position - num_columns;
|
||||||
*cursor_bar
|
*cursor_bar
|
||||||
} else {
|
} else {
|
||||||
// Else, don't change what our start position is from whatever it is set to!
|
// Else, don't change what our start position is from whatever it is set to!
|
||||||
*cursor_bar
|
*cursor_bar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_start_position(
|
pub fn get_start_position(
|
||||||
num_rows: u64, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut u64,
|
num_rows: u64, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut u64,
|
||||||
currently_selected_position: u64, is_resized: bool,
|
currently_selected_position: u64, is_resized: bool,
|
||||||
) -> u64 {
|
) -> u64 {
|
||||||
if is_resized {
|
if is_resized {
|
||||||
*scroll_position_bar = 0;
|
*scroll_position_bar = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
match scroll_direction {
|
match scroll_direction {
|
||||||
app::ScrollDirection::DOWN => {
|
app::ScrollDirection::DOWN => {
|
||||||
if currently_selected_position < *scroll_position_bar + num_rows {
|
if currently_selected_position < *scroll_position_bar + num_rows {
|
||||||
// If, using previous_scrolled_position, we can see the element
|
// If, using previous_scrolled_position, we can see the element
|
||||||
// (so within that and + num_rows) just reuse the current previously scrolled position
|
// (so within that and + num_rows) just reuse the current previously scrolled position
|
||||||
*scroll_position_bar
|
*scroll_position_bar
|
||||||
} else if currently_selected_position >= num_rows {
|
} else if currently_selected_position >= num_rows {
|
||||||
// Else if the current position past the last element visible in the list, omit
|
// Else if the current position past the last element visible in the list, omit
|
||||||
// until we can see that element
|
// until we can see that element
|
||||||
*scroll_position_bar = currently_selected_position - num_rows;
|
*scroll_position_bar = currently_selected_position - num_rows;
|
||||||
*scroll_position_bar
|
*scroll_position_bar
|
||||||
} else {
|
} else {
|
||||||
// Else, if it is not past the last element visible, do not omit anything
|
// Else, if it is not past the last element visible, do not omit anything
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app::ScrollDirection::UP => {
|
app::ScrollDirection::UP => {
|
||||||
if currently_selected_position <= *scroll_position_bar {
|
if currently_selected_position <= *scroll_position_bar {
|
||||||
// If it's past the first element, then show from that element downwards
|
// If it's past the first element, then show from that element downwards
|
||||||
*scroll_position_bar = currently_selected_position;
|
*scroll_position_bar = currently_selected_position;
|
||||||
*scroll_position_bar
|
*scroll_position_bar
|
||||||
} else if currently_selected_position >= *scroll_position_bar + num_rows {
|
} else if currently_selected_position >= *scroll_position_bar + num_rows {
|
||||||
*scroll_position_bar = currently_selected_position - num_rows;
|
*scroll_position_bar = currently_selected_position - num_rows;
|
||||||
*scroll_position_bar
|
*scroll_position_bar
|
||||||
} else {
|
} else {
|
||||||
// Else, don't change what our start position is from whatever it is set to!
|
// Else, don't change what our start position is from whatever it is set to!
|
||||||
*scroll_position_bar
|
*scroll_position_bar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
std::cmp::min(
|
||||||
|
(num_bars_available as f64 * use_percentage / 100.0).round() as usize,
|
||||||
|
num_bars_available,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
pub const STALE_MAX_MILLISECONDS: u128 = 60 * 1000; // How long to store data.
|
pub const STALE_MAX_MILLISECONDS: u128 = 60 * 1000;
|
||||||
|
// How long to store data.
|
||||||
pub const TIME_STARTS_FROM: u64 = 60 * 1000;
|
pub const TIME_STARTS_FROM: u64 = 60 * 1000;
|
||||||
pub const TICK_RATE_IN_MILLISECONDS: u64 = 200; // How fast the screen refreshes
|
pub const TICK_RATE_IN_MILLISECONDS: u64 = 200;
|
||||||
|
// How fast the screen refreshes
|
||||||
pub const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u128 = 1000;
|
pub const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u128 = 1000;
|
||||||
pub const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u128 = 1000;
|
pub const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u128 = 1000;
|
||||||
pub const NUM_COLOURS: i32 = 256;
|
pub const NUM_COLOURS: i32 = 256;
|
||||||
|
@ -11,48 +13,48 @@ pub const DEFAULT_WINDOWS_CONFIG_FILE_PATH: &str = "bottom/bottom.toml";
|
||||||
|
|
||||||
// Help text
|
// Help text
|
||||||
pub const GENERAL_HELP_TEXT: [&str; 15] = [
|
pub const GENERAL_HELP_TEXT: [&str; 15] = [
|
||||||
"General Keybindings\n\n",
|
"General Keybindings\n\n",
|
||||||
"q, Ctrl-c Quit bottom\n",
|
"q, Ctrl-c Quit bottom\n",
|
||||||
"Esc Close filters, dialog boxes, etc.\n",
|
"Esc Close filters, dialog boxes, etc.\n",
|
||||||
"Ctrl-r Reset all data\n",
|
"Ctrl-r Reset all data\n",
|
||||||
"f Freeze display\n",
|
"f Freeze display\n",
|
||||||
"Ctrl-Arrow Move currently selected widget\n",
|
"Ctrl-Arrow Move currently selected widget\n",
|
||||||
"Shift-Arrow Move currently selected widget\n",
|
"Shift-Arrow Move currently selected widget\n",
|
||||||
"H/J/K/L Move currently selected widget up/down/left/right\n",
|
"H/J/K/L Move currently selected widget up/down/left/right\n",
|
||||||
"Up, k Move cursor up\n",
|
"Up, k Move cursor up\n",
|
||||||
"Down, j Move cursor down\n",
|
"Down, j Move cursor down\n",
|
||||||
"? Open the help screen\n",
|
"? Open the help screen\n",
|
||||||
"gg Skip to the first entry of a list\n",
|
"gg Skip to the first entry of a list\n",
|
||||||
"G Skip to the last entry of a list\n",
|
"G Skip to the last entry of a list\n",
|
||||||
"Enter Maximize the currently selected widget\n",
|
"Enter Maximize the currently selected widget\n",
|
||||||
"/ Filter out graph lines (only CPU at the moment)\n",
|
"/ Filter out graph lines (only CPU at the moment)\n",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const PROCESS_HELP_TEXT: [&str; 8] = [
|
pub const PROCESS_HELP_TEXT: [&str; 8] = [
|
||||||
"Process Keybindings\n\n",
|
"Process Keybindings\n\n",
|
||||||
"dd Kill the highlighted process\n",
|
"dd Kill the highlighted process\n",
|
||||||
"c Sort by CPU usage\n",
|
"c Sort by CPU usage\n",
|
||||||
"m Sort by memory usage\n",
|
"m Sort by memory usage\n",
|
||||||
"p Sort by PID\n",
|
"p Sort by PID\n",
|
||||||
"n Sort by process name\n",
|
"n Sort by process name\n",
|
||||||
"Tab Group together processes with the same name\n",
|
"Tab Group together processes with the same name\n",
|
||||||
"Ctrl-f, / Open up the search widget\n",
|
"Ctrl-f, / Open up the search widget\n",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const SEARCH_HELP_TEXT: [&str; 13] = [
|
pub const SEARCH_HELP_TEXT: [&str; 13] = [
|
||||||
"Search Keybindings\n\n",
|
"Search Keybindings\n\n",
|
||||||
"Tab Toggle between searching for PID and name.\n",
|
"Tab Toggle between searching for PID and name.\n",
|
||||||
"Esc Close search widget\n",
|
"Esc Close search widget\n",
|
||||||
"Ctrl-a Skip to the start of search widget\n",
|
"Ctrl-a Skip to the start of search widget\n",
|
||||||
"Ctrl-e Skip to the end of search widget\n",
|
"Ctrl-e Skip to the end of search widget\n",
|
||||||
"Ctrl-u Clear the current search query\n",
|
"Ctrl-u Clear the current search query\n",
|
||||||
"Backspace Delete the character behind the cursor\n",
|
"Backspace Delete the character behind the cursor\n",
|
||||||
"Delete Delete the character at the cursor\n",
|
"Delete Delete the character at the cursor\n",
|
||||||
"Left Move cursor left\n",
|
"Left Move cursor left\n",
|
||||||
"Right Move cursor right\n",
|
"Right Move cursor right\n",
|
||||||
"Alt-c/F1 Toggle whether to ignore case\n",
|
"Alt-c/F1 Toggle whether to ignore case\n",
|
||||||
"Alt-w/F2 Toggle whether to match the whole word\n",
|
"Alt-w/F2 Toggle whether to match the whole word\n",
|
||||||
"Alt-r/F3 Toggle whether to use regex\n",
|
"Alt-r/F3 Toggle whether to use regex\n",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const DEFAULT_CONFIG_CONTENT: &str = r##"
|
pub const DEFAULT_CONFIG_CONTENT: &str = r##"
|
||||||
|
|
|
@ -6,317 +6,320 @@ use std::collections::HashMap;
|
||||||
use constants::*;
|
use constants::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
data_farmer,
|
data_farmer,
|
||||||
data_harvester::{self, processes::ProcessHarvest},
|
data_harvester::{self, processes::ProcessHarvest},
|
||||||
App,
|
App,
|
||||||
},
|
},
|
||||||
constants,
|
constants,
|
||||||
utils::gen_util::{get_exact_byte_values, get_simple_byte_values},
|
utils::gen_util::{get_exact_byte_values, get_simple_byte_values},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct ConvertedNetworkData {
|
pub struct ConvertedNetworkData {
|
||||||
pub rx: Vec<(f64, f64)>,
|
pub rx: Vec<(f64, f64)>,
|
||||||
pub tx: Vec<(f64, f64)>,
|
pub tx: Vec<(f64, f64)>,
|
||||||
pub rx_display: String,
|
pub rx_display: String,
|
||||||
pub tx_display: String,
|
pub tx_display: String,
|
||||||
pub total_rx_display: String,
|
pub total_rx_display: String,
|
||||||
pub total_tx_display: String,
|
pub total_tx_display: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Debug)]
|
#[derive(Clone, Default, Debug)]
|
||||||
pub struct ConvertedProcessData {
|
pub struct ConvertedProcessData {
|
||||||
pub pid: u32,
|
pub pid: u32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub cpu_usage: f64,
|
pub cpu_usage: f64,
|
||||||
pub mem_usage: f64,
|
pub mem_usage: f64,
|
||||||
pub group_pids: Vec<u32>,
|
pub group_pids: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Debug)]
|
#[derive(Clone, Default, Debug)]
|
||||||
pub struct ConvertedCpuData {
|
pub struct ConvertedCpuData {
|
||||||
pub cpu_name: String,
|
pub cpu_name: String,
|
||||||
pub cpu_data: Vec<(f64, f64)>,
|
/// Tuple is time, value
|
||||||
|
pub cpu_data: Vec<(f64, f64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_temp_row(app: &App) -> Vec<Vec<String>> {
|
pub fn convert_temp_row(app: &App) -> Vec<Vec<String>> {
|
||||||
let mut sensor_vector: Vec<Vec<String>> = Vec::new();
|
let mut sensor_vector: Vec<Vec<String>> = Vec::new();
|
||||||
|
|
||||||
let current_data = &app.data_collection;
|
let current_data = &app.data_collection;
|
||||||
let temp_type = &app.app_config_fields.temperature_type;
|
let temp_type = &app.app_config_fields.temperature_type;
|
||||||
|
|
||||||
if current_data.temp_harvest.is_empty() {
|
if current_data.temp_harvest.is_empty() {
|
||||||
sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()])
|
sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()])
|
||||||
} else {
|
} else {
|
||||||
for sensor in ¤t_data.temp_harvest {
|
for sensor in ¤t_data.temp_harvest {
|
||||||
sensor_vector.push(vec![
|
sensor_vector.push(vec![
|
||||||
sensor.component_name.to_string(),
|
sensor.component_name.to_string(),
|
||||||
(sensor.temperature.ceil() as u64).to_string()
|
(sensor.temperature.ceil() as u64).to_string()
|
||||||
+ match temp_type {
|
+ match temp_type {
|
||||||
data_harvester::temperature::TemperatureType::Celsius => "C",
|
data_harvester::temperature::TemperatureType::Celsius => "C",
|
||||||
data_harvester::temperature::TemperatureType::Kelvin => "K",
|
data_harvester::temperature::TemperatureType::Kelvin => "K",
|
||||||
data_harvester::temperature::TemperatureType::Fahrenheit => "F",
|
data_harvester::temperature::TemperatureType::Fahrenheit => "F",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sensor_vector
|
sensor_vector
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec<Vec<String>> {
|
pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec<Vec<String>> {
|
||||||
let mut disk_vector: Vec<Vec<String>> = Vec::new();
|
let mut disk_vector: Vec<Vec<String>> = Vec::new();
|
||||||
for (itx, disk) in current_data.disk_harvest.iter().enumerate() {
|
for (itx, disk) in current_data.disk_harvest.iter().enumerate() {
|
||||||
let io_activity = if current_data.io_labels.len() > itx {
|
let io_activity = if current_data.io_labels.len() > itx {
|
||||||
let converted_read = get_simple_byte_values(current_data.io_labels[itx].0, false);
|
let converted_read = get_simple_byte_values(current_data.io_labels[itx].0, false);
|
||||||
let converted_write = get_simple_byte_values(current_data.io_labels[itx].1, false);
|
let converted_write = get_simple_byte_values(current_data.io_labels[itx].1, false);
|
||||||
(
|
(
|
||||||
format!("{:.*}{}/s", 0, converted_read.0, converted_read.1),
|
format!("{:.*}{}/s", 0, converted_read.0, converted_read.1),
|
||||||
format!("{:.*}{}/s", 0, converted_write.0, converted_write.1),
|
format!("{:.*}{}/s", 0, converted_write.0, converted_write.1),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
("0B/s".to_string(), "0B/s".to_string())
|
("0B/s".to_string(), "0B/s".to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
let converted_free_space = get_simple_byte_values(disk.free_space, false);
|
let converted_free_space = get_simple_byte_values(disk.free_space, false);
|
||||||
let converted_total_space = get_simple_byte_values(disk.total_space, false);
|
let converted_total_space = get_simple_byte_values(disk.total_space, false);
|
||||||
disk_vector.push(vec![
|
disk_vector.push(vec![
|
||||||
disk.name.to_string(),
|
disk.name.to_string(),
|
||||||
disk.mount_point.to_string(),
|
disk.mount_point.to_string(),
|
||||||
format!(
|
format!(
|
||||||
"{:.0}%",
|
"{:.0}%",
|
||||||
disk.used_space as f64 / disk.total_space as f64 * 100_f64
|
disk.used_space as f64 / disk.total_space as f64 * 100_f64
|
||||||
),
|
),
|
||||||
format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1),
|
format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1),
|
||||||
format!(
|
format!(
|
||||||
"{:.*}{}",
|
"{:.*}{}",
|
||||||
0, converted_total_space.0, converted_total_space.1
|
0, converted_total_space.0, converted_total_space.1
|
||||||
),
|
),
|
||||||
io_activity.0,
|
io_activity.0,
|
||||||
io_activity.1,
|
io_activity.1,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
disk_vector
|
disk_vector
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_cpu_data_points(
|
pub fn convert_cpu_data_points(
|
||||||
show_avg_cpu: bool, current_data: &data_farmer::DataCollection,
|
show_avg_cpu: bool, current_data: &data_farmer::DataCollection,
|
||||||
) -> Vec<ConvertedCpuData> {
|
) -> Vec<ConvertedCpuData> {
|
||||||
let mut cpu_data_vector: Vec<ConvertedCpuData> = Vec::new();
|
let mut cpu_data_vector: Vec<ConvertedCpuData> = Vec::new();
|
||||||
let current_time = current_data.current_instant;
|
let current_time = current_data.current_instant;
|
||||||
let cpu_listing_offset = if show_avg_cpu { 0 } else { 1 };
|
let cpu_listing_offset = if show_avg_cpu { 0 } else { 1 };
|
||||||
|
|
||||||
for (time, data) in ¤t_data.timed_data_vec {
|
for (time, data) in ¤t_data.timed_data_vec {
|
||||||
let time_from_start: f64 = (TIME_STARTS_FROM as f64
|
let time_from_start: f64 = (TIME_STARTS_FROM as f64
|
||||||
- current_time.duration_since(*time).as_millis() as f64)
|
- current_time.duration_since(*time).as_millis() as f64)
|
||||||
.floor();
|
.floor();
|
||||||
|
|
||||||
for (itx, cpu) in data.cpu_data.iter().enumerate() {
|
for (itx, cpu) in data.cpu_data.iter().enumerate() {
|
||||||
if !show_avg_cpu && itx == 0 {
|
if !show_avg_cpu && itx == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the vector exists yet
|
// Check if the vector exists yet
|
||||||
let itx_offset = itx - cpu_listing_offset;
|
let itx_offset = itx - cpu_listing_offset;
|
||||||
if cpu_data_vector.len() <= itx_offset {
|
if cpu_data_vector.len() <= itx_offset {
|
||||||
cpu_data_vector.push(ConvertedCpuData::default());
|
cpu_data_vector.push(ConvertedCpuData::default());
|
||||||
cpu_data_vector[itx_offset].cpu_name =
|
cpu_data_vector[itx_offset].cpu_name =
|
||||||
current_data.cpu_harvest[itx].cpu_name.clone();
|
current_data.cpu_harvest[itx].cpu_name.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
//Insert joiner points
|
//Insert joiner points
|
||||||
for &(joiner_offset, joiner_val) in &cpu.1 {
|
for &(joiner_offset, joiner_val) in &cpu.1 {
|
||||||
let offset_time = time_from_start - joiner_offset as f64;
|
let offset_time = time_from_start - joiner_offset as f64;
|
||||||
cpu_data_vector[itx_offset]
|
cpu_data_vector[itx_offset]
|
||||||
.cpu_data
|
.cpu_data
|
||||||
.push((offset_time, joiner_val));
|
.push((offset_time, joiner_val));
|
||||||
}
|
}
|
||||||
|
|
||||||
cpu_data_vector[itx_offset]
|
cpu_data_vector[itx_offset]
|
||||||
.cpu_data
|
.cpu_data
|
||||||
.push((time_from_start, cpu.0));
|
.push((time_from_start, cpu.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cpu_data_vector
|
cpu_data_vector
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_mem_data_points(current_data: &data_farmer::DataCollection) -> Vec<(f64, f64)> {
|
pub fn convert_mem_data_points(current_data: &data_farmer::DataCollection) -> Vec<(f64, f64)> {
|
||||||
let mut result: Vec<(f64, f64)> = Vec::new();
|
let mut result: Vec<(f64, f64)> = Vec::new();
|
||||||
let current_time = current_data.current_instant;
|
let current_time = current_data.current_instant;
|
||||||
|
|
||||||
for (time, data) in ¤t_data.timed_data_vec {
|
for (time, data) in ¤t_data.timed_data_vec {
|
||||||
let time_from_start: f64 = (TIME_STARTS_FROM as f64
|
let time_from_start: f64 = (TIME_STARTS_FROM as f64
|
||||||
- current_time.duration_since(*time).as_millis() as f64)
|
- current_time.duration_since(*time).as_millis() as f64)
|
||||||
.floor();
|
.floor();
|
||||||
|
|
||||||
//Insert joiner points
|
//Insert joiner points
|
||||||
for &(joiner_offset, joiner_val) in &data.mem_data.1 {
|
for &(joiner_offset, joiner_val) in &data.mem_data.1 {
|
||||||
let offset_time = time_from_start - joiner_offset as f64;
|
let offset_time = time_from_start - joiner_offset as f64;
|
||||||
result.push((offset_time, joiner_val));
|
result.push((offset_time, joiner_val));
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push((time_from_start, data.mem_data.0));
|
result.push((time_from_start, data.mem_data.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_swap_data_points(current_data: &data_farmer::DataCollection) -> Vec<(f64, f64)> {
|
pub fn convert_swap_data_points(current_data: &data_farmer::DataCollection) -> Vec<(f64, f64)> {
|
||||||
let mut result: Vec<(f64, f64)> = Vec::new();
|
let mut result: Vec<(f64, f64)> = Vec::new();
|
||||||
let current_time = current_data.current_instant;
|
let current_time = current_data.current_instant;
|
||||||
|
|
||||||
for (time, data) in ¤t_data.timed_data_vec {
|
for (time, data) in ¤t_data.timed_data_vec {
|
||||||
let time_from_start: f64 = (TIME_STARTS_FROM as f64
|
let time_from_start: f64 = (TIME_STARTS_FROM as f64
|
||||||
- current_time.duration_since(*time).as_millis() as f64)
|
- current_time.duration_since(*time).as_millis() as f64)
|
||||||
.floor();
|
.floor();
|
||||||
|
|
||||||
//Insert joiner points
|
//Insert joiner points
|
||||||
for &(joiner_offset, joiner_val) in &data.swap_data.1 {
|
for &(joiner_offset, joiner_val) in &data.swap_data.1 {
|
||||||
let offset_time = time_from_start - joiner_offset as f64;
|
let offset_time = time_from_start - joiner_offset as f64;
|
||||||
result.push((offset_time, joiner_val));
|
result.push((offset_time, joiner_val));
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push((time_from_start, data.swap_data.0));
|
result.push((time_from_start, data.swap_data.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_mem_labels(current_data: &data_farmer::DataCollection) -> (String, String) {
|
pub fn convert_mem_labels(current_data: &data_farmer::DataCollection) -> (String, String) {
|
||||||
let mem_label = if current_data.memory_harvest.mem_total_in_mb == 0 {
|
let mem_label = if current_data.memory_harvest.mem_total_in_mb == 0 {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
} else {
|
} else {
|
||||||
"RAM:".to_string()
|
"RAM:".to_string()
|
||||||
+ &format!(
|
+ &format!(
|
||||||
"{:3.0}%",
|
"{:3.0}%",
|
||||||
(current_data.memory_harvest.mem_used_in_mb as f64 * 100.0
|
(current_data.memory_harvest.mem_used_in_mb as f64 * 100.0
|
||||||
/ current_data.memory_harvest.mem_total_in_mb as f64)
|
/ current_data.memory_harvest.mem_total_in_mb as f64)
|
||||||
.round()
|
.round()
|
||||||
) + &format!(
|
)
|
||||||
" {:.1}GB/{:.1}GB",
|
+ &format!(
|
||||||
current_data.memory_harvest.mem_used_in_mb as f64 / 1024.0,
|
" {:.1}GB/{:.1}GB",
|
||||||
(current_data.memory_harvest.mem_total_in_mb as f64 / 1024.0).round()
|
current_data.memory_harvest.mem_used_in_mb as f64 / 1024.0,
|
||||||
)
|
(current_data.memory_harvest.mem_total_in_mb as f64 / 1024.0).round()
|
||||||
};
|
)
|
||||||
|
};
|
||||||
|
|
||||||
let swap_label = if current_data.swap_harvest.mem_total_in_mb == 0 {
|
let swap_label = if current_data.swap_harvest.mem_total_in_mb == 0 {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
} else {
|
} else {
|
||||||
"SWP:".to_string()
|
"SWP:".to_string()
|
||||||
+ &format!(
|
+ &format!(
|
||||||
"{:3.0}%",
|
"{:3.0}%",
|
||||||
(current_data.swap_harvest.mem_used_in_mb as f64 * 100.0
|
(current_data.swap_harvest.mem_used_in_mb as f64 * 100.0
|
||||||
/ current_data.swap_harvest.mem_total_in_mb as f64)
|
/ current_data.swap_harvest.mem_total_in_mb as f64)
|
||||||
.round()
|
.round()
|
||||||
) + &format!(
|
)
|
||||||
" {:.1}GB/{:.1}GB",
|
+ &format!(
|
||||||
current_data.swap_harvest.mem_used_in_mb as f64 / 1024.0,
|
" {:.1}GB/{:.1}GB",
|
||||||
(current_data.swap_harvest.mem_total_in_mb as f64 / 1024.0).round()
|
current_data.swap_harvest.mem_used_in_mb as f64 / 1024.0,
|
||||||
)
|
(current_data.swap_harvest.mem_total_in_mb as f64 / 1024.0).round()
|
||||||
};
|
)
|
||||||
|
};
|
||||||
|
|
||||||
(mem_label, swap_label)
|
(mem_label, swap_label)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_network_data_points(
|
pub fn convert_network_data_points(
|
||||||
current_data: &data_farmer::DataCollection,
|
current_data: &data_farmer::DataCollection,
|
||||||
) -> ConvertedNetworkData {
|
) -> ConvertedNetworkData {
|
||||||
let mut rx: Vec<(f64, f64)> = Vec::new();
|
let mut rx: Vec<(f64, f64)> = Vec::new();
|
||||||
let mut tx: Vec<(f64, f64)> = Vec::new();
|
let mut tx: Vec<(f64, f64)> = Vec::new();
|
||||||
|
|
||||||
let current_time = current_data.current_instant;
|
let current_time = current_data.current_instant;
|
||||||
for (time, data) in ¤t_data.timed_data_vec {
|
for (time, data) in ¤t_data.timed_data_vec {
|
||||||
let time_from_start: f64 = (TIME_STARTS_FROM as f64
|
let time_from_start: f64 = (TIME_STARTS_FROM as f64
|
||||||
- current_time.duration_since(*time).as_millis() as f64)
|
- current_time.duration_since(*time).as_millis() as f64)
|
||||||
.floor();
|
.floor();
|
||||||
|
|
||||||
//Insert joiner points
|
//Insert joiner points
|
||||||
for &(joiner_offset, joiner_val) in &data.rx_data.1 {
|
for &(joiner_offset, joiner_val) in &data.rx_data.1 {
|
||||||
let offset_time = time_from_start - joiner_offset as f64;
|
let offset_time = time_from_start - joiner_offset as f64;
|
||||||
rx.push((offset_time, joiner_val));
|
rx.push((offset_time, joiner_val));
|
||||||
}
|
}
|
||||||
|
|
||||||
for &(joiner_offset, joiner_val) in &data.tx_data.1 {
|
for &(joiner_offset, joiner_val) in &data.tx_data.1 {
|
||||||
let offset_time = time_from_start - joiner_offset as f64;
|
let offset_time = time_from_start - joiner_offset as f64;
|
||||||
tx.push((offset_time, joiner_val));
|
tx.push((offset_time, joiner_val));
|
||||||
}
|
}
|
||||||
|
|
||||||
rx.push((time_from_start, data.rx_data.0));
|
rx.push((time_from_start, data.rx_data.0));
|
||||||
tx.push((time_from_start, data.tx_data.0));
|
tx.push((time_from_start, data.tx_data.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_rx_converted_result: (f64, String);
|
let total_rx_converted_result: (f64, String);
|
||||||
let rx_converted_result: (f64, String);
|
let rx_converted_result: (f64, String);
|
||||||
let total_tx_converted_result: (f64, String);
|
let total_tx_converted_result: (f64, String);
|
||||||
let tx_converted_result: (f64, String);
|
let tx_converted_result: (f64, String);
|
||||||
|
|
||||||
rx_converted_result = get_exact_byte_values(current_data.network_harvest.rx, false);
|
rx_converted_result = get_exact_byte_values(current_data.network_harvest.rx, false);
|
||||||
total_rx_converted_result = get_exact_byte_values(current_data.network_harvest.total_rx, false);
|
total_rx_converted_result = get_exact_byte_values(current_data.network_harvest.total_rx, false);
|
||||||
let rx_display = format!("{:.*}{}", 1, rx_converted_result.0, rx_converted_result.1);
|
let rx_display = format!("{:.*}{}", 1, rx_converted_result.0, rx_converted_result.1);
|
||||||
let total_rx_display = format!(
|
let total_rx_display = format!(
|
||||||
"{:.*}{}",
|
"{:.*}{}",
|
||||||
1, total_rx_converted_result.0, total_rx_converted_result.1
|
1, total_rx_converted_result.0, total_rx_converted_result.1
|
||||||
);
|
);
|
||||||
|
|
||||||
tx_converted_result = get_exact_byte_values(current_data.network_harvest.tx, false);
|
tx_converted_result = get_exact_byte_values(current_data.network_harvest.tx, false);
|
||||||
total_tx_converted_result = get_exact_byte_values(current_data.network_harvest.total_tx, false);
|
total_tx_converted_result = get_exact_byte_values(current_data.network_harvest.total_tx, false);
|
||||||
let tx_display = format!("{:.*}{}", 1, tx_converted_result.0, tx_converted_result.1);
|
let tx_display = format!("{:.*}{}", 1, tx_converted_result.0, tx_converted_result.1);
|
||||||
let total_tx_display = format!(
|
let total_tx_display = format!(
|
||||||
"{:.*}{}",
|
"{:.*}{}",
|
||||||
1, total_tx_converted_result.0, total_tx_converted_result.1
|
1, total_tx_converted_result.0, total_tx_converted_result.1
|
||||||
);
|
);
|
||||||
|
|
||||||
ConvertedNetworkData {
|
ConvertedNetworkData {
|
||||||
rx,
|
rx,
|
||||||
tx,
|
tx,
|
||||||
rx_display,
|
rx_display,
|
||||||
tx_display,
|
tx_display,
|
||||||
total_rx_display,
|
total_rx_display,
|
||||||
total_tx_display,
|
total_tx_display,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_process_data(
|
pub fn convert_process_data(
|
||||||
current_data: &data_farmer::DataCollection,
|
current_data: &data_farmer::DataCollection,
|
||||||
) -> (HashMap<u32, ProcessHarvest>, Vec<ConvertedProcessData>) {
|
) -> (HashMap<u32, ProcessHarvest>, Vec<ConvertedProcessData>) {
|
||||||
let mut single_list: HashMap<u32, ProcessHarvest> = HashMap::new();
|
let mut single_list: HashMap<u32, ProcessHarvest> = HashMap::new();
|
||||||
|
|
||||||
// cpu, mem, pids
|
// cpu, mem, pids
|
||||||
let mut grouped_hashmap: HashMap<String, (u32, f64, f64, Vec<u32>)> =
|
let mut grouped_hashmap: HashMap<String, (u32, f64, f64, Vec<u32>)> =
|
||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
// Go through every single process in the list... and build a hashmap + single list
|
// Go through every single process in the list... and build a hashmap + single list
|
||||||
for process in &(current_data).process_harvest {
|
for process in &(current_data).process_harvest {
|
||||||
let entry = grouped_hashmap.entry(process.name.clone()).or_insert((
|
let entry = grouped_hashmap.entry(process.name.clone()).or_insert((
|
||||||
process.pid,
|
process.pid,
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
));
|
));
|
||||||
|
|
||||||
(*entry).1 += process.cpu_usage_percent;
|
(*entry).1 += process.cpu_usage_percent;
|
||||||
(*entry).2 += process.mem_usage_percent;
|
(*entry).2 += process.mem_usage_percent;
|
||||||
(*entry).3.push(process.pid);
|
(*entry).3.push(process.pid);
|
||||||
|
|
||||||
single_list.insert(process.pid, process.clone());
|
single_list.insert(process.pid, process.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let grouped_list: Vec<ConvertedProcessData> = grouped_hashmap
|
let grouped_list: Vec<ConvertedProcessData> = grouped_hashmap
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, process_details)| {
|
.map(|(name, process_details)| {
|
||||||
let p = process_details.clone();
|
let p = process_details.clone();
|
||||||
ConvertedProcessData {
|
ConvertedProcessData {
|
||||||
pid: p.0,
|
pid: p.0,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
cpu_usage: p.1,
|
cpu_usage: p.1,
|
||||||
mem_usage: p.2,
|
mem_usage: p.2,
|
||||||
group_pids: p.3,
|
group_pids: p.3,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
(single_list, grouped_list)
|
(single_list, grouped_list)
|
||||||
}
|
}
|
||||||
|
|
1449
src/main.rs
1449
src/main.rs
File diff suppressed because it is too large
Load diff
267
src/options.rs
Normal file
267
src/options.rs
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{data_harvester, App, WidgetPosition},
|
||||||
|
constants::*,
|
||||||
|
utils::error::{self, BottomError},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub flags: Option<ConfigFlags>,
|
||||||
|
pub colors: Option<ConfigColours>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize)]
|
||||||
|
pub struct ConfigFlags {
|
||||||
|
pub avg_cpu: Option<bool>,
|
||||||
|
pub dot_marker: Option<bool>,
|
||||||
|
pub temperature_type: Option<String>,
|
||||||
|
pub rate: Option<u64>,
|
||||||
|
pub left_legend: Option<bool>,
|
||||||
|
pub current_usage: Option<bool>,
|
||||||
|
pub group_processes: Option<bool>,
|
||||||
|
pub case_sensitive: Option<bool>,
|
||||||
|
pub whole_word: Option<bool>,
|
||||||
|
pub regex: Option<bool>,
|
||||||
|
pub default_widget: Option<String>,
|
||||||
|
pub show_disabled_data: Option<bool>,
|
||||||
|
pub basic: Option<bool>,
|
||||||
|
//disabled_cpu_cores: Option<Vec<u64>>, // TODO: [FEATURE] Enable disabling cores in config/flags
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize)]
|
||||||
|
pub struct ConfigColours {
|
||||||
|
pub table_header_color: Option<String>,
|
||||||
|
pub avg_cpu_color: Option<String>,
|
||||||
|
pub cpu_core_colors: Option<Vec<String>>,
|
||||||
|
pub ram_color: Option<String>,
|
||||||
|
pub swap_color: Option<String>,
|
||||||
|
pub rx_color: Option<String>,
|
||||||
|
pub tx_color: Option<String>,
|
||||||
|
pub rx_total_color: Option<String>,
|
||||||
|
pub tx_total_color: Option<String>,
|
||||||
|
pub border_color: Option<String>,
|
||||||
|
pub highlighted_border_color: Option<String>,
|
||||||
|
pub text_color: Option<String>,
|
||||||
|
pub selected_text_color: Option<String>,
|
||||||
|
pub selected_bg_color: Option<String>,
|
||||||
|
pub widget_title_color: Option<String>,
|
||||||
|
pub graph_color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_update_rate_in_milliseconds(
|
||||||
|
update_rate: &Option<&str>, config: &Config,
|
||||||
|
) -> error::Result<u128> {
|
||||||
|
let update_rate_in_milliseconds = if let Some(update_rate) = update_rate {
|
||||||
|
update_rate.parse::<u128>()?
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(rate) = flags.rate {
|
||||||
|
rate as u128
|
||||||
|
} else {
|
||||||
|
DEFAULT_REFRESH_RATE_IN_MILLISECONDS
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DEFAULT_REFRESH_RATE_IN_MILLISECONDS
|
||||||
|
};
|
||||||
|
|
||||||
|
if update_rate_in_milliseconds < 250 {
|
||||||
|
return Err(BottomError::InvalidArg(
|
||||||
|
"Please set your update rate to be greater than 250 milliseconds.".to_string(),
|
||||||
|
));
|
||||||
|
} else if update_rate_in_milliseconds > u128::from(std::u64::MAX) {
|
||||||
|
return Err(BottomError::InvalidArg(
|
||||||
|
"Please set your update rate to be less than unsigned INT_MAX.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(update_rate_in_milliseconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_temperature_option(
|
||||||
|
matches: &clap::ArgMatches<'static>, config: &Config,
|
||||||
|
) -> error::Result<data_harvester::temperature::TemperatureType> {
|
||||||
|
if matches.is_present("FAHRENHEIT") {
|
||||||
|
return Ok(data_harvester::temperature::TemperatureType::Fahrenheit);
|
||||||
|
} else if matches.is_present("KELVIN") {
|
||||||
|
return Ok(data_harvester::temperature::TemperatureType::Kelvin);
|
||||||
|
} else if matches.is_present("CELSIUS") {
|
||||||
|
return Ok(data_harvester::temperature::TemperatureType::Celsius);
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(temp_type) = &flags.temperature_type {
|
||||||
|
// Give lowest priority to config.
|
||||||
|
return match temp_type.as_str() {
|
||||||
|
"fahrenheit" | "f" => {
|
||||||
|
Ok(data_harvester::temperature::TemperatureType::Fahrenheit)
|
||||||
|
}
|
||||||
|
"kelvin" | "k" => {
|
||||||
|
Ok(data_harvester::temperature::TemperatureType::Kelvin)
|
||||||
|
}
|
||||||
|
"celsius" | "c" => {
|
||||||
|
Ok(data_harvester::temperature::TemperatureType::Celsius)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Err(BottomError::ConfigError(
|
||||||
|
"Invalid temperature type. Please have the value be of the form <kelvin|k|celsius|c|fahrenheit|f>".to_string()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(data_harvester::temperature::TemperatureType::Celsius)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_avg_cpu_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||||
|
if matches.is_present("AVG_CPU") {
|
||||||
|
return true;
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(avg_cpu) = flags.avg_cpu {
|
||||||
|
return avg_cpu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_use_dot_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||||
|
if matches.is_present("DOT_MARKER") {
|
||||||
|
return true;
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(dot_marker) = flags.dot_marker {
|
||||||
|
return dot_marker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_use_left_legend_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||||
|
if matches.is_present("LEFT_LEGEND") {
|
||||||
|
return true;
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(left_legend) = flags.left_legend {
|
||||||
|
return left_legend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_use_current_cpu_total_option(
|
||||||
|
matches: &clap::ArgMatches<'static>, config: &Config,
|
||||||
|
) -> bool {
|
||||||
|
if matches.is_present("USE_CURR_USAGE") {
|
||||||
|
return true;
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(current_usage) = flags.current_usage {
|
||||||
|
return current_usage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_show_disabled_data_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||||
|
if matches.is_present("SHOW_DISABLED_DATA") {
|
||||||
|
return true;
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(show_disabled_data) = flags.show_disabled_data {
|
||||||
|
return show_disabled_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_use_basic_mode_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||||
|
if matches.is_present("BASIC_MODE") {
|
||||||
|
return true;
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(basic) = flags.basic {
|
||||||
|
return basic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_app_grouping(matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App) {
|
||||||
|
if matches.is_present("GROUP_PROCESSES") {
|
||||||
|
app.toggle_grouping();
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(grouping) = flags.group_processes {
|
||||||
|
if grouping {
|
||||||
|
app.toggle_grouping();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_app_case_sensitive(
|
||||||
|
matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App,
|
||||||
|
) {
|
||||||
|
if matches.is_present("CASE_SENSITIVE") {
|
||||||
|
app.process_search_state.search_toggle_ignore_case();
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(case_sensitive) = flags.case_sensitive {
|
||||||
|
if case_sensitive {
|
||||||
|
app.process_search_state.search_toggle_ignore_case();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_app_match_whole_word(
|
||||||
|
matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App,
|
||||||
|
) {
|
||||||
|
if matches.is_present("WHOLE_WORD") {
|
||||||
|
app.process_search_state.search_toggle_whole_word();
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(whole_word) = flags.whole_word {
|
||||||
|
if whole_word {
|
||||||
|
app.process_search_state.search_toggle_whole_word();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_app_use_regex(matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App) {
|
||||||
|
if matches.is_present("REGEX_DEFAULT") {
|
||||||
|
app.process_search_state.search_toggle_regex();
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(regex) = flags.regex {
|
||||||
|
if regex {
|
||||||
|
app.process_search_state.search_toggle_regex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_widget(matches: &clap::ArgMatches<'static>, config: &Config) -> WidgetPosition {
|
||||||
|
if matches.is_present("CPU_WIDGET") {
|
||||||
|
return WidgetPosition::Cpu;
|
||||||
|
} else if matches.is_present("MEM_WIDGET") {
|
||||||
|
return WidgetPosition::Mem;
|
||||||
|
} else if matches.is_present("DISK_WIDGET") {
|
||||||
|
return WidgetPosition::Disk;
|
||||||
|
} else if matches.is_present("TEMP_WIDGET") {
|
||||||
|
return WidgetPosition::Temp;
|
||||||
|
} else if matches.is_present("NET_WIDGET") {
|
||||||
|
return WidgetPosition::Network;
|
||||||
|
} else if matches.is_present("PROC_WIDGET") {
|
||||||
|
return WidgetPosition::Process;
|
||||||
|
} else if let Some(flags) = &config.flags {
|
||||||
|
if let Some(default_widget) = &flags.default_widget {
|
||||||
|
return match default_widget.as_str() {
|
||||||
|
"cpu_default" => WidgetPosition::Cpu,
|
||||||
|
"memory_default" => WidgetPosition::Mem,
|
||||||
|
"processes_default" => WidgetPosition::Process,
|
||||||
|
"network_default" => WidgetPosition::Network,
|
||||||
|
"temperature_default" => WidgetPosition::Temp,
|
||||||
|
"disk_default" => WidgetPosition::Disk,
|
||||||
|
_ => WidgetPosition::Process,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetPosition::Process
|
||||||
|
}
|
|
@ -6,84 +6,84 @@ pub type Result<T> = result::Result<T, BottomError>;
|
||||||
/// An error that can occur while Bottom runs.
|
/// An error that can occur while Bottom runs.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum BottomError {
|
pub enum BottomError {
|
||||||
/// An error when there is an IO exception.
|
/// An error when there is an IO exception.
|
||||||
InvalidIO(String),
|
InvalidIO(String),
|
||||||
/// An error when there is an invalid argument passed in.
|
/// An error when there is an invalid argument passed in.
|
||||||
InvalidArg(String),
|
InvalidArg(String),
|
||||||
/// An error when the heim library encounters a problem.
|
/// An error when the heim library encounters a problem.
|
||||||
InvalidHeim(String),
|
InvalidHeim(String),
|
||||||
/// An error when the Crossterm library encounters a problem.
|
/// An error when the Crossterm library encounters a problem.
|
||||||
CrosstermError(String),
|
CrosstermError(String),
|
||||||
/// An error to represent generic errors.
|
/// An error to represent generic errors.
|
||||||
GenericError(String),
|
GenericError(String),
|
||||||
/// An error to represent errors with fern.
|
/// An error to represent errors with fern.
|
||||||
FernError(String),
|
FernError(String),
|
||||||
/// An error to represent errors with the config.
|
/// An error to represent errors with the config.
|
||||||
ConfigError(String),
|
ConfigError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for BottomError {
|
impl std::fmt::Display for BottomError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
BottomError::InvalidIO(ref message) => {
|
BottomError::InvalidIO(ref message) => {
|
||||||
write!(f, "Encountered an IO exception: {}", message)
|
write!(f, "Encountered an IO exception: {}", message)
|
||||||
}
|
}
|
||||||
BottomError::InvalidArg(ref message) => write!(f, "Invalid argument: {}", message),
|
BottomError::InvalidArg(ref message) => write!(f, "Invalid argument: {}", message),
|
||||||
BottomError::InvalidHeim(ref message) => write!(
|
BottomError::InvalidHeim(ref message) => write!(
|
||||||
f,
|
f,
|
||||||
"Invalid error during data collection due to Heim: {}",
|
"Invalid error during data collection due to Heim: {}",
|
||||||
message
|
message
|
||||||
),
|
),
|
||||||
BottomError::CrosstermError(ref message) => {
|
BottomError::CrosstermError(ref message) => {
|
||||||
write!(f, "Invalid error due to Crossterm: {}", message)
|
write!(f, "Invalid error due to Crossterm: {}", message)
|
||||||
}
|
}
|
||||||
BottomError::GenericError(ref message) => write!(f, "{}", message),
|
BottomError::GenericError(ref message) => write!(f, "{}", message),
|
||||||
BottomError::FernError(ref message) => write!(f, "Invalid fern error: {}", message),
|
BottomError::FernError(ref message) => write!(f, "Invalid fern error: {}", message),
|
||||||
BottomError::ConfigError(ref message) => {
|
BottomError::ConfigError(ref message) => {
|
||||||
write!(f, "Invalid config file error: {}", message)
|
write!(f, "Invalid config file error: {}", message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for BottomError {
|
impl From<std::io::Error> for BottomError {
|
||||||
fn from(err: std::io::Error) -> Self {
|
fn from(err: std::io::Error) -> Self {
|
||||||
BottomError::InvalidIO(err.to_string())
|
BottomError::InvalidIO(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<heim::Error> for BottomError {
|
impl From<heim::Error> for BottomError {
|
||||||
fn from(err: heim::Error) -> Self {
|
fn from(err: heim::Error) -> Self {
|
||||||
BottomError::InvalidHeim(err.to_string())
|
BottomError::InvalidHeim(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<crossterm::ErrorKind> for BottomError {
|
impl From<crossterm::ErrorKind> for BottomError {
|
||||||
fn from(err: crossterm::ErrorKind) -> Self {
|
fn from(err: crossterm::ErrorKind) -> Self {
|
||||||
BottomError::CrosstermError(err.to_string())
|
BottomError::CrosstermError(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::num::ParseIntError> for BottomError {
|
impl From<std::num::ParseIntError> for BottomError {
|
||||||
fn from(err: std::num::ParseIntError) -> Self {
|
fn from(err: std::num::ParseIntError) -> Self {
|
||||||
BottomError::InvalidArg(err.to_string())
|
BottomError::InvalidArg(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::string::String> for BottomError {
|
impl From<std::string::String> for BottomError {
|
||||||
fn from(err: std::string::String) -> Self {
|
fn from(err: std::string::String) -> Self {
|
||||||
BottomError::GenericError(err)
|
BottomError::GenericError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<toml::de::Error> for BottomError {
|
impl From<toml::de::Error> for BottomError {
|
||||||
fn from(err: toml::de::Error) -> Self {
|
fn from(err: toml::de::Error) -> Self {
|
||||||
BottomError::ConfigError(err.to_string())
|
BottomError::ConfigError(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<fern::InitError> for BottomError {
|
impl From<fern::InitError> for BottomError {
|
||||||
fn from(err: fern::InitError) -> Self {
|
fn from(err: fern::InitError) -> Self {
|
||||||
BottomError::FernError(err.to_string())
|
BottomError::FernError(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,87 +1,87 @@
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
pub fn float_min(a: f32, b: f32) -> f32 {
|
pub fn float_min(a: f32, b: f32) -> f32 {
|
||||||
match a.partial_cmp(&b) {
|
match a.partial_cmp(&b) {
|
||||||
Some(x) => match x {
|
Some(x) => match x {
|
||||||
Ordering::Greater => b,
|
Ordering::Greater => b,
|
||||||
Ordering::Less => a,
|
Ordering::Less => a,
|
||||||
Ordering::Equal => a,
|
Ordering::Equal => a,
|
||||||
},
|
},
|
||||||
None => a,
|
None => a,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn float_max(a: f32, b: f32) -> f32 {
|
pub fn float_max(a: f32, b: f32) -> f32 {
|
||||||
match a.partial_cmp(&b) {
|
match a.partial_cmp(&b) {
|
||||||
Some(x) => match x {
|
Some(x) => match x {
|
||||||
Ordering::Greater => a,
|
Ordering::Greater => a,
|
||||||
Ordering::Less => b,
|
Ordering::Less => b,
|
||||||
Ordering::Equal => a,
|
Ordering::Equal => a,
|
||||||
},
|
},
|
||||||
None => a,
|
None => a,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a tuple containing the value and the unit. In units of 1024.
|
/// Returns a tuple containing the value and the unit. In units of 1024.
|
||||||
/// This only supports up to a tebibyte.
|
/// This only supports up to a tebibyte.
|
||||||
pub fn get_exact_byte_values(bytes: u64, spacing: bool) -> (f64, String) {
|
pub fn get_exact_byte_values(bytes: u64, spacing: bool) -> (f64, String) {
|
||||||
match bytes {
|
match bytes {
|
||||||
b if b < 1024 => (
|
b if b < 1024 => (
|
||||||
bytes as f64,
|
bytes as f64,
|
||||||
if spacing {
|
if spacing {
|
||||||
" B".to_string()
|
" B".to_string()
|
||||||
} else {
|
} else {
|
||||||
"B".to_string()
|
"B".to_string()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
b if b < 1_048_576 => (bytes as f64 / 1024.0, "KiB".to_string()),
|
b if b < 1_048_576 => (bytes as f64 / 1024.0, "KiB".to_string()),
|
||||||
b if b < 1_073_741_824 => (bytes as f64 / 1_048_576.0, "MiB".to_string()),
|
b if b < 1_073_741_824 => (bytes as f64 / 1_048_576.0, "MiB".to_string()),
|
||||||
b if b < 1_099_511_627_776 => (bytes as f64 / 1_073_741_824.0, "GiB".to_string()),
|
b if b < 1_099_511_627_776 => (bytes as f64 / 1_073_741_824.0, "GiB".to_string()),
|
||||||
_ => (bytes as f64 / 1_099_511_627_776.0, "TiB".to_string()),
|
_ => (bytes as f64 / 1_099_511_627_776.0, "TiB".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a tuple containing the value and the unit. In units of 1000.
|
/// Returns a tuple containing the value and the unit. In units of 1000.
|
||||||
/// This only supports up to a terabyte. Note the "byte" unit will have a space appended to match the others.
|
/// This only supports up to a terabyte. Note the "byte" unit will have a space appended to match the others.
|
||||||
pub fn get_simple_byte_values(bytes: u64, spacing: bool) -> (f64, String) {
|
pub fn get_simple_byte_values(bytes: u64, spacing: bool) -> (f64, String) {
|
||||||
match bytes {
|
match bytes {
|
||||||
b if b < 1000 => (
|
b if b < 1000 => (
|
||||||
bytes as f64,
|
bytes as f64,
|
||||||
if spacing {
|
if spacing {
|
||||||
" B".to_string()
|
" B".to_string()
|
||||||
} else {
|
} else {
|
||||||
"B".to_string()
|
"B".to_string()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
b if b < 1_000_000 => (bytes as f64 / 1000.0, "KB".to_string()),
|
b if b < 1_000_000 => (bytes as f64 / 1000.0, "KB".to_string()),
|
||||||
b if b < 1_000_000_000 => (bytes as f64 / 1_000_000.0, "MB".to_string()),
|
b if b < 1_000_000_000 => (bytes as f64 / 1_000_000.0, "MB".to_string()),
|
||||||
b if b < 1_000_000_000_000 => (bytes as f64 / 1_000_000_000.0, "GB".to_string()),
|
b if b < 1_000_000_000_000 => (bytes as f64 / 1_000_000_000.0, "GB".to_string()),
|
||||||
_ => (bytes as f64 / 1_000_000_000_000.0, "TB".to_string()),
|
_ => (bytes as f64 / 1_000_000_000_000.0, "TB".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gotta get partial ordering? No problem, here's something to deal with it~
|
/// Gotta get partial ordering? No problem, here's something to deal with it~
|
||||||
pub fn get_ordering<T: std::cmp::PartialOrd>(
|
pub fn get_ordering<T: std::cmp::PartialOrd>(
|
||||||
a_val: T, b_val: T, reverse_order: bool,
|
a_val: T, b_val: T, reverse_order: bool,
|
||||||
) -> std::cmp::Ordering {
|
) -> std::cmp::Ordering {
|
||||||
match a_val.partial_cmp(&b_val) {
|
match a_val.partial_cmp(&b_val) {
|
||||||
Some(x) => match x {
|
Some(x) => match x {
|
||||||
Ordering::Greater => {
|
Ordering::Greater => {
|
||||||
if reverse_order {
|
if reverse_order {
|
||||||
std::cmp::Ordering::Less
|
std::cmp::Ordering::Less
|
||||||
} else {
|
} else {
|
||||||
std::cmp::Ordering::Greater
|
std::cmp::Ordering::Greater
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ordering::Less => {
|
Ordering::Less => {
|
||||||
if reverse_order {
|
if reverse_order {
|
||||||
std::cmp::Ordering::Greater
|
std::cmp::Ordering::Greater
|
||||||
} else {
|
} else {
|
||||||
std::cmp::Ordering::Less
|
std::cmp::Ordering::Less
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ordering::Equal => Ordering::Equal,
|
Ordering::Equal => Ordering::Equal,
|
||||||
},
|
},
|
||||||
None => Ordering::Equal,
|
None => Ordering::Equal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
pub fn init_logger() -> Result<(), fern::InitError> {
|
pub fn init_logger() -> Result<(), fern::InitError> {
|
||||||
fern::Dispatch::new()
|
fern::Dispatch::new()
|
||||||
.format(|out, message, record| {
|
.format(|out, message, record| {
|
||||||
out.finish(format_args!(
|
out.finish(format_args!(
|
||||||
"{}[{}][{}] {}",
|
"{}[{}][{}] {}",
|
||||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S:%f]"),
|
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S:%f]"),
|
||||||
record.target(),
|
record.target(),
|
||||||
record.level(),
|
record.level(),
|
||||||
message
|
message
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.level(if cfg!(debug_assertions) {
|
.level(if cfg!(debug_assertions) {
|
||||||
log::LevelFilter::Debug
|
log::LevelFilter::Debug
|
||||||
} else {
|
} else {
|
||||||
log::LevelFilter::Info
|
log::LevelFilter::Info
|
||||||
})
|
})
|
||||||
.chain(fern::log_file("debug.log")?)
|
.chain(fern::log_file("debug.log")?)
|
||||||
.apply()?;
|
.apply()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,69 +5,97 @@ use predicates::prelude::*;
|
||||||
|
|
||||||
// These tests are mostly here just to ensure that invalid results will be caught when passing arguments...
|
// These tests are mostly here just to ensure that invalid results will be caught when passing arguments...
|
||||||
|
|
||||||
// TODO: [TEST] Allow for release testing.
|
// TODO: [TEST] Allow for release testing. Do this with paths.
|
||||||
|
|
||||||
//======================RATES======================//
|
//======================RATES======================//
|
||||||
|
|
||||||
fn get_os_binary_loc() -> String {
|
fn get_os_binary_loc() -> String {
|
||||||
if cfg!(target_os = "linux") {
|
if cfg!(target_os = "linux") {
|
||||||
"./target/x86_64-unknown-linux-gnu/debug/btm".to_string()
|
"./target/x86_64-unknown-linux-gnu/debug/btm".to_string()
|
||||||
} else if cfg!(target_os = "windows") {
|
} else if cfg!(target_os = "windows") {
|
||||||
"./target/x86_64-pc-windows-msvc/debug/btm".to_string()
|
"./target/x86_64-pc-windows-msvc/debug/btm".to_string()
|
||||||
} else if cfg!(target_os = "macos") {
|
} else if cfg!(target_os = "macos") {
|
||||||
"./target/x86_64-apple-darwin/debug/btm".to_string()
|
"./target/x86_64-apple-darwin/debug/btm".to_string()
|
||||||
} else {
|
} else {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_small_rate() -> Result<(), Box<dyn std::error::Error>> {
|
fn test_small_rate() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Command::new(get_os_binary_loc())
|
Command::new(get_os_binary_loc())
|
||||||
.arg("-r")
|
.arg("-r")
|
||||||
.arg("249")
|
.arg("249")
|
||||||
.assert()
|
.assert()
|
||||||
.failure()
|
.failure()
|
||||||
.stderr(predicate::str::contains("rate to be greater than 250"));
|
.stderr(predicate::str::contains("rate to be greater than 250"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_large_rate() -> Result<(), Box<dyn std::error::Error>> {
|
fn test_large_rate() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Command::new(get_os_binary_loc())
|
Command::new(get_os_binary_loc())
|
||||||
.arg("-r")
|
.arg("-r")
|
||||||
.arg("18446744073709551616")
|
.arg("18446744073709551616")
|
||||||
.assert()
|
.assert()
|
||||||
.failure()
|
.failure()
|
||||||
.stderr(predicate::str::contains(
|
.stderr(predicate::str::contains(
|
||||||
"rate to be less than unsigned INT_MAX.",
|
"rate to be less than unsigned INT_MAX.",
|
||||||
));
|
));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_negative_rate() -> Result<(), Box<dyn std::error::Error>> {
|
fn test_negative_rate() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// This test should auto fail due to how clap works
|
// This test should auto fail due to how clap works
|
||||||
Command::new(get_os_binary_loc())
|
Command::new(get_os_binary_loc())
|
||||||
.arg("-r")
|
.arg("-r")
|
||||||
.arg("-1000")
|
.arg("-1000")
|
||||||
.assert()
|
.assert()
|
||||||
.failure()
|
.failure()
|
||||||
.stderr(predicate::str::contains(
|
.stderr(predicate::str::contains(
|
||||||
"wasn't expected, or isn't valid in this context",
|
"wasn't expected, or isn't valid in this context",
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_rate() -> Result<(), Box<dyn std::error::Error>> {
|
fn test_invalid_rate() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Command::new(get_os_binary_loc())
|
Command::new(get_os_binary_loc())
|
||||||
.arg("-r")
|
.arg("-r")
|
||||||
.arg("100-1000")
|
.arg("100-1000")
|
||||||
.assert()
|
.assert()
|
||||||
.failure()
|
.failure()
|
||||||
.stderr(predicate::str::contains("invalid digit"));
|
.stderr(predicate::str::contains("invalid digit"));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_conflicting_temps() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
Command::new(get_os_binary_loc())
|
||||||
|
.arg("-c")
|
||||||
|
.arg("-f")
|
||||||
|
.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains(
|
||||||
|
"cannot be used with one or more of the other specified arguments",
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_conflicting_default_widget() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
Command::new(get_os_binary_loc())
|
||||||
|
.arg("--cpu_default")
|
||||||
|
.arg("--disk_default")
|
||||||
|
.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains(
|
||||||
|
"cannot be used with one or more of the other specified arguments",
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue