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:
Clement Tsang 2024-05-07 02:03:30 -04:00 committed by GitHub
parent bcc89170a6
commit 398bf5930f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 696 additions and 720 deletions

10
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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,
};

View File

@ -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,

View File

@ -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 {

View File

@ -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
}

View File

@ -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.
///

View File

@ -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()))
}),
);

View File

@ -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
}

View File

@ -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<'_>) {

View File

@ -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 &current_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 &current_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 &current_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}",

View File

@ -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,

View File

@ -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) => {

View File

@ -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)]

View File

@ -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}")),
}
}

View File

@ -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
View 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"
);
}
}

View File

@ -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()),
}
}
}

View File

@ -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());

View File

@ -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)

View File

@ -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)]

View File

@ -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>

View File

@ -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());