mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-04-08 17:05:59 +02:00
refactor: some string-related code cleanup/refactor (#1463)
* other: organize some utility function files * deps: remove kstring * refactor: some naming changes * refactor: some more small refactoring/naming changes * simplify to_cell to return a Cow * enable lints
This commit is contained in:
parent
bcc89170a6
commit
398bf5930f
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -172,7 +172,6 @@ dependencies = [
|
||||
"indexmap",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"kstring",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2",
|
||||
@ -656,15 +655,6 @@ version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "kstring"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747"
|
||||
dependencies = [
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
21
Cargo.toml
21
Cargo.toml
@ -86,7 +86,6 @@ humantime = "2.1.0"
|
||||
indexmap = "2.2.6"
|
||||
indoc = "2.0.5"
|
||||
itertools = "0.12.1"
|
||||
kstring = { version = "2.0.0", features = ["arc"] }
|
||||
log = { version = "0.4.21", optional = true }
|
||||
nvml-wrapper = { version = "0.10.0", optional = true, features = ["legacy-functions"] }
|
||||
regex = "1.10.4"
|
||||
@ -208,17 +207,15 @@ assets = [
|
||||
{ source = "desktop/bottom.desktop", dest = "/usr/share/applications/bottom.desktop", mode = "644" },
|
||||
]
|
||||
|
||||
# Activate whenever we bump the unofficial MSRV to 1.74, I guess?
|
||||
# [lints.rust]
|
||||
# rust_2018_idioms = "deny"
|
||||
[lints.rust]
|
||||
rust_2018_idioms = "deny"
|
||||
# missing_docs = "deny"
|
||||
# unused_extern_crates = "deny"
|
||||
|
||||
# [lints.rustdoc]
|
||||
# broken_intra_doc_links = "deny"
|
||||
# missing_crate_level_docs = "deny"
|
||||
[lints.rustdoc]
|
||||
broken_intra_doc_links = "deny"
|
||||
missing_crate_level_docs = "deny"
|
||||
|
||||
# [lints.clippy]
|
||||
# todo = "deny"
|
||||
# unimplemented = "deny"
|
||||
# missing_safety_doc = "deny"
|
||||
[lints.clippy]
|
||||
todo = "deny"
|
||||
unimplemented = "deny"
|
||||
missing_safety_doc = "deny"
|
||||
|
@ -21,7 +21,7 @@ use hashbrown::HashMap;
|
||||
use crate::data_collection::batteries;
|
||||
use crate::{
|
||||
data_collection::{cpu, disks, memory, network, processes::ProcessHarvest, temperature, Data},
|
||||
utils::{data_prefixes::*, general::get_decimal_bytes},
|
||||
utils::data_prefixes::*,
|
||||
Pid,
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,7 @@ use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete, UnicodeSegmentati
|
||||
use crate::{
|
||||
app::{layout_manager::BottomWidgetType, query::*},
|
||||
constants,
|
||||
utils::general::str_width,
|
||||
utils::strings::str_width,
|
||||
widgets::{
|
||||
BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState,
|
||||
ProcWidgetState, TempWidgetState,
|
||||
|
@ -208,11 +208,10 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
if !app.frozen_state.is_frozen() {
|
||||
// Convert all data into tui-compliant components
|
||||
// Convert all data into data for the displayed widgets.
|
||||
|
||||
// Network
|
||||
if app.used_widgets.use_net {
|
||||
let network_data = convert_network_data_points(
|
||||
let network_data = convert_network_points(
|
||||
&app.data_collection,
|
||||
app.app_config_fields.use_basic_mode
|
||||
|| app.app_config_fields.use_old_network_legend,
|
||||
@ -232,18 +231,16 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Disk
|
||||
if app.used_widgets.use_disk {
|
||||
app.converted_data.ingest_disk_data(&app.data_collection);
|
||||
app.converted_data.convert_disk_data(&app.data_collection);
|
||||
|
||||
for disk in app.states.disk_state.widget_states.values_mut() {
|
||||
disk.force_data_update();
|
||||
}
|
||||
}
|
||||
|
||||
// Temperatures
|
||||
if app.used_widgets.use_temp {
|
||||
app.converted_data.ingest_temp_data(
|
||||
app.converted_data.convert_temp_data(
|
||||
&app.data_collection,
|
||||
app.app_config_fields.temperature_type,
|
||||
);
|
||||
@ -253,22 +250,25 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Memory
|
||||
if app.used_widgets.use_mem {
|
||||
app.converted_data.mem_data =
|
||||
convert_mem_data_points(&app.data_collection);
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
app.converted_data.cache_data =
|
||||
convert_cache_data_points(&app.data_collection);
|
||||
}
|
||||
|
||||
app.converted_data.swap_data =
|
||||
convert_swap_data_points(&app.data_collection);
|
||||
|
||||
#[cfg(feature = "zfs")]
|
||||
{
|
||||
app.converted_data.arc_data =
|
||||
convert_arc_data_points(&app.data_collection);
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu")]
|
||||
{
|
||||
app.converted_data.gpu_data =
|
||||
@ -277,8 +277,10 @@ fn main() -> Result<()> {
|
||||
|
||||
app.converted_data.mem_labels =
|
||||
convert_mem_label(&app.data_collection.memory_harvest);
|
||||
|
||||
app.converted_data.swap_labels =
|
||||
convert_mem_label(&app.data_collection.swap_harvest);
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
app.converted_data.cache_labels =
|
||||
@ -287,26 +289,22 @@ fn main() -> Result<()> {
|
||||
|
||||
#[cfg(feature = "zfs")]
|
||||
{
|
||||
let arc_labels =
|
||||
app.converted_data.arc_labels =
|
||||
convert_mem_label(&app.data_collection.arc_harvest);
|
||||
app.converted_data.arc_labels = arc_labels;
|
||||
}
|
||||
}
|
||||
|
||||
// CPU
|
||||
if app.used_widgets.use_cpu {
|
||||
app.converted_data.ingest_cpu_data(&app.data_collection);
|
||||
app.converted_data.convert_cpu_data(&app.data_collection);
|
||||
app.converted_data.load_avg_data = app.data_collection.load_avg_harvest;
|
||||
}
|
||||
|
||||
// Processes
|
||||
if app.used_widgets.use_proc {
|
||||
for proc in app.states.proc_state.widget_states.values_mut() {
|
||||
proc.force_data_update();
|
||||
}
|
||||
}
|
||||
|
||||
// Battery
|
||||
#[cfg(feature = "battery")]
|
||||
{
|
||||
if app.used_widgets.use_battery {
|
||||
|
@ -152,7 +152,7 @@ impl<DataType: DataToCell<H>, H: ColumnHeader, S: SortType, C: DataTableColumn<H
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::num::NonZeroU16;
|
||||
use std::{borrow::Cow, num::NonZeroU16};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -164,7 +164,7 @@ mod test {
|
||||
impl DataToCell<&'static str> for TestType {
|
||||
fn to_cell(
|
||||
&self, _column: &&'static str, _calculated_width: NonZeroU16,
|
||||
) -> Option<tui::text::Text<'_>> {
|
||||
) -> Option<Cow<'static, str>> {
|
||||
None
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::num::NonZeroU16;
|
||||
use std::{borrow::Cow, num::NonZeroU16};
|
||||
|
||||
use tui::{text::Text, widgets::Row};
|
||||
use tui::widgets::Row;
|
||||
|
||||
use super::{ColumnHeader, DataTableColumn};
|
||||
use crate::canvas::Painter;
|
||||
@ -9,8 +9,9 @@ pub trait DataToCell<H>
|
||||
where
|
||||
H: ColumnHeader,
|
||||
{
|
||||
/// Given data, a column, and its corresponding width, return what should be displayed in the [`DataTable`](super::DataTable).
|
||||
fn to_cell(&self, column: &H, calculated_width: NonZeroU16) -> Option<Text<'_>>;
|
||||
/// Given data, a column, and its corresponding width, return the string in the cell that will
|
||||
/// be displayed in the [`DataTable`](super::DataTable).
|
||||
fn to_cell(&self, column: &H, calculated_width: NonZeroU16) -> Option<Cow<'static, str>>;
|
||||
|
||||
/// Apply styling to the generated [`Row`] of cells.
|
||||
///
|
||||
|
@ -20,6 +20,7 @@ use crate::{
|
||||
app::layout_manager::BottomWidget,
|
||||
canvas::Painter,
|
||||
constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
|
||||
utils::strings::truncate_to_text,
|
||||
};
|
||||
|
||||
pub enum SelectionState {
|
||||
@ -225,7 +226,9 @@ where
|
||||
.iter()
|
||||
.zip(&self.state.calculated_widths)
|
||||
.filter_map(|(column, &width)| {
|
||||
data_row.to_cell(column.inner(), width)
|
||||
data_row
|
||||
.to_cell(column.inner(), width)
|
||||
.map(|content| truncate_to_text(&content, width.get()))
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -8,7 +8,7 @@ use super::{
|
||||
ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps, DataTableState,
|
||||
DataTableStyling, DataToCell,
|
||||
};
|
||||
use crate::utils::general::truncate_to_text;
|
||||
use crate::utils::strings::truncate_to_text;
|
||||
|
||||
/// Denotes the sort order.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@ -99,6 +99,7 @@ impl SortType for Sortable {
|
||||
};
|
||||
// TODO: I think I can get away with removing the truncate_to_text call since
|
||||
// I almost always bind to at least the header size...
|
||||
// TODO: Or should we instead truncate but ALWAYS leave the arrow at the end?
|
||||
truncate_to_text(&concat_string!(c.header(), arrow), width.get())
|
||||
} else {
|
||||
truncate_to_text(&c.header(), width.get())
|
||||
@ -361,7 +362,7 @@ mod test {
|
||||
impl DataToCell<ColumnType> for TestType {
|
||||
fn to_cell(
|
||||
&self, _column: &ColumnType, _calculated_width: NonZeroU16,
|
||||
) -> Option<tui::text::Text<'_>> {
|
||||
) -> Option<Cow<'static, str>> {
|
||||
None
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,8 @@ use tui::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::utils::general::partial_ordering;
|
||||
|
||||
use super::{Context, Dataset, Point, TimeChart};
|
||||
use crate::utils::general::partial_ordering;
|
||||
|
||||
impl TimeChart<'_> {
|
||||
pub(crate) fn draw_points(&self, ctx: &mut Context<'_>) {
|
||||
|
@ -3,13 +3,13 @@
|
||||
|
||||
// TODO: Split this up!
|
||||
|
||||
use kstring::KString;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
app::{data_farmer::DataCollection, AxisScaling},
|
||||
canvas::components::time_chart::Point,
|
||||
data_collection::{cpu::CpuDataType, memory::MemHarvest, temperature::TemperatureType},
|
||||
utils::{data_prefixes::*, data_units::DataUnit, general::*},
|
||||
utils::{data_prefixes::*, data_units::DataUnit},
|
||||
widgets::{DiskWidgetData, TempWidgetData},
|
||||
};
|
||||
|
||||
@ -96,7 +96,7 @@ pub struct ConvertedData {
|
||||
|
||||
impl ConvertedData {
|
||||
// TODO: Can probably heavily reduce this step to avoid clones.
|
||||
pub fn ingest_disk_data(&mut self, data: &DataCollection) {
|
||||
pub fn convert_disk_data(&mut self, data: &DataCollection) {
|
||||
self.disk_data.clear();
|
||||
|
||||
data.disk_harvest
|
||||
@ -110,26 +110,26 @@ impl ConvertedData {
|
||||
};
|
||||
|
||||
self.disk_data.push(DiskWidgetData {
|
||||
name: KString::from_ref(&disk.name),
|
||||
mount_point: KString::from_ref(&disk.mount_point),
|
||||
name: Cow::Owned(disk.name.to_string()),
|
||||
mount_point: Cow::Owned(disk.mount_point.to_string()),
|
||||
free_bytes: disk.free_space,
|
||||
used_bytes: disk.used_space,
|
||||
total_bytes: disk.total_space,
|
||||
summed_total_bytes,
|
||||
io_read: io_read.into(),
|
||||
io_write: io_write.into(),
|
||||
io_read: Cow::Owned(io_read.to_string()),
|
||||
io_write: Cow::Owned(io_write.to_string()),
|
||||
});
|
||||
});
|
||||
|
||||
self.disk_data.shrink_to_fit();
|
||||
}
|
||||
|
||||
pub fn ingest_temp_data(&mut self, data: &DataCollection, temperature_type: TemperatureType) {
|
||||
pub fn convert_temp_data(&mut self, data: &DataCollection, temperature_type: TemperatureType) {
|
||||
self.temp_data.clear();
|
||||
|
||||
data.temp_harvest.iter().for_each(|temp_harvest| {
|
||||
self.temp_data.push(TempWidgetData {
|
||||
sensor: KString::from_ref(&temp_harvest.name),
|
||||
sensor: Cow::Owned(temp_harvest.name.to_string()),
|
||||
temperature_value: temp_harvest.temperature.map(|temp| temp.ceil() as u64),
|
||||
temperature_type,
|
||||
});
|
||||
@ -138,7 +138,7 @@ impl ConvertedData {
|
||||
self.temp_data.shrink_to_fit();
|
||||
}
|
||||
|
||||
pub fn ingest_cpu_data(&mut self, current_data: &DataCollection) {
|
||||
pub fn convert_cpu_data(&mut self, current_data: &DataCollection) {
|
||||
let current_time = current_data.current_instant;
|
||||
|
||||
// (Re-)initialize the vector if the lengths don't match...
|
||||
@ -207,11 +207,11 @@ impl ConvertedData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_mem_data_points(current_data: &DataCollection) -> Vec<Point> {
|
||||
pub fn convert_mem_data_points(data: &DataCollection) -> Vec<Point> {
|
||||
let mut result: Vec<Point> = Vec::new();
|
||||
let current_time = current_data.current_instant;
|
||||
let current_time = data.current_instant;
|
||||
|
||||
for (time, data) in ¤t_data.timed_data_vec {
|
||||
for (time, data) in &data.timed_data_vec {
|
||||
if let Some(mem_data) = data.mem_data {
|
||||
let time_from_start: f64 =
|
||||
(current_time.duration_since(*time).as_millis() as f64).floor();
|
||||
@ -226,11 +226,11 @@ pub fn convert_mem_data_points(current_data: &DataCollection) -> Vec<Point> {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn convert_cache_data_points(current_data: &DataCollection) -> Vec<Point> {
|
||||
pub fn convert_cache_data_points(data: &DataCollection) -> Vec<Point> {
|
||||
let mut result: Vec<Point> = Vec::new();
|
||||
let current_time = current_data.current_instant;
|
||||
let current_time = data.current_instant;
|
||||
|
||||
for (time, data) in ¤t_data.timed_data_vec {
|
||||
for (time, data) in &data.timed_data_vec {
|
||||
if let Some(cache_data) = data.cache_data {
|
||||
let time_from_start: f64 =
|
||||
(current_time.duration_since(*time).as_millis() as f64).floor();
|
||||
@ -244,11 +244,11 @@ pub fn convert_cache_data_points(current_data: &DataCollection) -> Vec<Point> {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn convert_swap_data_points(current_data: &DataCollection) -> Vec<Point> {
|
||||
pub fn convert_swap_data_points(data: &DataCollection) -> Vec<Point> {
|
||||
let mut result: Vec<Point> = Vec::new();
|
||||
let current_time = current_data.current_instant;
|
||||
let current_time = data.current_instant;
|
||||
|
||||
for (time, data) in ¤t_data.timed_data_vec {
|
||||
for (time, data) in &data.timed_data_vec {
|
||||
if let Some(swap_data) = data.swap_data {
|
||||
let time_from_start: f64 =
|
||||
(current_time.duration_since(*time).as_millis() as f64).floor();
|
||||
@ -266,19 +266,14 @@ pub fn convert_swap_data_points(current_data: &DataCollection) -> Vec<Point> {
|
||||
///
|
||||
/// The expected usage is to divide out the given value with the returned denominator in order to be able to use it
|
||||
/// with the returned binary unit (e.g. divide 3000 bytes by 1024 to have a value in KiB).
|
||||
fn get_mem_binary_unit_and_denominator(bytes: u64) -> (&'static str, f64) {
|
||||
if bytes < KIBI_LIMIT {
|
||||
// Stick with bytes if under a kibibyte.
|
||||
("B", 1.0)
|
||||
} else if bytes < MEBI_LIMIT {
|
||||
("KiB", KIBI_LIMIT_F64)
|
||||
} else if bytes < GIBI_LIMIT {
|
||||
("MiB", MEBI_LIMIT_F64)
|
||||
} else if bytes < TEBI_LIMIT {
|
||||
("GiB", GIBI_LIMIT_F64)
|
||||
} else {
|
||||
// Otherwise just use tebibytes, which is probably safe for most use cases.
|
||||
("TiB", TEBI_LIMIT_F64)
|
||||
#[inline]
|
||||
fn get_binary_unit_and_denominator(bytes: u64) -> (&'static str, f64) {
|
||||
match bytes {
|
||||
b if b < KIBI_LIMIT => ("B", 1.0),
|
||||
b if b < MEBI_LIMIT => ("KiB", KIBI_LIMIT_F64),
|
||||
b if b < GIBI_LIMIT => ("MiB", MEBI_LIMIT_F64),
|
||||
b if b < TEBI_LIMIT => ("GiB", GIBI_LIMIT_F64),
|
||||
_ => ("TiB", TEBI_LIMIT_F64),
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,7 +281,7 @@ fn get_mem_binary_unit_and_denominator(bytes: u64) -> (&'static str, f64) {
|
||||
pub fn convert_mem_label(harvest: &MemHarvest) -> Option<(String, String)> {
|
||||
if harvest.total_bytes > 0 {
|
||||
Some((format!("{:3.0}%", harvest.use_percent.unwrap_or(0.0)), {
|
||||
let (unit, denominator) = get_mem_binary_unit_and_denominator(harvest.total_bytes);
|
||||
let (unit, denominator) = get_binary_unit_and_denominator(harvest.total_bytes);
|
||||
|
||||
format!(
|
||||
" {:.1}{}/{:.1}{}",
|
||||
@ -301,7 +296,7 @@ pub fn convert_mem_label(harvest: &MemHarvest) -> Option<(String, String)> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rx_tx_data_points(
|
||||
pub fn get_network_points(
|
||||
data: &DataCollection, scale_type: &AxisScaling, unit_type: &DataUnit, use_binary_prefix: bool,
|
||||
) -> (Vec<Point>, Vec<Point>) {
|
||||
let mut rx: Vec<Point> = Vec::new();
|
||||
@ -347,11 +342,11 @@ pub fn get_rx_tx_data_points(
|
||||
(rx, tx)
|
||||
}
|
||||
|
||||
pub fn convert_network_data_points(
|
||||
pub fn convert_network_points(
|
||||
data: &DataCollection, need_four_points: bool, scale_type: &AxisScaling, unit_type: &DataUnit,
|
||||
use_binary_prefix: bool,
|
||||
) -> ConvertedNetworkData {
|
||||
let (rx, tx) = get_rx_tx_data_points(data, scale_type, unit_type, use_binary_prefix);
|
||||
let (rx, tx) = get_network_points(data, scale_type, unit_type, use_binary_prefix);
|
||||
|
||||
let unit = match unit_type {
|
||||
DataUnit::Byte => "B/s",
|
||||
@ -613,8 +608,7 @@ pub fn convert_gpu_data(current_data: &DataCollection) -> Option<Vec<ConvertedGp
|
||||
points,
|
||||
mem_percent: format!("{:3.0}%", gpu.1.use_percent.unwrap_or(0.0)),
|
||||
mem_total: {
|
||||
let (unit, denominator) =
|
||||
get_mem_binary_unit_and_denominator(gpu.1.total_bytes);
|
||||
let (unit, denominator) = get_binary_unit_and_denominator(gpu.1.total_bytes);
|
||||
|
||||
format!(
|
||||
" {:.1}{unit}/{:.1}{unit}",
|
||||
|
11
src/lib.rs
11
src/lib.rs
@ -21,6 +21,7 @@ pub mod utils {
|
||||
pub mod error;
|
||||
pub mod general;
|
||||
pub mod logging;
|
||||
pub mod strings;
|
||||
}
|
||||
pub mod canvas;
|
||||
pub mod constants;
|
||||
@ -337,14 +338,14 @@ pub fn update_data(app: &mut App) {
|
||||
|
||||
for proc in app.states.proc_state.widget_states.values_mut() {
|
||||
if proc.force_update_data {
|
||||
proc.ingest_data(data_source);
|
||||
proc.set_table_data(data_source);
|
||||
proc.force_update_data = false;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Make this CPU force update less terrible.
|
||||
if app.states.cpu_state.force_update.is_some() {
|
||||
app.converted_data.ingest_cpu_data(data_source);
|
||||
app.converted_data.convert_cpu_data(data_source);
|
||||
app.converted_data.load_avg_data = data_source.load_avg_harvest;
|
||||
|
||||
app.states.cpu_state.force_update = None;
|
||||
@ -361,7 +362,7 @@ pub fn update_data(app: &mut App) {
|
||||
let data = &app.converted_data.temp_data;
|
||||
for temp in app.states.temp_state.widget_states.values_mut() {
|
||||
if temp.force_update_data {
|
||||
temp.ingest_data(data);
|
||||
temp.set_table_data(data);
|
||||
temp.force_update_data = false;
|
||||
}
|
||||
}
|
||||
@ -370,7 +371,7 @@ pub fn update_data(app: &mut App) {
|
||||
let data = &app.converted_data.disk_data;
|
||||
for disk in app.states.disk_state.widget_states.values_mut() {
|
||||
if disk.force_update_data {
|
||||
disk.ingest_data(data);
|
||||
disk.set_table_data(data);
|
||||
disk.force_update_data = false;
|
||||
}
|
||||
}
|
||||
@ -397,7 +398,7 @@ pub fn update_data(app: &mut App) {
|
||||
}
|
||||
|
||||
if app.states.net_state.force_update.is_some() {
|
||||
let (rx, tx) = get_rx_tx_data_points(
|
||||
let (rx, tx) = get_network_points(
|
||||
data_source,
|
||||
&app.app_config_fields.network_scale_type,
|
||||
&app.app_config_fields.network_unit_type,
|
||||
|
@ -15,6 +15,7 @@ use std::{
|
||||
use anyhow::{Context, Result};
|
||||
use clap::ArgMatches;
|
||||
pub use colours::ConfigColours;
|
||||
pub use config::Config;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use indexmap::IndexSet;
|
||||
use regex::Regex;
|
||||
@ -33,7 +34,6 @@ use crate::{
|
||||
},
|
||||
widgets::*,
|
||||
};
|
||||
pub use config::Config;
|
||||
|
||||
macro_rules! is_flag_enabled {
|
||||
($flag_name:ident, $matches:expr, $config:expr) => {
|
||||
|
@ -7,7 +7,6 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use self::ignore_list::IgnoreList;
|
||||
use self::{cpu::CpuConfig, layout::Row, process_columns::ProcessConfig};
|
||||
|
||||
use super::ConfigColours;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
|
@ -37,3 +37,59 @@ pub const LOG_KIBI_LIMIT_U32: u32 = 10;
|
||||
pub const LOG_MEBI_LIMIT_U32: u32 = 20;
|
||||
pub const LOG_GIBI_LIMIT_U32: u32 = 30;
|
||||
pub const LOG_TEBI_LIMIT_U32: u32 = 40;
|
||||
|
||||
/// Returns a tuple containing the value and the unit in bytes. In units of 1024.
|
||||
/// This only supports up to a tebi. Note the "single" unit will have a space appended to match the others if
|
||||
/// `spacing` is true.
|
||||
#[inline]
|
||||
pub fn get_binary_bytes(bytes: u64) -> (f64, &'static str) {
|
||||
match bytes {
|
||||
b if b < KIBI_LIMIT => (bytes as f64, "B"),
|
||||
b if b < MEBI_LIMIT => (bytes as f64 / KIBI_LIMIT_F64, "KiB"),
|
||||
b if b < GIBI_LIMIT => (bytes as f64 / MEBI_LIMIT_F64, "MiB"),
|
||||
b if b < TEBI_LIMIT => (bytes as f64 / GIBI_LIMIT_F64, "GiB"),
|
||||
_ => (bytes as f64 / TEBI_LIMIT_F64, "TiB"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a tuple containing the value and the unit in bytes. In units of 1000.
|
||||
/// This only supports up to a tera. Note the "single" unit will have a space appended to match the others if
|
||||
/// `spacing` is true.
|
||||
#[inline]
|
||||
pub fn get_decimal_bytes(bytes: u64) -> (f64, &'static str) {
|
||||
match bytes {
|
||||
b if b < KILO_LIMIT => (bytes as f64, "B"),
|
||||
b if b < MEGA_LIMIT => (bytes as f64 / KILO_LIMIT_F64, "KB"),
|
||||
b if b < GIGA_LIMIT => (bytes as f64 / MEGA_LIMIT_F64, "MB"),
|
||||
b if b < TERA_LIMIT => (bytes as f64 / GIGA_LIMIT_F64, "GB"),
|
||||
_ => (bytes as f64 / TERA_LIMIT_F64, "TB"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a tuple containing the value and the unit. In units of 1024.
|
||||
/// This only supports up to a tebi. Note the "single" unit will have a space appended to match the others if
|
||||
/// `spacing` is true.
|
||||
#[inline]
|
||||
pub fn get_binary_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
||||
match quantity {
|
||||
b if b < KIBI_LIMIT => (quantity as f64, unit.to_string()),
|
||||
b if b < MEBI_LIMIT => (quantity as f64 / KIBI_LIMIT_F64, format!("Ki{unit}")),
|
||||
b if b < GIBI_LIMIT => (quantity as f64 / MEBI_LIMIT_F64, format!("Mi{unit}")),
|
||||
b if b < TEBI_LIMIT => (quantity as f64 / GIBI_LIMIT_F64, format!("Gi{unit}")),
|
||||
_ => (quantity as f64 / TEBI_LIMIT_F64, format!("Ti{unit}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a tuple containing the value and the unit. In units of 1000.
|
||||
/// This only supports up to a tera. Note the "single" unit will have a space appended to match the others if
|
||||
/// `spacing` is true.
|
||||
#[inline]
|
||||
pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
||||
match quantity {
|
||||
b if b < KILO_LIMIT => (quantity as f64, unit.to_string()),
|
||||
b if b < MEGA_LIMIT => (quantity as f64 / KILO_LIMIT_F64, format!("K{unit}")),
|
||||
b if b < GIGA_LIMIT => (quantity as f64 / MEGA_LIMIT_F64, format!("M{unit}")),
|
||||
b if b < TERA_LIMIT => (quantity as f64 / GIGA_LIMIT_F64, format!("G{unit}")),
|
||||
_ => (quantity as f64 / TERA_LIMIT_F64, format!("T{unit}")),
|
||||
}
|
||||
}
|
||||
|
@ -1,230 +1,4 @@
|
||||
use std::{cmp::Ordering, num::NonZeroUsize};
|
||||
|
||||
use tui::{
|
||||
style::Style,
|
||||
text::{Line, Span, Text},
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::data_prefixes::*;
|
||||
|
||||
/// Returns a tuple containing the value and the unit in bytes. In units of 1024.
|
||||
/// This only supports up to a tebi. Note the "single" unit will have a space appended to match the others if
|
||||
/// `spacing` is true.
|
||||
pub fn get_binary_bytes(bytes: u64) -> (f64, &'static str) {
|
||||
match bytes {
|
||||
b if b < KIBI_LIMIT => (bytes as f64, "B"),
|
||||
b if b < MEBI_LIMIT => (bytes as f64 / 1024.0, "KiB"),
|
||||
b if b < GIBI_LIMIT => (bytes as f64 / 1_048_576.0, "MiB"),
|
||||
b if b < TERA_LIMIT => (bytes as f64 / 1_073_741_824.0, "GiB"),
|
||||
_ => (bytes as f64 / 1_099_511_627_776.0, "TiB"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a tuple containing the value and the unit in bytes. In units of 1000.
|
||||
/// This only supports up to a tera. Note the "single" unit will have a space appended to match the others if
|
||||
/// `spacing` is true.
|
||||
pub fn get_decimal_bytes(bytes: u64) -> (f64, &'static str) {
|
||||
match bytes {
|
||||
b if b < KILO_LIMIT => (bytes as f64, "B"),
|
||||
b if b < MEGA_LIMIT => (bytes as f64 / 1000.0, "KB"),
|
||||
b if b < GIGA_LIMIT => (bytes as f64 / 1_000_000.0, "MB"),
|
||||
b if b < TERA_LIMIT => (bytes as f64 / 1_000_000_000.0, "GB"),
|
||||
_ => (bytes as f64 / 1_000_000_000_000.0, "TB"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a tuple containing the value and the unit. In units of 1024.
|
||||
/// This only supports up to a tebi. Note the "single" unit will have a space appended to match the others if
|
||||
/// `spacing` is true.
|
||||
pub fn get_binary_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
||||
match quantity {
|
||||
b if b < KIBI_LIMIT => (quantity as f64, unit.to_string()),
|
||||
b if b < MEBI_LIMIT => (quantity as f64 / 1024.0, format!("Ki{unit}")),
|
||||
b if b < GIBI_LIMIT => (quantity as f64 / 1_048_576.0, format!("Mi{unit}")),
|
||||
b if b < TERA_LIMIT => (quantity as f64 / 1_073_741_824.0, format!("Gi{unit}")),
|
||||
_ => (quantity as f64 / 1_099_511_627_776.0, format!("Ti{unit}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a tuple containing the value and the unit. In units of 1000.
|
||||
/// This only supports up to a tera. Note the "single" unit will have a space appended to match the others if
|
||||
/// `spacing` is true.
|
||||
pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
||||
match quantity {
|
||||
b if b < KILO_LIMIT => (quantity as f64, unit.to_string()),
|
||||
b if b < MEGA_LIMIT => (quantity as f64 / 1000.0, format!("K{unit}")),
|
||||
b if b < GIGA_LIMIT => (quantity as f64 / 1_000_000.0, format!("M{unit}")),
|
||||
b if b < TERA_LIMIT => (quantity as f64 / 1_000_000_000.0, format!("G{unit}")),
|
||||
_ => (quantity as f64 / 1_000_000_000_000.0, format!("T{unit}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Truncates text if it is too long, and adds an ellipsis at the end if needed.
|
||||
///
|
||||
/// TODO: Maybe cache results from this function for some cases? e.g. columns
|
||||
pub fn truncate_to_text<'a, U: Into<usize>>(content: &str, width: U) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![Line::from(vec![Span::raw(truncate_str(content, width))])],
|
||||
style: Style::default(),
|
||||
alignment: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the width of a str `s`. This takes into account some things like
|
||||
/// joiners when calculating width.
|
||||
pub fn str_width(s: &str) -> usize {
|
||||
UnicodeSegmentation::graphemes(s, true)
|
||||
.map(|g| {
|
||||
if g.contains('\u{200d}') {
|
||||
2
|
||||
} else {
|
||||
UnicodeWidthStr::width(g)
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Returns the "width" of grapheme `g`. This takes into account some things like
|
||||
/// joiners when calculating width.
|
||||
///
|
||||
/// Note that while you *can* pass in an entire string, the point is to check
|
||||
/// individual graphemes (e.g. `"a"`, `"💎"`, `"大"`, `"🇨🇦"`).
|
||||
#[inline]
|
||||
fn grapheme_width(g: &str) -> usize {
|
||||
if g.contains('\u{200d}') {
|
||||
2
|
||||
} else {
|
||||
UnicodeWidthStr::width(g)
|
||||
}
|
||||
}
|
||||
|
||||
enum AsciiIterationResult {
|
||||
Complete,
|
||||
Remaining(usize),
|
||||
}
|
||||
|
||||
/// Greedily add characters to the output until a non-ASCII grapheme is found, or
|
||||
/// the output is `width` long.
|
||||
#[inline]
|
||||
fn greedy_ascii_add(content: &str, width: NonZeroUsize) -> (String, AsciiIterationResult) {
|
||||
let width: usize = width.into();
|
||||
|
||||
const SIZE_OF_ELLIPSIS: usize = 3;
|
||||
let mut text = Vec::with_capacity(width - 1 + SIZE_OF_ELLIPSIS);
|
||||
|
||||
let s = content.as_bytes();
|
||||
|
||||
let mut current_index = 0;
|
||||
|
||||
while current_index < width - 1 {
|
||||
let current_byte = s[current_index];
|
||||
if current_byte.is_ascii() {
|
||||
text.push(current_byte);
|
||||
current_index += 1;
|
||||
} else {
|
||||
debug_assert!(text.is_ascii());
|
||||
|
||||
let current_index = AsciiIterationResult::Remaining(current_index);
|
||||
|
||||
// SAFETY: This conversion is safe to do unchecked, we only push ASCII characters up to
|
||||
// this point.
|
||||
let current_text = unsafe { String::from_utf8_unchecked(text) };
|
||||
|
||||
return (current_text, current_index);
|
||||
}
|
||||
}
|
||||
|
||||
// If we made it all the way through, then we probably hit the width limit.
|
||||
debug_assert!(text.is_ascii());
|
||||
|
||||
let current_index = if s[current_index].is_ascii() {
|
||||
let mut ellipsis = [0; SIZE_OF_ELLIPSIS];
|
||||
'…'.encode_utf8(&mut ellipsis);
|
||||
text.extend_from_slice(&ellipsis);
|
||||
AsciiIterationResult::Complete
|
||||
} else {
|
||||
AsciiIterationResult::Remaining(current_index)
|
||||
};
|
||||
|
||||
// SAFETY: This conversion is safe to do unchecked, we only push ASCII characters up to
|
||||
// this point.
|
||||
let current_text = unsafe { String::from_utf8_unchecked(text) };
|
||||
|
||||
(current_text, current_index)
|
||||
}
|
||||
|
||||
/// Truncates a string to the specified width with an ellipsis character.
|
||||
///
|
||||
/// NB: This probably does not handle EVERY case, but I think it handles most cases
|
||||
/// we will use this function for fine... hopefully.
|
||||
///
|
||||
/// TODO: Maybe fuzz this function?
|
||||
/// TODO: Maybe release this as a lib? Testing against Fish's script [here](https://github.com/ridiculousfish/widecharwidth)
|
||||
/// might be useful.
|
||||
#[inline]
|
||||
fn truncate_str<U: Into<usize>>(content: &str, width: U) -> String {
|
||||
let width = width.into();
|
||||
|
||||
if content.len() <= width {
|
||||
// If the entire string fits in the width, then we just
|
||||
// need to copy the entire string over.
|
||||
|
||||
content.to_owned()
|
||||
} else if let Some(nz_width) = NonZeroUsize::new(width) {
|
||||
// What we are essentially doing is optimizing for the case that
|
||||
// most, if not all of the string is ASCII. As such:
|
||||
// - Step through each byte until (width - 1) is hit or we find a non-ascii
|
||||
// byte.
|
||||
// - If the byte is ascii, then add it.
|
||||
//
|
||||
// If we didn't get a complete truncated string, then continue on treating the rest as graphemes.
|
||||
|
||||
let (mut text, res) = greedy_ascii_add(content, nz_width);
|
||||
match res {
|
||||
AsciiIterationResult::Complete => text,
|
||||
AsciiIterationResult::Remaining(current_index) => {
|
||||
let mut curr_width = text.len();
|
||||
let mut early_break = false;
|
||||
|
||||
// This tracks the length of the last added string - note this does NOT match the grapheme *width*.
|
||||
// Since the previous characters are always ASCII, this is always initialized as 1, unless the string
|
||||
// is empty.
|
||||
let mut last_added_str_len = if text.is_empty() { 0 } else { 1 };
|
||||
|
||||
// Cases to handle:
|
||||
// - Completes adding the entire string.
|
||||
// - Adds a character up to the boundary, then fails.
|
||||
// - Adds a character not up to the boundary, then fails.
|
||||
// Inspired by https://tomdebruijn.com/posts/rust-string-length-width-calculations/
|
||||
for g in UnicodeSegmentation::graphemes(&content[current_index..], true) {
|
||||
let g_width = grapheme_width(g);
|
||||
|
||||
if curr_width + g_width <= width {
|
||||
curr_width += g_width;
|
||||
last_added_str_len = g.len();
|
||||
text.push_str(g);
|
||||
} else {
|
||||
early_break = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if early_break {
|
||||
if curr_width == width {
|
||||
// Remove the last grapheme cluster added.
|
||||
text.truncate(text.len() - last_added_str_len);
|
||||
}
|
||||
text.push('…');
|
||||
}
|
||||
text
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String::default()
|
||||
}
|
||||
}
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[inline]
|
||||
pub const fn sort_partial_fn<T: PartialOrd>(is_descending: bool) -> fn(T, T) -> Ordering {
|
||||
@ -250,29 +24,6 @@ pub fn partial_ordering_desc<T: PartialOrd>(a: T, b: T) -> Ordering {
|
||||
partial_ordering(a, b).reverse()
|
||||
}
|
||||
|
||||
/// Checks that the first string is equal to any of the other ones in a ASCII case-insensitive match.
|
||||
///
|
||||
/// The generated code is the same as writing:
|
||||
/// `to_ascii_lowercase(a) == to_ascii_lowercase(b) || to_ascii_lowercase(a) == to_ascii_lowercase(c)`,
|
||||
/// but without allocating and copying temporaries.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// assert!(multi_eq_ignore_ascii_case!("test", "test"));
|
||||
/// assert!(multi_eq_ignore_ascii_case!("test", "a" | "b" | "test"));
|
||||
/// assert!(!multi_eq_ignore_ascii_case!("test", "a" | "b" | "c"));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! multi_eq_ignore_ascii_case {
|
||||
( $lhs:expr, $last:literal ) => {
|
||||
$lhs.eq_ignore_ascii_case($last)
|
||||
};
|
||||
( $lhs:expr, $head:literal | $($tail:tt)* ) => {
|
||||
$lhs.eq_ignore_ascii_case($head) || multi_eq_ignore_ascii_case!($lhs, $($tail)*)
|
||||
};
|
||||
}
|
||||
|
||||
/// A trait for additional clamping functions on numeric types.
|
||||
pub trait ClampExt {
|
||||
/// Restrict a value by a lower bound. If the current value is _lower_ than `lower_bound`,
|
||||
@ -314,287 +65,6 @@ clamp_num_impl!(u8, u16, u32, u64, usize);
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sort_partial_fn() {
|
||||
let mut x = vec![9, 5, 20, 15, 10, 5];
|
||||
let mut y = vec![1.0, 15.0, -1.0, -100.0, -100.1, 16.15, -100.0];
|
||||
|
||||
x.sort_by(|a, b| sort_partial_fn(false)(a, b));
|
||||
assert_eq!(x, vec![5, 5, 9, 10, 15, 20]);
|
||||
|
||||
x.sort_by(|a, b| sort_partial_fn(true)(a, b));
|
||||
assert_eq!(x, vec![20, 15, 10, 9, 5, 5]);
|
||||
|
||||
y.sort_by(|a, b| sort_partial_fn(false)(a, b));
|
||||
assert_eq!(y, vec![-100.1, -100.0, -100.0, -1.0, 1.0, 15.0, 16.15]);
|
||||
|
||||
y.sort_by(|a, b| sort_partial_fn(true)(a, b));
|
||||
assert_eq!(y, vec![16.15, 15.0, 1.0, -1.0, -100.0, -100.0, -100.1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_str() {
|
||||
let cpu_header = "CPU(c)▲";
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(cpu_header, 8_usize),
|
||||
cpu_header,
|
||||
"should match base string as there is extra room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(cpu_header, 7_usize),
|
||||
cpu_header,
|
||||
"should match base string as there is enough room"
|
||||
);
|
||||
|
||||
assert_eq!(truncate_str(cpu_header, 6_usize), "CPU(c…");
|
||||
assert_eq!(truncate_str(cpu_header, 5_usize), "CPU(…");
|
||||
assert_eq!(truncate_str(cpu_header, 4_usize), "CPU…");
|
||||
assert_eq!(truncate_str(cpu_header, 1_usize), "…");
|
||||
assert_eq!(truncate_str(cpu_header, 0_usize), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_ascii() {
|
||||
let content = "0123456";
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(content, 8_usize),
|
||||
content,
|
||||
"should match base string as there is extra room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(content, 7_usize),
|
||||
content,
|
||||
"should match base string as there is enough room"
|
||||
);
|
||||
|
||||
assert_eq!(truncate_str(content, 6_usize), "01234…");
|
||||
assert_eq!(truncate_str(content, 5_usize), "0123…");
|
||||
assert_eq!(truncate_str(content, 4_usize), "012…");
|
||||
assert_eq!(truncate_str(content, 1_usize), "…");
|
||||
assert_eq!(truncate_str(content, 0_usize), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_cjk() {
|
||||
let cjk = "施氏食獅史";
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(cjk, 11_usize),
|
||||
cjk,
|
||||
"should match base string as there is extra room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(cjk, 10_usize),
|
||||
cjk,
|
||||
"should match base string as there is enough room"
|
||||
);
|
||||
|
||||
assert_eq!(truncate_str(cjk, 9_usize), "施氏食獅…");
|
||||
assert_eq!(truncate_str(cjk, 8_usize), "施氏食…");
|
||||
assert_eq!(truncate_str(cjk, 2_usize), "…");
|
||||
assert_eq!(truncate_str(cjk, 1_usize), "…");
|
||||
assert_eq!(truncate_str(cjk, 0_usize), "");
|
||||
|
||||
let cjk_2 = "你好嗎";
|
||||
assert_eq!(truncate_str(cjk_2, 5_usize), "你好…");
|
||||
assert_eq!(truncate_str(cjk_2, 4_usize), "你…");
|
||||
assert_eq!(truncate_str(cjk_2, 3_usize), "你…");
|
||||
assert_eq!(truncate_str(cjk_2, 2_usize), "…");
|
||||
assert_eq!(truncate_str(cjk_2, 1_usize), "…");
|
||||
assert_eq!(truncate_str(cjk_2, 0_usize), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_mixed_one() {
|
||||
let test = "Test (施氏食獅史) Test";
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(test, 30_usize),
|
||||
test,
|
||||
"should match base string as there is extra room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(test, 22_usize),
|
||||
test,
|
||||
"should match base string as there is just enough room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(test, 21_usize),
|
||||
"Test (施氏食獅史) Te…",
|
||||
"should truncate the t and replace the s with ellipsis"
|
||||
);
|
||||
|
||||
assert_eq!(truncate_str(test, 20_usize), "Test (施氏食獅史) T…");
|
||||
assert_eq!(truncate_str(test, 19_usize), "Test (施氏食獅史) …");
|
||||
assert_eq!(truncate_str(test, 18_usize), "Test (施氏食獅史)…");
|
||||
assert_eq!(truncate_str(test, 17_usize), "Test (施氏食獅史…");
|
||||
assert_eq!(truncate_str(test, 16_usize), "Test (施氏食獅…");
|
||||
assert_eq!(truncate_str(test, 15_usize), "Test (施氏食獅…");
|
||||
assert_eq!(truncate_str(test, 14_usize), "Test (施氏食…");
|
||||
assert_eq!(truncate_str(test, 13_usize), "Test (施氏食…");
|
||||
assert_eq!(truncate_str(test, 8_usize), "Test (…");
|
||||
assert_eq!(truncate_str(test, 7_usize), "Test (…");
|
||||
assert_eq!(truncate_str(test, 6_usize), "Test …");
|
||||
assert_eq!(truncate_str(test, 5_usize), "Test…");
|
||||
assert_eq!(truncate_str(test, 4_usize), "Tes…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_mixed_two() {
|
||||
let test = "Test (施氏abc食abc獅史) Test";
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(test, 30_usize),
|
||||
test,
|
||||
"should match base string as there is extra room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(test, 28_usize),
|
||||
test,
|
||||
"should match base string as there is just enough room"
|
||||
);
|
||||
|
||||
assert_eq!(truncate_str(test, 26_usize), "Test (施氏abc食abc獅史) T…");
|
||||
assert_eq!(truncate_str(test, 21_usize), "Test (施氏abc食abc獅…");
|
||||
assert_eq!(truncate_str(test, 20_usize), "Test (施氏abc食abc…");
|
||||
assert_eq!(truncate_str(test, 16_usize), "Test (施氏abc食…");
|
||||
assert_eq!(truncate_str(test, 15_usize), "Test (施氏abc…");
|
||||
assert_eq!(truncate_str(test, 14_usize), "Test (施氏abc…");
|
||||
assert_eq!(truncate_str(test, 11_usize), "Test (施氏…");
|
||||
assert_eq!(truncate_str(test, 10_usize), "Test (施…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_flags() {
|
||||
let flag = "🇨🇦";
|
||||
assert_eq!(truncate_str(flag, 3_usize), flag);
|
||||
assert_eq!(truncate_str(flag, 2_usize), flag);
|
||||
assert_eq!(truncate_str(flag, 1_usize), "…");
|
||||
assert_eq!(truncate_str(flag, 0_usize), "");
|
||||
|
||||
let flag_text = "oh 🇨🇦";
|
||||
assert_eq!(truncate_str(flag_text, 6_usize), flag_text);
|
||||
assert_eq!(truncate_str(flag_text, 5_usize), flag_text);
|
||||
assert_eq!(truncate_str(flag_text, 4_usize), "oh …");
|
||||
|
||||
let flag_text_wrap = "!🇨🇦!";
|
||||
assert_eq!(truncate_str(flag_text_wrap, 6_usize), flag_text_wrap);
|
||||
assert_eq!(truncate_str(flag_text_wrap, 4_usize), flag_text_wrap);
|
||||
assert_eq!(truncate_str(flag_text_wrap, 3_usize), "!…");
|
||||
assert_eq!(truncate_str(flag_text_wrap, 2_usize), "!…");
|
||||
assert_eq!(truncate_str(flag_text_wrap, 1_usize), "…");
|
||||
|
||||
let flag_cjk = "加拿大🇨🇦";
|
||||
assert_eq!(truncate_str(flag_cjk, 9_usize), flag_cjk);
|
||||
assert_eq!(truncate_str(flag_cjk, 8_usize), flag_cjk);
|
||||
assert_eq!(truncate_str(flag_cjk, 7_usize), "加拿大…");
|
||||
assert_eq!(truncate_str(flag_cjk, 6_usize), "加拿…");
|
||||
assert_eq!(truncate_str(flag_cjk, 5_usize), "加拿…");
|
||||
assert_eq!(truncate_str(flag_cjk, 4_usize), "加…");
|
||||
|
||||
let flag_mix = "🇨🇦加gaa拿naa大daai🇨🇦";
|
||||
assert_eq!(truncate_str(flag_mix, 20_usize), flag_mix);
|
||||
assert_eq!(truncate_str(flag_mix, 19_usize), "🇨🇦加gaa拿naa大daai…");
|
||||
assert_eq!(truncate_str(flag_mix, 18_usize), "🇨🇦加gaa拿naa大daa…");
|
||||
assert_eq!(truncate_str(flag_mix, 17_usize), "🇨🇦加gaa拿naa大da…");
|
||||
assert_eq!(truncate_str(flag_mix, 15_usize), "🇨🇦加gaa拿naa大…");
|
||||
assert_eq!(truncate_str(flag_mix, 14_usize), "🇨🇦加gaa拿naa…");
|
||||
assert_eq!(truncate_str(flag_mix, 13_usize), "🇨🇦加gaa拿naa…");
|
||||
assert_eq!(truncate_str(flag_mix, 3_usize), "🇨🇦…");
|
||||
assert_eq!(truncate_str(flag_mix, 2_usize), "…");
|
||||
assert_eq!(truncate_str(flag_mix, 1_usize), "…");
|
||||
assert_eq!(truncate_str(flag_mix, 0_usize), "");
|
||||
}
|
||||
|
||||
/// This might not be the best way to handle it, but this at least tests that it doesn't crash...
|
||||
#[test]
|
||||
fn test_truncate_hindi() {
|
||||
// cSpell:disable
|
||||
let test = "हिन्दी";
|
||||
assert_eq!(truncate_str(test, 10_usize), test);
|
||||
assert_eq!(truncate_str(test, 6_usize), "हिन्दी");
|
||||
assert_eq!(truncate_str(test, 5_usize), "हिन्दी");
|
||||
assert_eq!(truncate_str(test, 4_usize), "हिन्…");
|
||||
assert_eq!(truncate_str(test, 3_usize), "हि…");
|
||||
assert_eq!(truncate_str(test, 2_usize), "…");
|
||||
assert_eq!(truncate_str(test, 1_usize), "…");
|
||||
assert_eq!(truncate_str(test, 0_usize), "");
|
||||
// cSpell:enable
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_emoji() {
|
||||
let heart_1 = "♥";
|
||||
assert_eq!(truncate_str(heart_1, 2_usize), heart_1);
|
||||
assert_eq!(truncate_str(heart_1, 1_usize), heart_1);
|
||||
assert_eq!(truncate_str(heart_1, 0_usize), "");
|
||||
|
||||
let heart_2 = "❤";
|
||||
assert_eq!(truncate_str(heart_2, 2_usize), heart_2);
|
||||
assert_eq!(truncate_str(heart_2, 1_usize), heart_2);
|
||||
assert_eq!(truncate_str(heart_2, 0_usize), "");
|
||||
|
||||
// This one has a U+FE0F modifier at the end, and is thus considered "emoji-presentation",
|
||||
// see https://github.com/fish-shell/fish-shell/issues/10461#issuecomment-2079624670.
|
||||
// This shouldn't really be a common issue in a terminal but eh.
|
||||
let heart_emoji_pres = "❤️";
|
||||
assert_eq!(truncate_str(heart_emoji_pres, 2_usize), heart_emoji_pres);
|
||||
assert_eq!(truncate_str(heart_emoji_pres, 1_usize), "…");
|
||||
assert_eq!(truncate_str(heart_emoji_pres, 0_usize), "");
|
||||
|
||||
let emote = "💎";
|
||||
assert_eq!(truncate_str(emote, 2_usize), emote);
|
||||
assert_eq!(truncate_str(emote, 1_usize), "…");
|
||||
assert_eq!(truncate_str(emote, 0_usize), "");
|
||||
|
||||
let family = "👨👨👧👦";
|
||||
assert_eq!(truncate_str(family, 2_usize), family);
|
||||
assert_eq!(truncate_str(family, 1_usize), "…");
|
||||
assert_eq!(truncate_str(family, 0_usize), "");
|
||||
|
||||
let scientist = "👩🔬";
|
||||
assert_eq!(truncate_str(scientist, 2_usize), scientist);
|
||||
assert_eq!(truncate_str(scientist, 1_usize), "…");
|
||||
assert_eq!(truncate_str(scientist, 0_usize), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_eq_ignore_ascii_case() {
|
||||
assert!(
|
||||
multi_eq_ignore_ascii_case!("test", "test"),
|
||||
"single comparison should succeed"
|
||||
);
|
||||
assert!(
|
||||
multi_eq_ignore_ascii_case!("test", "a" | "test"),
|
||||
"double comparison should succeed"
|
||||
);
|
||||
assert!(
|
||||
multi_eq_ignore_ascii_case!("test", "a" | "b" | "test"),
|
||||
"multi comparison should succeed"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!multi_eq_ignore_ascii_case!("test", "a"),
|
||||
"single non-matching should fail"
|
||||
);
|
||||
assert!(
|
||||
!multi_eq_ignore_ascii_case!("test", "a" | "b"),
|
||||
"double non-matching should fail"
|
||||
);
|
||||
assert!(
|
||||
!multi_eq_ignore_ascii_case!("test", "a" | "b" | "c"),
|
||||
"multi non-matching should fail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clamp_upper() {
|
||||
let val: usize = 100;
|
||||
|
478
src/utils/strings.rs
Normal file
478
src/utils/strings.rs
Normal file
@ -0,0 +1,478 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use tui::text::Text;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Truncates text if it is too long, and adds an ellipsis at the end if needed.
|
||||
///
|
||||
/// TODO: Maybe cache results from this function for some cases? e.g. columns
|
||||
#[inline]
|
||||
pub fn truncate_to_text<'a, U: Into<usize>>(content: &str, width: U) -> Text<'a> {
|
||||
Text::raw(truncate_str(content, width))
|
||||
}
|
||||
|
||||
/// Returns the width of a str `s`. This takes into account some things like
|
||||
/// joiners when calculating width.
|
||||
#[inline]
|
||||
pub fn str_width(s: &str) -> usize {
|
||||
UnicodeSegmentation::graphemes(s, true)
|
||||
.map(|g| {
|
||||
if g.contains('\u{200d}') {
|
||||
2
|
||||
} else {
|
||||
UnicodeWidthStr::width(g)
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Returns the "width" of grapheme `g`. This takes into account some things like
|
||||
/// joiners when calculating width.
|
||||
///
|
||||
/// Note that while you *can* pass in an entire string, the point is to check
|
||||
/// individual graphemes (e.g. `"a"`, `"💎"`, `"大"`, `"🇨🇦"`).
|
||||
#[inline]
|
||||
fn grapheme_width(g: &str) -> usize {
|
||||
if g.contains('\u{200d}') {
|
||||
2
|
||||
} else {
|
||||
UnicodeWidthStr::width(g)
|
||||
}
|
||||
}
|
||||
|
||||
enum AsciiIterationResult {
|
||||
Complete,
|
||||
Remaining(usize),
|
||||
}
|
||||
|
||||
/// Greedily add characters to the output until a non-ASCII grapheme is found, or
|
||||
/// the output is `width` long.
|
||||
#[inline]
|
||||
fn greedy_ascii_add(content: &str, width: NonZeroUsize) -> (String, AsciiIterationResult) {
|
||||
let width: usize = width.into();
|
||||
|
||||
const SIZE_OF_ELLIPSIS: usize = 3;
|
||||
let mut text = Vec::with_capacity(width - 1 + SIZE_OF_ELLIPSIS);
|
||||
|
||||
let s = content.as_bytes();
|
||||
|
||||
let mut current_index = 0;
|
||||
|
||||
while current_index < width - 1 {
|
||||
let current_byte = s[current_index];
|
||||
if current_byte.is_ascii() {
|
||||
text.push(current_byte);
|
||||
current_index += 1;
|
||||
} else {
|
||||
debug_assert!(text.is_ascii());
|
||||
|
||||
let current_index = AsciiIterationResult::Remaining(current_index);
|
||||
|
||||
// SAFETY: This conversion is safe to do unchecked, we only push ASCII characters up to
|
||||
// this point.
|
||||
let current_text = unsafe { String::from_utf8_unchecked(text) };
|
||||
|
||||
return (current_text, current_index);
|
||||
}
|
||||
}
|
||||
|
||||
// If we made it all the way through, then we probably hit the width limit.
|
||||
debug_assert!(text.is_ascii());
|
||||
|
||||
let current_index = if s[current_index].is_ascii() {
|
||||
let mut ellipsis = [0; SIZE_OF_ELLIPSIS];
|
||||
'…'.encode_utf8(&mut ellipsis);
|
||||
text.extend_from_slice(&ellipsis);
|
||||
AsciiIterationResult::Complete
|
||||
} else {
|
||||
AsciiIterationResult::Remaining(current_index)
|
||||
};
|
||||
|
||||
// SAFETY: This conversion is safe to do unchecked, we only push ASCII characters up to
|
||||
// this point.
|
||||
let current_text = unsafe { String::from_utf8_unchecked(text) };
|
||||
|
||||
(current_text, current_index)
|
||||
}
|
||||
|
||||
/// Truncates a string to the specified width with an ellipsis character.
|
||||
///
|
||||
/// NB: This probably does not handle EVERY case, but I think it handles most cases
|
||||
/// we will use this function for fine... hopefully.
|
||||
///
|
||||
/// TODO: Maybe fuzz this function?
|
||||
/// TODO: Maybe release this as a lib? Testing against Fish's script [here](https://github.com/ridiculousfish/widecharwidth) might be useful.
|
||||
#[inline]
|
||||
fn truncate_str<U: Into<usize>>(content: &str, width: U) -> String {
|
||||
let width = width.into();
|
||||
|
||||
if content.len() <= width {
|
||||
// If the entire string fits in the width, then we just
|
||||
// need to copy the entire string over.
|
||||
|
||||
content.to_owned()
|
||||
} else if let Some(nz_width) = NonZeroUsize::new(width) {
|
||||
// What we are essentially doing is optimizing for the case that
|
||||
// most, if not all of the string is ASCII. As such:
|
||||
// - Step through each byte until (width - 1) is hit or we find a non-ascii
|
||||
// byte.
|
||||
// - If the byte is ascii, then add it.
|
||||
//
|
||||
// If we didn't get a complete truncated string, then continue on treating the rest as graphemes.
|
||||
|
||||
let (mut text, res) = greedy_ascii_add(content, nz_width);
|
||||
match res {
|
||||
AsciiIterationResult::Complete => text,
|
||||
AsciiIterationResult::Remaining(current_index) => {
|
||||
let mut curr_width = text.len();
|
||||
let mut early_break = false;
|
||||
|
||||
// This tracks the length of the last added string - note this does NOT match the grapheme *width*.
|
||||
// Since the previous characters are always ASCII, this is always initialized as 1, unless the string
|
||||
// is empty.
|
||||
let mut last_added_str_len = if text.is_empty() { 0 } else { 1 };
|
||||
|
||||
// Cases to handle:
|
||||
// - Completes adding the entire string.
|
||||
// - Adds a character up to the boundary, then fails.
|
||||
// - Adds a character not up to the boundary, then fails.
|
||||
// Inspired by https://tomdebruijn.com/posts/rust-string-length-width-calculations/
|
||||
for g in UnicodeSegmentation::graphemes(&content[current_index..], true) {
|
||||
let g_width = grapheme_width(g);
|
||||
|
||||
if curr_width + g_width <= width {
|
||||
curr_width += g_width;
|
||||
last_added_str_len = g.len();
|
||||
text.push_str(g);
|
||||
} else {
|
||||
early_break = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if early_break {
|
||||
if curr_width == width {
|
||||
// Remove the last grapheme cluster added.
|
||||
text.truncate(text.len() - last_added_str_len);
|
||||
}
|
||||
text.push('…');
|
||||
}
|
||||
text
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that the first string is equal to any of the other ones in a ASCII case-insensitive match.
|
||||
///
|
||||
/// The generated code is the same as writing:
|
||||
/// `to_ascii_lowercase(a) == to_ascii_lowercase(b) || to_ascii_lowercase(a) == to_ascii_lowercase(c)`,
|
||||
/// but without allocating and copying temporaries.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// assert!(multi_eq_ignore_ascii_case!("test", "test"));
|
||||
/// assert!(multi_eq_ignore_ascii_case!("test", "a" | "b" | "test"));
|
||||
/// assert!(!multi_eq_ignore_ascii_case!("test", "a" | "b" | "c"));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! multi_eq_ignore_ascii_case {
|
||||
( $lhs:expr, $last:literal ) => {
|
||||
$lhs.eq_ignore_ascii_case($last)
|
||||
};
|
||||
( $lhs:expr, $head:literal | $($tail:tt)* ) => {
|
||||
$lhs.eq_ignore_ascii_case($head) || multi_eq_ignore_ascii_case!($lhs, $($tail)*)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::general::sort_partial_fn;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sort_partial_fn() {
|
||||
let mut x = vec![9, 5, 20, 15, 10, 5];
|
||||
let mut y = vec![1.0, 15.0, -1.0, -100.0, -100.1, 16.15, -100.0];
|
||||
|
||||
x.sort_by(|a, b| sort_partial_fn(false)(a, b));
|
||||
assert_eq!(x, vec![5, 5, 9, 10, 15, 20]);
|
||||
|
||||
x.sort_by(|a, b| sort_partial_fn(true)(a, b));
|
||||
assert_eq!(x, vec![20, 15, 10, 9, 5, 5]);
|
||||
|
||||
y.sort_by(|a, b| sort_partial_fn(false)(a, b));
|
||||
assert_eq!(y, vec![-100.1, -100.0, -100.0, -1.0, 1.0, 15.0, 16.15]);
|
||||
|
||||
y.sort_by(|a, b| sort_partial_fn(true)(a, b));
|
||||
assert_eq!(y, vec![16.15, 15.0, 1.0, -1.0, -100.0, -100.0, -100.1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_str() {
|
||||
let cpu_header = "CPU(c)▲";
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(cpu_header, 8_usize),
|
||||
cpu_header,
|
||||
"should match base string as there is extra room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(cpu_header, 7_usize),
|
||||
cpu_header,
|
||||
"should match base string as there is enough room"
|
||||
);
|
||||
|
||||
assert_eq!(truncate_str(cpu_header, 6_usize), "CPU(c…");
|
||||
assert_eq!(truncate_str(cpu_header, 5_usize), "CPU(…");
|
||||
assert_eq!(truncate_str(cpu_header, 4_usize), "CPU…");
|
||||
assert_eq!(truncate_str(cpu_header, 1_usize), "…");
|
||||
assert_eq!(truncate_str(cpu_header, 0_usize), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_ascii() {
|
||||
let content = "0123456";
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(content, 8_usize),
|
||||
content,
|
||||
"should match base string as there is extra room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(content, 7_usize),
|
||||
content,
|
||||
"should match base string as there is enough room"
|
||||
);
|
||||
|
||||
assert_eq!(truncate_str(content, 6_usize), "01234…");
|
||||
assert_eq!(truncate_str(content, 5_usize), "0123…");
|
||||
assert_eq!(truncate_str(content, 4_usize), "012…");
|
||||
assert_eq!(truncate_str(content, 1_usize), "…");
|
||||
assert_eq!(truncate_str(content, 0_usize), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_cjk() {
|
||||
let cjk = "施氏食獅史";
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(cjk, 11_usize),
|
||||
cjk,
|
||||
"should match base string as there is extra room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(cjk, 10_usize),
|
||||
cjk,
|
||||
"should match base string as there is enough room"
|
||||
);
|
||||
|
||||
assert_eq!(truncate_str(cjk, 9_usize), "施氏食獅…");
|
||||
assert_eq!(truncate_str(cjk, 8_usize), "施氏食…");
|
||||
assert_eq!(truncate_str(cjk, 2_usize), "…");
|
||||
assert_eq!(truncate_str(cjk, 1_usize), "…");
|
||||
assert_eq!(truncate_str(cjk, 0_usize), "");
|
||||
|
||||
let cjk_2 = "你好嗎";
|
||||
assert_eq!(truncate_str(cjk_2, 5_usize), "你好…");
|
||||
assert_eq!(truncate_str(cjk_2, 4_usize), "你…");
|
||||
assert_eq!(truncate_str(cjk_2, 3_usize), "你…");
|
||||
assert_eq!(truncate_str(cjk_2, 2_usize), "…");
|
||||
assert_eq!(truncate_str(cjk_2, 1_usize), "…");
|
||||
assert_eq!(truncate_str(cjk_2, 0_usize), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_mixed_one() {
|
||||
let test = "Test (施氏食獅史) Test";
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(test, 30_usize),
|
||||
test,
|
||||
"should match base string as there is extra room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(test, 22_usize),
|
||||
test,
|
||||
"should match base string as there is just enough room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(test, 21_usize),
|
||||
"Test (施氏食獅史) Te…",
|
||||
"should truncate the t and replace the s with ellipsis"
|
||||
);
|
||||
|
||||
assert_eq!(truncate_str(test, 20_usize), "Test (施氏食獅史) T…");
|
||||
assert_eq!(truncate_str(test, 19_usize), "Test (施氏食獅史) …");
|
||||
assert_eq!(truncate_str(test, 18_usize), "Test (施氏食獅史)…");
|
||||
assert_eq!(truncate_str(test, 17_usize), "Test (施氏食獅史…");
|
||||
assert_eq!(truncate_str(test, 16_usize), "Test (施氏食獅…");
|
||||
assert_eq!(truncate_str(test, 15_usize), "Test (施氏食獅…");
|
||||
assert_eq!(truncate_str(test, 14_usize), "Test (施氏食…");
|
||||
assert_eq!(truncate_str(test, 13_usize), "Test (施氏食…");
|
||||
assert_eq!(truncate_str(test, 8_usize), "Test (…");
|
||||
assert_eq!(truncate_str(test, 7_usize), "Test (…");
|
||||
assert_eq!(truncate_str(test, 6_usize), "Test …");
|
||||
assert_eq!(truncate_str(test, 5_usize), "Test…");
|
||||
assert_eq!(truncate_str(test, 4_usize), "Tes…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_mixed_two() {
|
||||
let test = "Test (施氏abc食abc獅史) Test";
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(test, 30_usize),
|
||||
test,
|
||||
"should match base string as there is extra room"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_str(test, 28_usize),
|
||||
test,
|
||||
"should match base string as there is just enough room"
|
||||
);
|
||||
|
||||
assert_eq!(truncate_str(test, 26_usize), "Test (施氏abc食abc獅史) T…");
|
||||
assert_eq!(truncate_str(test, 21_usize), "Test (施氏abc食abc獅…");
|
||||
assert_eq!(truncate_str(test, 20_usize), "Test (施氏abc食abc…");
|
||||
assert_eq!(truncate_str(test, 16_usize), "Test (施氏abc食…");
|
||||
assert_eq!(truncate_str(test, 15_usize), "Test (施氏abc…");
|
||||
assert_eq!(truncate_str(test, 14_usize), "Test (施氏abc…");
|
||||
assert_eq!(truncate_str(test, 11_usize), "Test (施氏…");
|
||||
assert_eq!(truncate_str(test, 10_usize), "Test (施…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_flags() {
|
||||
let flag = "🇨🇦";
|
||||
assert_eq!(truncate_str(flag, 3_usize), flag);
|
||||
assert_eq!(truncate_str(flag, 2_usize), flag);
|
||||
assert_eq!(truncate_str(flag, 1_usize), "…");
|
||||
assert_eq!(truncate_str(flag, 0_usize), "");
|
||||
|
||||
let flag_text = "oh 🇨🇦";
|
||||
assert_eq!(truncate_str(flag_text, 6_usize), flag_text);
|
||||
assert_eq!(truncate_str(flag_text, 5_usize), flag_text);
|
||||
assert_eq!(truncate_str(flag_text, 4_usize), "oh …");
|
||||
|
||||
let flag_text_wrap = "!🇨🇦!";
|
||||
assert_eq!(truncate_str(flag_text_wrap, 6_usize), flag_text_wrap);
|
||||
assert_eq!(truncate_str(flag_text_wrap, 4_usize), flag_text_wrap);
|
||||
assert_eq!(truncate_str(flag_text_wrap, 3_usize), "!…");
|
||||
assert_eq!(truncate_str(flag_text_wrap, 2_usize), "!…");
|
||||
assert_eq!(truncate_str(flag_text_wrap, 1_usize), "…");
|
||||
|
||||
let flag_cjk = "加拿大🇨🇦";
|
||||
assert_eq!(truncate_str(flag_cjk, 9_usize), flag_cjk);
|
||||
assert_eq!(truncate_str(flag_cjk, 8_usize), flag_cjk);
|
||||
assert_eq!(truncate_str(flag_cjk, 7_usize), "加拿大…");
|
||||
assert_eq!(truncate_str(flag_cjk, 6_usize), "加拿…");
|
||||
assert_eq!(truncate_str(flag_cjk, 5_usize), "加拿…");
|
||||
assert_eq!(truncate_str(flag_cjk, 4_usize), "加…");
|
||||
|
||||
let flag_mix = "🇨🇦加gaa拿naa大daai🇨🇦";
|
||||
assert_eq!(truncate_str(flag_mix, 20_usize), flag_mix);
|
||||
assert_eq!(truncate_str(flag_mix, 19_usize), "🇨🇦加gaa拿naa大daai…");
|
||||
assert_eq!(truncate_str(flag_mix, 18_usize), "🇨🇦加gaa拿naa大daa…");
|
||||
assert_eq!(truncate_str(flag_mix, 17_usize), "🇨🇦加gaa拿naa大da…");
|
||||
assert_eq!(truncate_str(flag_mix, 15_usize), "🇨🇦加gaa拿naa大…");
|
||||
assert_eq!(truncate_str(flag_mix, 14_usize), "🇨🇦加gaa拿naa…");
|
||||
assert_eq!(truncate_str(flag_mix, 13_usize), "🇨🇦加gaa拿naa…");
|
||||
assert_eq!(truncate_str(flag_mix, 3_usize), "🇨🇦…");
|
||||
assert_eq!(truncate_str(flag_mix, 2_usize), "…");
|
||||
assert_eq!(truncate_str(flag_mix, 1_usize), "…");
|
||||
assert_eq!(truncate_str(flag_mix, 0_usize), "");
|
||||
}
|
||||
|
||||
/// This might not be the best way to handle it, but this at least tests that it doesn't crash...
|
||||
#[test]
|
||||
fn test_truncate_hindi() {
|
||||
// cSpell:disable
|
||||
let test = "हिन्दी";
|
||||
assert_eq!(truncate_str(test, 10_usize), test);
|
||||
assert_eq!(truncate_str(test, 6_usize), "हिन्दी");
|
||||
assert_eq!(truncate_str(test, 5_usize), "हिन्दी");
|
||||
assert_eq!(truncate_str(test, 4_usize), "हिन्…");
|
||||
assert_eq!(truncate_str(test, 3_usize), "हि…");
|
||||
assert_eq!(truncate_str(test, 2_usize), "…");
|
||||
assert_eq!(truncate_str(test, 1_usize), "…");
|
||||
assert_eq!(truncate_str(test, 0_usize), "");
|
||||
// cSpell:enable
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_emoji() {
|
||||
let heart_1 = "♥";
|
||||
assert_eq!(truncate_str(heart_1, 2_usize), heart_1);
|
||||
assert_eq!(truncate_str(heart_1, 1_usize), heart_1);
|
||||
assert_eq!(truncate_str(heart_1, 0_usize), "");
|
||||
|
||||
let heart_2 = "❤";
|
||||
assert_eq!(truncate_str(heart_2, 2_usize), heart_2);
|
||||
assert_eq!(truncate_str(heart_2, 1_usize), heart_2);
|
||||
assert_eq!(truncate_str(heart_2, 0_usize), "");
|
||||
|
||||
// This one has a U+FE0F modifier at the end, and is thus considered "emoji-presentation",
|
||||
// see https://github.com/fish-shell/fish-shell/issues/10461#issuecomment-2079624670.
|
||||
// This shouldn't really be a common issue in a terminal but eh.
|
||||
let heart_emoji_pres = "❤️";
|
||||
assert_eq!(truncate_str(heart_emoji_pres, 2_usize), heart_emoji_pres);
|
||||
assert_eq!(truncate_str(heart_emoji_pres, 1_usize), "…");
|
||||
assert_eq!(truncate_str(heart_emoji_pres, 0_usize), "");
|
||||
|
||||
let emote = "💎";
|
||||
assert_eq!(truncate_str(emote, 2_usize), emote);
|
||||
assert_eq!(truncate_str(emote, 1_usize), "…");
|
||||
assert_eq!(truncate_str(emote, 0_usize), "");
|
||||
|
||||
let family = "👨👨👧👦";
|
||||
assert_eq!(truncate_str(family, 2_usize), family);
|
||||
assert_eq!(truncate_str(family, 1_usize), "…");
|
||||
assert_eq!(truncate_str(family, 0_usize), "");
|
||||
|
||||
let scientist = "👩🔬";
|
||||
assert_eq!(truncate_str(scientist, 2_usize), scientist);
|
||||
assert_eq!(truncate_str(scientist, 1_usize), "…");
|
||||
assert_eq!(truncate_str(scientist, 0_usize), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_eq_ignore_ascii_case() {
|
||||
assert!(
|
||||
multi_eq_ignore_ascii_case!("test", "test"),
|
||||
"single comparison should succeed"
|
||||
);
|
||||
assert!(
|
||||
multi_eq_ignore_ascii_case!("test", "a" | "test"),
|
||||
"double comparison should succeed"
|
||||
);
|
||||
assert!(
|
||||
multi_eq_ignore_ascii_case!("test", "a" | "b" | "test"),
|
||||
"multi comparison should succeed"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!multi_eq_ignore_ascii_case!("test", "a"),
|
||||
"single non-matching should fail"
|
||||
);
|
||||
assert!(
|
||||
!multi_eq_ignore_ascii_case!("test", "a" | "b"),
|
||||
"double non-matching should fail"
|
||||
);
|
||||
assert!(
|
||||
!multi_eq_ignore_ascii_case!("test", "a" | "b" | "c"),
|
||||
"multi non-matching should fail"
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use std::{borrow::Cow, num::NonZeroU16, time::Instant};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use tui::{style::Style, text::Text, widgets::Row};
|
||||
use tui::{style::Style, widgets::Row};
|
||||
|
||||
use crate::{
|
||||
app::AppConfigFields,
|
||||
@ -16,7 +16,6 @@ use crate::{
|
||||
data_collection::cpu::CpuDataType,
|
||||
data_conversion::CpuWidgetData,
|
||||
options::config::cpu::CpuDefault,
|
||||
utils::general::truncate_to_text,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
@ -81,7 +80,9 @@ impl CpuWidgetTableData {
|
||||
}
|
||||
|
||||
impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
|
||||
fn to_cell(&self, column: &CpuWidgetColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
|
||||
fn to_cell(
|
||||
&self, column: &CpuWidgetColumn, calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
const CPU_TRUNCATE_BREAKPOINT: u16 = 5;
|
||||
|
||||
let calculated_width = calculated_width.get();
|
||||
@ -107,25 +108,19 @@ impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
|
||||
} else {
|
||||
match column {
|
||||
CpuWidgetColumn::CPU => match data_type {
|
||||
CpuDataType::Avg => Some(truncate_to_text("AVG", calculated_width)),
|
||||
CpuDataType::Avg => Some("AVG".into()),
|
||||
CpuDataType::Cpu(index) => {
|
||||
let index_str = index.to_string();
|
||||
let text = if calculated_width < CPU_TRUNCATE_BREAKPOINT {
|
||||
truncate_to_text(&index_str, calculated_width)
|
||||
index_str.into()
|
||||
} else {
|
||||
truncate_to_text(
|
||||
&concat_string!("CPU", index_str),
|
||||
calculated_width,
|
||||
)
|
||||
concat_string!("CPU", index_str).into()
|
||||
};
|
||||
|
||||
Some(text)
|
||||
}
|
||||
},
|
||||
CpuWidgetColumn::Use => Some(truncate_to_text(
|
||||
&format!("{:.0}%", last_entry.round()),
|
||||
calculated_width,
|
||||
)),
|
||||
CpuWidgetColumn::Use => Some(format!("{:.0}%", last_entry.round()).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,5 @@
|
||||
use std::{borrow::Cow, cmp::max, num::NonZeroU16};
|
||||
|
||||
use kstring::KString;
|
||||
use tui::text::Text;
|
||||
|
||||
use crate::{
|
||||
app::AppConfigFields,
|
||||
canvas::{
|
||||
@ -12,23 +9,23 @@ use crate::{
|
||||
},
|
||||
styling::CanvasStyling,
|
||||
},
|
||||
utils::general::{get_decimal_bytes, sort_partial_fn, truncate_to_text},
|
||||
utils::{data_prefixes::get_decimal_bytes, general::sort_partial_fn},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DiskWidgetData {
|
||||
pub name: KString,
|
||||
pub mount_point: KString,
|
||||
pub name: Cow<'static, str>,
|
||||
pub mount_point: Cow<'static, str>,
|
||||
pub free_bytes: Option<u64>,
|
||||
pub used_bytes: Option<u64>,
|
||||
pub total_bytes: Option<u64>,
|
||||
pub summed_total_bytes: Option<u64>,
|
||||
pub io_read: KString,
|
||||
pub io_write: KString,
|
||||
pub io_read: Cow<'static, str>,
|
||||
pub io_write: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl DiskWidgetData {
|
||||
pub fn total_space(&self) -> KString {
|
||||
fn total_space(&self) -> Cow<'static, str> {
|
||||
if let Some(total_bytes) = self.total_bytes {
|
||||
let converted_total_space = get_decimal_bytes(total_bytes);
|
||||
format!(
|
||||
@ -41,7 +38,7 @@ impl DiskWidgetData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn free_space(&self) -> KString {
|
||||
fn free_space(&self) -> Cow<'static, str> {
|
||||
if let Some(free_bytes) = self.free_bytes {
|
||||
let converted_free_space = get_decimal_bytes(free_bytes);
|
||||
format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1).into()
|
||||
@ -50,7 +47,7 @@ impl DiskWidgetData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn used_space(&self) -> KString {
|
||||
fn used_space(&self) -> Cow<'static, str> {
|
||||
if let Some(used_bytes) = self.used_bytes {
|
||||
let converted_free_space = get_decimal_bytes(used_bytes);
|
||||
format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1).into()
|
||||
@ -59,7 +56,7 @@ impl DiskWidgetData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn free_percent(&self) -> Option<f64> {
|
||||
fn free_percent(&self) -> Option<f64> {
|
||||
if let (Some(free_bytes), Some(summed_total_bytes)) =
|
||||
(self.free_bytes, self.summed_total_bytes)
|
||||
{
|
||||
@ -69,14 +66,14 @@ impl DiskWidgetData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn free_percent_string(&self) -> KString {
|
||||
fn free_percent_string(&self) -> Cow<'static, str> {
|
||||
match self.free_percent() {
|
||||
Some(val) => format!("{val:.1}%").into(),
|
||||
None => "N/A".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn used_percent(&self) -> Option<f64> {
|
||||
fn used_percent(&self) -> Option<f64> {
|
||||
if let (Some(used_bytes), Some(summed_total_bytes)) =
|
||||
(self.used_bytes, self.summed_total_bytes)
|
||||
{
|
||||
@ -90,7 +87,7 @@ impl DiskWidgetData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn used_percent_string(&self) -> KString {
|
||||
fn used_percent_string(&self) -> Cow<'static, str> {
|
||||
match self.used_percent() {
|
||||
Some(val) => format!("{val:.1}%").into(),
|
||||
None => "N/A".into(),
|
||||
@ -128,22 +125,19 @@ impl ColumnHeader for DiskWidgetColumn {
|
||||
}
|
||||
|
||||
impl DataToCell<DiskWidgetColumn> for DiskWidgetData {
|
||||
fn to_cell(&self, column: &DiskWidgetColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
|
||||
let calculated_width = calculated_width.get();
|
||||
fn to_cell(
|
||||
&self, column: &DiskWidgetColumn, _calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
let text = match column {
|
||||
DiskWidgetColumn::Disk => truncate_to_text(&self.name, calculated_width),
|
||||
DiskWidgetColumn::Mount => truncate_to_text(&self.mount_point, calculated_width),
|
||||
DiskWidgetColumn::Used => truncate_to_text(&self.used_space(), calculated_width),
|
||||
DiskWidgetColumn::Free => truncate_to_text(&self.free_space(), calculated_width),
|
||||
DiskWidgetColumn::UsedPercent => {
|
||||
truncate_to_text(&self.used_percent_string(), calculated_width)
|
||||
}
|
||||
DiskWidgetColumn::FreePercent => {
|
||||
truncate_to_text(&self.free_percent_string(), calculated_width)
|
||||
}
|
||||
DiskWidgetColumn::Total => truncate_to_text(&self.total_space(), calculated_width),
|
||||
DiskWidgetColumn::IoRead => truncate_to_text(&self.io_read, calculated_width),
|
||||
DiskWidgetColumn::IoWrite => truncate_to_text(&self.io_write, calculated_width),
|
||||
DiskWidgetColumn::Disk => self.name.clone(),
|
||||
DiskWidgetColumn::Mount => self.mount_point.clone(),
|
||||
DiskWidgetColumn::Used => self.used_space(),
|
||||
DiskWidgetColumn::Free => self.free_space(),
|
||||
DiskWidgetColumn::UsedPercent => self.used_percent_string(),
|
||||
DiskWidgetColumn::FreePercent => self.free_percent_string(),
|
||||
DiskWidgetColumn::Total => self.total_space(),
|
||||
DiskWidgetColumn::IoRead => self.io_read.clone(),
|
||||
DiskWidgetColumn::IoWrite => self.io_write.clone(),
|
||||
};
|
||||
|
||||
Some(text)
|
||||
@ -251,7 +245,8 @@ impl DiskTableWidget {
|
||||
self.force_update_data = true;
|
||||
}
|
||||
|
||||
pub fn ingest_data(&mut self, data: &[DiskWidgetData]) {
|
||||
/// Update the current table data.
|
||||
pub fn set_table_data(&mut self, data: &[DiskWidgetData]) {
|
||||
let mut data = data.to_vec();
|
||||
if let Some(column) = self.table.columns.get(self.table.sort_index()) {
|
||||
column.sort_by(&mut data, self.table.order());
|
||||
|
@ -421,9 +421,11 @@ impl ProcWidgetState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the current table data.
|
||||
///
|
||||
/// This function *only* updates the displayed process data. If there is a need to update the actual *stored* data,
|
||||
/// call it before this function.
|
||||
pub fn ingest_data(&mut self, data_collection: &DataCollection) {
|
||||
pub fn set_table_data(&mut self, data_collection: &DataCollection) {
|
||||
let data = match &self.mode {
|
||||
ProcWidgetMode::Grouped | ProcWidgetMode::Normal => {
|
||||
self.get_normal_data(&data_collection.process_data.process_harvest)
|
||||
|
@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{max, Ordering},
|
||||
fmt::Display,
|
||||
num::NonZeroU16,
|
||||
@ -6,7 +7,7 @@ use std::{
|
||||
};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use tui::{text::Text, widgets::Row};
|
||||
use tui::widgets::Row;
|
||||
|
||||
use super::proc_widget_column::ProcColumn;
|
||||
use crate::{
|
||||
@ -16,7 +17,6 @@ use crate::{
|
||||
},
|
||||
data_collection::processes::ProcessHarvest,
|
||||
data_conversion::{binary_byte_string, dec_bytes_per_second_string, dec_bytes_string},
|
||||
utils::general::truncate_to_text,
|
||||
Pid,
|
||||
};
|
||||
|
||||
@ -301,40 +301,37 @@ impl ProcWidgetData {
|
||||
}
|
||||
|
||||
impl DataToCell<ProcColumn> for ProcWidgetData {
|
||||
fn to_cell(&self, column: &ProcColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
|
||||
fn to_cell(
|
||||
&self, column: &ProcColumn, calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
let calculated_width = calculated_width.get();
|
||||
|
||||
// TODO: Optimize the string allocations here...
|
||||
// TODO: Also maybe just pull in the to_string call but add a variable for the differences.
|
||||
Some(truncate_to_text(
|
||||
&match column {
|
||||
ProcColumn::CpuPercent => {
|
||||
format!("{:.1}%", self.cpu_usage_percent)
|
||||
Some(match column {
|
||||
ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent).into(),
|
||||
ProcColumn::MemoryVal | ProcColumn::MemoryPercent => self.mem_usage.to_string().into(),
|
||||
ProcColumn::Pid => self.pid.to_string().into(),
|
||||
ProcColumn::Count => self.num_similar.to_string().into(),
|
||||
ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string().into(),
|
||||
ProcColumn::ReadPerSecond => dec_bytes_per_second_string(self.rps).into(),
|
||||
ProcColumn::WritePerSecond => dec_bytes_per_second_string(self.wps).into(),
|
||||
ProcColumn::TotalRead => dec_bytes_string(self.total_read).into(),
|
||||
ProcColumn::TotalWrite => dec_bytes_string(self.total_write).into(),
|
||||
ProcColumn::State => {
|
||||
if calculated_width < 8 {
|
||||
self.process_char.to_string().into()
|
||||
} else {
|
||||
self.process_state.clone().into()
|
||||
}
|
||||
ProcColumn::MemoryVal | ProcColumn::MemoryPercent => self.mem_usage.to_string(),
|
||||
ProcColumn::Pid => self.pid.to_string(),
|
||||
ProcColumn::Count => self.num_similar.to_string(),
|
||||
ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string(),
|
||||
ProcColumn::ReadPerSecond => dec_bytes_per_second_string(self.rps),
|
||||
ProcColumn::WritePerSecond => dec_bytes_per_second_string(self.wps),
|
||||
ProcColumn::TotalRead => dec_bytes_string(self.total_read),
|
||||
ProcColumn::TotalWrite => dec_bytes_string(self.total_write),
|
||||
ProcColumn::State => {
|
||||
if calculated_width < 8 {
|
||||
self.process_char.to_string()
|
||||
} else {
|
||||
self.process_state.clone()
|
||||
}
|
||||
}
|
||||
ProcColumn::User => self.user.clone(),
|
||||
ProcColumn::Time => format_time(self.time),
|
||||
#[cfg(feature = "gpu")]
|
||||
ProcColumn::GpuMem | ProcColumn::GpuMemPercent => self.gpu_mem_usage.to_string(),
|
||||
#[cfg(feature = "gpu")]
|
||||
ProcColumn::GpuUtilPercent => format!("{:.1}%", self.gpu_usage),
|
||||
},
|
||||
calculated_width,
|
||||
))
|
||||
}
|
||||
ProcColumn::User => self.user.clone().into(),
|
||||
ProcColumn::Time => format_time(self.time).into(),
|
||||
#[cfg(feature = "gpu")]
|
||||
ProcColumn::GpuMem | ProcColumn::GpuMemPercent => self.gpu_mem_usage.to_string().into(),
|
||||
#[cfg(feature = "gpu")]
|
||||
ProcColumn::GpuUtilPercent => format!("{:.1}%", self.gpu_usage).into(),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
@ -1,11 +1,6 @@
|
||||
use std::{borrow::Cow, num::NonZeroU16};
|
||||
|
||||
use tui::text::Text;
|
||||
|
||||
use crate::{
|
||||
canvas::components::data_table::{ColumnHeader, DataTableColumn, DataToCell},
|
||||
utils::general::truncate_to_text,
|
||||
};
|
||||
use crate::canvas::components::data_table::{ColumnHeader, DataTableColumn, DataToCell};
|
||||
|
||||
pub struct SortTableColumn;
|
||||
|
||||
@ -16,8 +11,10 @@ impl ColumnHeader for SortTableColumn {
|
||||
}
|
||||
|
||||
impl DataToCell<SortTableColumn> for &'static str {
|
||||
fn to_cell(&self, _column: &SortTableColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
|
||||
Some(truncate_to_text(self, calculated_width.get()))
|
||||
fn to_cell(
|
||||
&self, _column: &SortTableColumn, _calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
Some(Cow::Borrowed(self))
|
||||
}
|
||||
|
||||
fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>
|
||||
@ -29,8 +26,10 @@ impl DataToCell<SortTableColumn> for &'static str {
|
||||
}
|
||||
|
||||
impl DataToCell<SortTableColumn> for Cow<'static, str> {
|
||||
fn to_cell(&self, _column: &SortTableColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
|
||||
Some(truncate_to_text(self, calculated_width.get()))
|
||||
fn to_cell(
|
||||
&self, _column: &SortTableColumn, _calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
Some(self.clone())
|
||||
}
|
||||
|
||||
fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>
|
||||
|
@ -1,8 +1,6 @@
|
||||
use std::{borrow::Cow, cmp::max, num::NonZeroU16};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use kstring::KString;
|
||||
use tui::text::Text;
|
||||
|
||||
use crate::{
|
||||
app::AppConfigFields,
|
||||
@ -14,12 +12,12 @@ use crate::{
|
||||
styling::CanvasStyling,
|
||||
},
|
||||
data_collection::temperature::TemperatureType,
|
||||
utils::general::{sort_partial_fn, truncate_to_text},
|
||||
utils::general::sort_partial_fn,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TempWidgetData {
|
||||
pub sensor: KString,
|
||||
pub sensor: Cow<'static, str>,
|
||||
pub temperature_value: Option<u64>,
|
||||
pub temperature_type: TemperatureType,
|
||||
}
|
||||
@ -39,7 +37,7 @@ impl ColumnHeader for TempWidgetColumn {
|
||||
}
|
||||
|
||||
impl TempWidgetData {
|
||||
pub fn temperature(&self) -> KString {
|
||||
pub fn temperature(&self) -> Cow<'static, str> {
|
||||
match self.temperature_value {
|
||||
Some(temp_val) => {
|
||||
let temp_type = match self.temperature_type {
|
||||
@ -55,10 +53,12 @@ impl TempWidgetData {
|
||||
}
|
||||
|
||||
impl DataToCell<TempWidgetColumn> for TempWidgetData {
|
||||
fn to_cell(&self, column: &TempWidgetColumn, calculated_width: NonZeroU16) -> Option<Text<'_>> {
|
||||
fn to_cell(
|
||||
&self, column: &TempWidgetColumn, _calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
Some(match column {
|
||||
TempWidgetColumn::Sensor => truncate_to_text(&self.sensor, calculated_width.get()),
|
||||
TempWidgetColumn::Temp => truncate_to_text(&self.temperature(), calculated_width.get()),
|
||||
TempWidgetColumn::Sensor => self.sensor.clone(),
|
||||
TempWidgetColumn::Temp => self.temperature(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -135,7 +135,8 @@ impl TempWidgetState {
|
||||
self.force_update_data = true;
|
||||
}
|
||||
|
||||
pub fn ingest_data(&mut self, data: &[TempWidgetData]) {
|
||||
/// Update the current table data.
|
||||
pub fn set_table_data(&mut self, data: &[TempWidgetData]) {
|
||||
let mut data = data.to_vec();
|
||||
if let Some(column) = self.table.columns.get(self.table.sort_index()) {
|
||||
column.sort_by(&mut data, self.table.order());
|
||||
|
Loading…
x
Reference in New Issue
Block a user