diff --git a/Cargo.lock b/Cargo.lock index ddaa490e..29b3902d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 0e24e82f..d3979b2d 100644 --- a/Cargo.toml +++ b/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" diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index 3ad819be..f04408eb 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -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, }; diff --git a/src/app/states.rs b/src/app/states.rs index 8b5f0ce8..ee69d90c 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -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, diff --git a/src/bin/main.rs b/src/bin/main.rs index 0ee3289f..c7841b4d 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -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 { diff --git a/src/canvas/components/data_table.rs b/src/canvas/components/data_table.rs index 41ebffb0..f4e26f0c 100644 --- a/src/canvas/components/data_table.rs +++ b/src/canvas/components/data_table.rs @@ -152,7 +152,7 @@ impl, H: ColumnHeader, S: SortType, C: DataTableColumn for TestType { fn to_cell( &self, _column: &&'static str, _calculated_width: NonZeroU16, - ) -> Option> { + ) -> Option> { None } diff --git a/src/canvas/components/data_table/data_type.rs b/src/canvas/components/data_table/data_type.rs index 0be5646f..ebac3c07 100644 --- a/src/canvas/components/data_table/data_type.rs +++ b/src/canvas/components/data_table/data_type.rs @@ -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 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>; + /// 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>; /// Apply styling to the generated [`Row`] of cells. /// diff --git a/src/canvas/components/data_table/draw.rs b/src/canvas/components/data_table/draw.rs index 486a430e..46304f83 100644 --- a/src/canvas/components/data_table/draw.rs +++ b/src/canvas/components/data_table/draw.rs @@ -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())) }), ); diff --git a/src/canvas/components/data_table/sortable.rs b/src/canvas/components/data_table/sortable.rs index 7b548309..1ece31cc 100644 --- a/src/canvas/components/data_table/sortable.rs +++ b/src/canvas/components/data_table/sortable.rs @@ -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 for TestType { fn to_cell( &self, _column: &ColumnType, _calculated_width: NonZeroU16, - ) -> Option> { + ) -> Option> { None } diff --git a/src/canvas/components/tui_widget/time_chart/points.rs b/src/canvas/components/tui_widget/time_chart/points.rs index c53dd264..350f9da8 100644 --- a/src/canvas/components/tui_widget/time_chart/points.rs +++ b/src/canvas/components/tui_widget/time_chart/points.rs @@ -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<'_>) { diff --git a/src/data_conversion.rs b/src/data_conversion.rs index 0d1c4604..e08503ec 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -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 { +pub fn convert_mem_data_points(data: &DataCollection) -> Vec { let mut result: Vec = 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 { } #[cfg(not(target_os = "windows"))] -pub fn convert_cache_data_points(current_data: &DataCollection) -> Vec { +pub fn convert_cache_data_points(data: &DataCollection) -> Vec { let mut result: Vec = 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 { result } -pub fn convert_swap_data_points(current_data: &DataCollection) -> Vec { +pub fn convert_swap_data_points(data: &DataCollection) -> Vec { let mut result: Vec = 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 { /// /// 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, Vec) { let mut rx: Vec = 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 { diff --git a/src/options/config.rs b/src/options/config.rs index 8a150ee8..5fe29fe2 100644 --- a/src/options/config.rs +++ b/src/options/config.rs @@ -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)] diff --git a/src/utils/data_prefixes.rs b/src/utils/data_prefixes.rs index 62a3edd6..516a66ba 100644 --- a/src/utils/data_prefixes.rs +++ b/src/utils/data_prefixes.rs @@ -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}")), + } +} diff --git a/src/utils/general.rs b/src/utils/general.rs index 30de7408..6f3d14b4 100644 --- a/src/utils/general.rs +++ b/src/utils/general.rs @@ -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>(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>(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(is_descending: bool) -> fn(T, T) -> Ordering { @@ -250,29 +24,6 @@ pub fn partial_ordering_desc(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; diff --git a/src/utils/strings.rs b/src/utils/strings.rs new file mode 100644 index 00000000..b7c54188 --- /dev/null +++ b/src/utils/strings.rs @@ -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>(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>(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" + ); + } +} diff --git a/src/widgets/cpu_graph.rs b/src/widgets/cpu_graph.rs index 15f1f939..e90ce697 100644 --- a/src/widgets/cpu_graph.rs +++ b/src/widgets/cpu_graph.rs @@ -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 for CpuWidgetTableData { - fn to_cell(&self, column: &CpuWidgetColumn, calculated_width: NonZeroU16) -> Option> { + fn to_cell( + &self, column: &CpuWidgetColumn, calculated_width: NonZeroU16, + ) -> Option> { const CPU_TRUNCATE_BREAKPOINT: u16 = 5; let calculated_width = calculated_width.get(); @@ -107,25 +108,19 @@ impl DataToCell 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()), } } } diff --git a/src/widgets/disk_table.rs b/src/widgets/disk_table.rs index f27cee94..047c0a14 100644 --- a/src/widgets/disk_table.rs +++ b/src/widgets/disk_table.rs @@ -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, pub used_bytes: Option, pub total_bytes: Option, pub summed_total_bytes: Option, - 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 { + fn free_percent(&self) -> Option { 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 { + fn used_percent(&self) -> Option { 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 for DiskWidgetData { - fn to_cell(&self, column: &DiskWidgetColumn, calculated_width: NonZeroU16) -> Option> { - let calculated_width = calculated_width.get(); + fn to_cell( + &self, column: &DiskWidgetColumn, _calculated_width: NonZeroU16, + ) -> Option> { 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()); diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs index 0ca64509..49087fb3 100644 --- a/src/widgets/process_table.rs +++ b/src/widgets/process_table.rs @@ -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) diff --git a/src/widgets/process_table/proc_widget_data.rs b/src/widgets/process_table/proc_widget_data.rs index 0537a474..656be892 100644 --- a/src/widgets/process_table/proc_widget_data.rs +++ b/src/widgets/process_table/proc_widget_data.rs @@ -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 for ProcWidgetData { - fn to_cell(&self, column: &ProcColumn, calculated_width: NonZeroU16) -> Option> { + fn to_cell( + &self, column: &ProcColumn, calculated_width: NonZeroU16, + ) -> Option> { 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)] diff --git a/src/widgets/process_table/sort_table.rs b/src/widgets/process_table/sort_table.rs index e17d62d9..02409785 100644 --- a/src/widgets/process_table/sort_table.rs +++ b/src/widgets/process_table/sort_table.rs @@ -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 for &'static str { - fn to_cell(&self, _column: &SortTableColumn, calculated_width: NonZeroU16) -> Option> { - Some(truncate_to_text(self, calculated_width.get())) + fn to_cell( + &self, _column: &SortTableColumn, _calculated_width: NonZeroU16, + ) -> Option> { + Some(Cow::Borrowed(self)) } fn column_widths>(data: &[Self], _columns: &[C]) -> Vec @@ -29,8 +26,10 @@ impl DataToCell for &'static str { } impl DataToCell for Cow<'static, str> { - fn to_cell(&self, _column: &SortTableColumn, calculated_width: NonZeroU16) -> Option> { - Some(truncate_to_text(self, calculated_width.get())) + fn to_cell( + &self, _column: &SortTableColumn, _calculated_width: NonZeroU16, + ) -> Option> { + Some(self.clone()) } fn column_widths>(data: &[Self], _columns: &[C]) -> Vec diff --git a/src/widgets/temperature_table.rs b/src/widgets/temperature_table.rs index 4ebc8745..01a22e38 100644 --- a/src/widgets/temperature_table.rs +++ b/src/widgets/temperature_table.rs @@ -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, 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 for TempWidgetData { - fn to_cell(&self, column: &TempWidgetColumn, calculated_width: NonZeroU16) -> Option> { + fn to_cell( + &self, column: &TempWidgetColumn, _calculated_width: NonZeroU16, + ) -> Option> { 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());