diff --git a/Cargo.lock b/Cargo.lock index cc26bd3..1dd394f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -222,7 +237,12 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", ] [[package]] @@ -839,6 +859,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1444,6 +1487,7 @@ dependencies = [ "serde", "thiserror", "toml", + "transmission-rpc", "url", "xdg", ] @@ -1570,6 +1614,7 @@ version = "0.3.3" dependencies = [ "anyhow", "base64 0.22.1", + "chrono", "clap", "crossterm", "futures", @@ -2344,6 +2389,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 9aef960..260402c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ rss = "2" reqwest = "0.12" regex = "1" thiserror = "1" +chrono = "0.4" # Async tokio = { version = "1", features = ["macros", "sync"] } diff --git a/README.md b/README.md index 1bff434..b58c21e 100644 --- a/README.md +++ b/README.md @@ -78,14 +78,22 @@ headers_hide = false [connection] url = "http://CHANGE_ME:9091/transmission/rpc" # REQUIRED! +# Refresh timings (in seconds) +torrents_refresh = 5 +stats_refresh = 5 +free_space_refresh = 10 + # If you need username and password to authenticate: # username = "CHANGE_ME" # password = "CHANGE_ME" -# Refresh timings (in seconds) -torrents_refresh = 5 -stats_refresh = 10 -free_space_refresh = 10 +[torrents_tab] +# Available fields: +# Id, Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, +# Padding, UploadRatio, UploadedEver, AddedDate, ActivityDate, PeersConnected +# SmallStatus +headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] + ``` There's also a self-documenting keymap config located at `~/.config/rustmission/keymap.toml` with sane defaults. diff --git a/rm-config/Cargo.toml b/rm-config/Cargo.toml index e1ab5f9..f903d13 100644 --- a/rm-config/Cargo.toml +++ b/rm-config/Cargo.toml @@ -20,3 +20,4 @@ url.workspace = true ratatui.workspace = true crossterm.workspace = true thiserror.workspace = true +transmission-rpc.workspace = true diff --git a/rm-config/defaults/config.toml b/rm-config/defaults/config.toml index 5cce726..7d87ef1 100644 --- a/rm-config/defaults/config.toml +++ b/rm-config/defaults/config.toml @@ -17,11 +17,19 @@ headers_hide = false [connection] url = "http://CHANGE_ME:9091/transmission/rpc" # REQUIRED! +# Refresh timings (in seconds) +torrents_refresh = 5 +stats_refresh = 5 +free_space_refresh = 10 + # If you need username and password to authenticate: # username = "CHANGE_ME" # password = "CHANGE_ME" -# Refresh timings (in seconds) -torrents_refresh = 5 -stats_refresh = 10 -free_space_refresh = 10 + +[torrents_tab] +# Available fields: +# Id, Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, +# Padding, UploadRatio, UploadedEver, AddedDate, ActivityDate, PeersConnected +# SmallStatus +headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] diff --git a/rm-config/src/lib.rs b/rm-config/src/lib.rs index a2e3a8b..59347e3 100644 --- a/rm-config/src/lib.rs +++ b/rm-config/src/lib.rs @@ -1,5 +1,5 @@ pub mod keymap; -mod main_config; +pub mod main_config; mod utils; use std::path::PathBuf; @@ -11,6 +11,7 @@ use main_config::MainConfig; pub struct Config { pub general: main_config::General, pub connection: main_config::Connection, + pub torrents_tab: main_config::TorrentsTab, pub keybindings: KeymapConfig, pub directories: Directories, } @@ -33,6 +34,7 @@ impl Config { Ok(Self { general: main_config.general, connection: main_config.connection, + torrents_tab: main_config.torrents_tab, keybindings: keybindings.clone(), directories, }) diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index 2a45893..50f85fe 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -1,19 +1,21 @@ -use std::{path::PathBuf, sync::OnceLock}; +use std::{io::ErrorKind, path::PathBuf, sync::OnceLock}; use anyhow::Result; -use ratatui::style::Color; +use ratatui::{layout::Constraint, style::Color}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::utils::{self, put_config}; +use crate::utils::{self}; -#[derive(Serialize, Deserialize)] +#[derive(Deserialize)] pub struct MainConfig { pub general: General, pub connection: Connection, + #[serde(default)] + pub torrents_tab: TorrentsTab, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Deserialize)] pub struct General { #[serde(default)] pub auto_hide: bool, @@ -33,7 +35,7 @@ fn default_beginner_mode() -> bool { true } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Deserialize)] pub struct Connection { pub username: Option, pub password: Option, @@ -50,19 +52,103 @@ fn default_refresh() -> u64 { 5 } +#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Copy)] +pub enum Header { + Name, + SizeWhenDone, + Progress, + Eta, + DownloadRate, + UploadRate, + DownloadDir, + Padding, + UploadRatio, + UploadedEver, + Id, + ActivityDate, + AddedDate, + PeersConnected, + SmallStatus, +} + +impl Header { + pub fn default_constraint(&self) -> Constraint { + match self { + Self::Name => Constraint::Max(70), + Self::SizeWhenDone => Constraint::Length(12), + Self::Progress => Constraint::Length(12), + Self::Eta => Constraint::Length(12), + Self::DownloadRate => Constraint::Length(12), + Self::UploadRate => Constraint::Length(12), + Self::DownloadDir => Constraint::Max(70), + Self::Padding => Constraint::Length(2), + Self::UploadRatio => Constraint::Length(6), + Self::UploadedEver => Constraint::Length(12), + Self::Id => Constraint::Length(4), + Self::ActivityDate => Constraint::Length(14), + Self::AddedDate => Constraint::Length(12), + Self::PeersConnected => Constraint::Length(6), + Self::SmallStatus => Constraint::Length(1), + } + } + + pub fn header_name(&self) -> &'static str { + match *self { + Self::Name => "Name", + Self::SizeWhenDone => "Size", + Self::Progress => "Progress", + Self::Eta => "ETA", + Self::DownloadRate => "Download", + Self::UploadRate => "Upload", + Self::DownloadDir => "Directory", + Self::Padding => "", + Self::UploadRatio => "Ratio", + Self::UploadedEver => "Up Ever", + Self::Id => "Id", + Self::ActivityDate => "Last active", + Self::AddedDate => "Added", + Self::PeersConnected => "Peers", + Self::SmallStatus => "", + } + } +} + +#[derive(Deserialize)] +pub struct TorrentsTab { + pub headers: Vec
, +} + +impl Default for TorrentsTab { + fn default() -> Self { + Self { + headers: vec![ + Header::Name, + Header::SizeWhenDone, + Header::Progress, + Header::Eta, + Header::DownloadRate, + Header::UploadRate, + ], + } + } +} + impl MainConfig { pub(crate) const FILENAME: &'static str = "config.toml"; const DEFAULT_CONFIG: &'static str = include_str!("../defaults/config.toml"); pub(crate) fn init() -> Result { - let Ok(config) = utils::fetch_config(Self::FILENAME) else { - put_config(Self::DEFAULT_CONFIG, Self::FILENAME)?; - // TODO: check if the user really changed the config. - println!("Update {:?} and start rustmission again", Self::path()); - std::process::exit(0); + match utils::fetch_config::(Self::FILENAME) { + Ok(config) => return Ok(config), + Err(e) => match e { + utils::ConfigFetchingError::Io(e) if e.kind() == ErrorKind::NotFound => { + utils::put_config::(Self::DEFAULT_CONFIG, Self::FILENAME)?; + println!("Update {:?} and start rustmission again", Self::path()); + std::process::exit(0); + } + _ => anyhow::bail!(e), + }, }; - - Ok(config) } pub(crate) fn path() -> &'static PathBuf { diff --git a/rm-config/src/utils.rs b/rm-config/src/utils.rs index 102afde..3654cdb 100644 --- a/rm-config/src/utils.rs +++ b/rm-config/src/utils.rs @@ -42,9 +42,9 @@ pub fn fetch_config(config_name: &str) -> Result( content: &'static str, filename: &str, -) -> Result { +) -> Result { let config_path = get_config_path(filename); let mut config_file = File::create(config_path)?; config_file.write_all(content.as_bytes())?; - Ok(toml::from_str(content).expect("default configs are correct")) + Ok(toml::from_str(content)?) } diff --git a/rm-main/Cargo.toml b/rm-main/Cargo.toml index d518e11..05d531f 100644 --- a/rm-main/Cargo.toml +++ b/rm-main/Cargo.toml @@ -34,3 +34,4 @@ rss.workspace = true reqwest.workspace = true regex.workspace = true throbber-widgets-tui.workspace = true +chrono.workspace = true diff --git a/rm-main/src/transmission/fetchers.rs b/rm-main/src/transmission/fetchers.rs index 45fc025..4181c78 100644 --- a/rm-main/src/transmission/fetchers.rs +++ b/rm-main/src/transmission/fetchers.rs @@ -72,6 +72,10 @@ pub async fn torrents(ctx: app::Ctx, table_manager: Arc>) { TorrentGetField::RateDownload, TorrentGetField::Status, TorrentGetField::DownloadDir, + TorrentGetField::UploadedEver, + TorrentGetField::ActivityDate, + TorrentGetField::AddedDate, + TorrentGetField::PeersConnected, ]; let rpc_response = ctx .client diff --git a/rm-main/src/ui/tabs/torrents/bottom_stats.rs b/rm-main/src/ui/tabs/torrents/bottom_stats.rs index 717c06c..eec8ab7 100644 --- a/rm-main/src/ui/tabs/torrents/bottom_stats.rs +++ b/rm-main/src/ui/tabs/torrents/bottom_stats.rs @@ -40,7 +40,7 @@ impl Component for BottomStats { let download = bytes_to_human_format(stats.download_speed); let upload = bytes_to_human_format(stats.upload_speed); - let mut text = format!("▼ {download} | ▲ {upload}"); + let mut text = format!(" {download} |  {upload}"); if let Some(free_space) = &*self.free_space.lock().unwrap() { let free_space = bytes_to_human_format(free_space.size_bytes); diff --git a/rm-main/src/ui/tabs/torrents/mod.rs b/rm-main/src/ui/tabs/torrents/mod.rs index 167cb75..b5d69fb 100644 --- a/rm-main/src/ui/tabs/torrents/mod.rs +++ b/rm-main/src/ui/tabs/torrents/mod.rs @@ -121,12 +121,10 @@ impl TorrentsTab { .accent_color); let table_widget = { - let table = Table::new(torrent_rows, table_manager_lock.widths) + let table = Table::new(torrent_rows, &table_manager_lock.widths) .highlight_style(highlight_table_style); if !self.ctx.config.general.headers_hide { - table.header(Row::new( - table_manager_lock.header().iter().map(|s| s.as_str()), - )) + table.header(Row::new(table_manager_lock.header().iter().cloned())) } else { table } diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index d5b5e6a..7dd0137 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -1,8 +1,10 @@ +use chrono::{Datelike, NaiveDateTime}; use ratatui::{ style::{Style, Stylize}, text::{Line, Span}, widgets::Row, }; +use rm_config::main_config::Header; use transmission_rpc::types::{Id, Torrent, TorrentStatus}; use crate::utils::{ @@ -17,31 +19,31 @@ pub struct RustmissionTorrent { pub eta_secs: String, pub download_speed: String, pub upload_speed: String, + pub uploaded_ever: String, + pub upload_ratio: String, status: TorrentStatus, pub style: Style, pub id: Id, pub download_dir: String, + pub activity_date: NaiveDateTime, + pub added_date: NaiveDateTime, + pub peers_connected: i64, } impl RustmissionTorrent { - pub fn to_row(&self) -> ratatui::widgets::Row { - Row::new([ - Line::from(self.torrent_name.as_str()), - Line::from(""), - Line::from(self.size_when_done.as_str()), - Line::from(self.progress.as_str()), - Line::from(self.eta_secs.as_str()), - Line::from(download_speed_format(&self.download_speed)), - Line::from(upload_speed_format(&self.upload_speed)), - Line::from(self.download_dir.as_str()), - ]) - .style(self.style) + pub fn to_row(&self, headers: &Vec
) -> ratatui::widgets::Row { + headers + .iter() + .map(|header| self.header_to_line(*header)) + .collect::() + .style(self.style) } pub fn to_row_with_higlighted_indices( &self, highlighted_indices: Vec, highlight_style: Style, + headers: &Vec
, ) -> ratatui::widgets::Row { let mut torrent_name_line = Line::default(); @@ -53,16 +55,54 @@ impl RustmissionTorrent { } } - Row::new([ - Line::from(torrent_name_line), - Line::from(""), - Line::from(self.size_when_done.as_str()), - Line::from(self.progress.as_str()), - Line::from(self.eta_secs.as_str()), - Line::from(download_speed_format(&self.download_speed)), - Line::from(upload_speed_format(&self.upload_speed)), - Line::from(self.download_dir.as_str()), - ]) + let mut cells = vec![]; + + for header in headers { + if *header == Header::Name { + cells.push(Line::from(torrent_name_line.clone())) + } else { + cells.push(self.header_to_line(*header)) + } + } + + Row::new(cells) + } + + fn header_to_line(&self, header: Header) -> Line { + match header { + Header::Name => Line::from(self.torrent_name.as_str()), + Header::SizeWhenDone => Line::from(self.size_when_done.as_str()), + Header::Progress => Line::from(self.progress.as_str()), + Header::Eta => Line::from(self.eta_secs.as_str()), + Header::DownloadRate => Line::from(download_speed_format(&self.download_speed)), + Header::UploadRate => Line::from(upload_speed_format(&self.upload_speed)), + Header::DownloadDir => Line::from(self.download_dir.as_str()), + Header::Padding => Line::raw(""), + Header::Id => match &self.id { + Id::Id(id) => Line::from(id.to_string()), + Id::Hash(hash) => Line::from(hash.as_str()), + }, + Header::UploadRatio => Line::from(self.upload_ratio.as_str()), + Header::UploadedEver => Line::from(self.uploaded_ever.as_str()), + Header::ActivityDate => time_to_line(self.activity_date), + Header::AddedDate => time_to_line(self.added_date), + Header::PeersConnected => Line::from(self.peers_connected.to_string()), + Header::SmallStatus => match self.status() { + TorrentStatus::Stopped => Line::from("󰏤"), + TorrentStatus::QueuedToVerify => Line::from("󱥸"), + TorrentStatus::Verifying => Line::from("󰑓"), + TorrentStatus::QueuedToDownload => Line::from("󱥸"), + TorrentStatus::QueuedToSeed => Line::from("󱥸"), + TorrentStatus::Seeding => { + if !self.upload_speed.is_empty() { + Line::from("") + } else { + Line::from("󰄬") + } + } + TorrentStatus::Downloading => Line::from(""), + }, + } } pub const fn status(&self) -> TorrentStatus { @@ -116,10 +156,30 @@ impl From<&Torrent> for RustmissionTorrent { _ => Style::default(), }; - let download_dir = t - .download_dir - .clone() - .expect("torrent download directory requested"); + let download_dir = t.download_dir.clone().expect("field requested"); + + let uploaded_ever = bytes_to_human_format(t.uploaded_ever.expect("field requested")); + + let upload_ratio = { + let raw = t.upload_ratio.expect("field requested"); + format!("{:.1}", raw) + }; + + let activity_date = { + let raw = t.activity_date.expect("field requested"); + chrono::DateTime::from_timestamp(raw, 0) + .unwrap() + .naive_local() + }; + + let added_date = { + let raw = t.added_date.expect("field requested"); + chrono::DateTime::from_timestamp(raw, 0) + .unwrap() + .naive_local() + }; + + let peers_connected = t.peers_connected.expect("field requested"); Self { torrent_name, @@ -132,6 +192,20 @@ impl From<&Torrent> for RustmissionTorrent { style, id, download_dir, + uploaded_ever, + upload_ratio, + activity_date, + added_date, + peers_connected, } } } + +fn time_to_line<'a>(time: NaiveDateTime) -> Line<'a> { + let today = chrono::Local::now(); + if time.year() == today.year() && time.month() == today.month() && time.day() == today.day() { + Line::from(time.format("Today %H:%M").to_string()) + } else { + Line::from(time.format("%y|%m|%d %H:%M").to_string()) + } +} diff --git a/rm-main/src/ui/tabs/torrents/table_manager.rs b/rm-main/src/ui/tabs/torrents/table_manager.rs index f340efa..48df59f 100644 --- a/rm-main/src/ui/tabs/torrents/table_manager.rs +++ b/rm-main/src/ui/tabs/torrents/table_manager.rs @@ -1,6 +1,10 @@ use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use ratatui::{prelude::*, widgets::Row}; -use std::sync::{Arc, Mutex}; +use rm_config::main_config::Header; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; use crate::{app, ui::components::table::GenericTable}; @@ -9,31 +13,27 @@ use super::rustmission_torrent::RustmissionTorrent; pub struct TableManager { ctx: app::Ctx, pub table: GenericTable, - pub widths: [Constraint; 8], + pub widths: Vec, pub filter: Arc>>, pub torrents_displaying_no: u16, - header: Vec, + headers: Vec<&'static str>, } impl TableManager { pub fn new(ctx: app::Ctx, table: GenericTable) -> Self { - let widths = Self::default_widths(); + let widths = Self::default_widths(&ctx.config.torrents_tab.headers); + let mut headers = vec![]; + for header in &ctx.config.torrents_tab.headers { + headers.push(header.header_name()); + } + Self { ctx, table, widths, filter: Arc::new(Mutex::new(None)), torrents_displaying_no: 0, - header: vec![ - "Name".to_owned(), - "".to_owned(), - "Size".to_owned(), - "Progress".to_owned(), - "ETA".to_owned(), - "Download".to_owned(), - "Upload".to_owned(), - "Directory".to_owned(), - ], + headers, } } @@ -46,13 +46,13 @@ impl TableManager { self.table .items .iter() - .map(RustmissionTorrent::to_row) + .map(|t| t.to_row(&self.ctx.config.torrents_tab.headers)) .collect() } } - pub const fn header(&self) -> &Vec { - &self.header + pub const fn header(&self) -> &Vec<&'static str> { + &self.headers } pub fn current_torrent(&mut self) -> Option<&mut RustmissionTorrent> { @@ -94,61 +94,77 @@ impl TableManager { for torrent in torrents { if let Some((_, indices)) = matcher.fuzzy_indices(&torrent.torrent_name, filter) { - rows.push(torrent.to_row_with_higlighted_indices(indices, highlight_style)) + rows.push(torrent.to_row_with_higlighted_indices( + indices, + highlight_style, + &self.ctx.config.torrents_tab.headers, + )) } } rows } - const fn default_widths() -> [Constraint; 8] { - [ - Constraint::Max(70), // Name - Constraint::Length(5), // - Constraint::Length(12), // Size - Constraint::Length(12), // Progress - Constraint::Length(12), // ETA - Constraint::Length(12), // Download - Constraint::Length(12), // Upload - Constraint::Max(70), // Download directory - ] + fn default_widths(headers: &Vec
) -> Vec { + let mut constraints = vec![]; + + for header in headers { + constraints.push(header.default_constraint()) + } + constraints } - fn header_widths(&self, rows: &[RustmissionTorrent]) -> [Constraint; 8] { + fn header_widths(&self, rows: &[RustmissionTorrent]) -> Vec { + let headers = &self.ctx.config.torrents_tab.headers; + if !self.ctx.config.general.auto_hide { - return Self::default_widths(); + return Self::default_widths(&headers); } - let mut download_width = 0; - let mut upload_width = 0; - let mut progress_width = 0; - let mut eta_width = 0; + let mut map = HashMap::new(); + + for header in headers { + map.insert(header, header.default_constraint()); + } + + let hidable_headers = [ + Header::Progress, + Header::UploadRate, + Header::DownloadRate, + Header::Eta, + ]; + + for hidable_header in &hidable_headers { + map.entry(hidable_header) + .and_modify(|c| *c = Constraint::Length(0)); + } for row in rows { if !row.download_speed.is_empty() { - download_width = 11; + map.entry(&Header::DownloadRate) + .and_modify(|c| *c = Header::DownloadRate.default_constraint()); } if !row.upload_speed.is_empty() { - upload_width = 11; + map.entry(&Header::UploadRate) + .and_modify(|c| *c = Header::UploadRate.default_constraint()); } if !row.progress.is_empty() { - progress_width = 11; + map.entry(&Header::Progress) + .and_modify(|c| *c = Header::Progress.default_constraint()); } if !row.eta_secs.is_empty() { - eta_width = 11; + map.entry(&Header::Eta) + .and_modify(|c| *c = Header::Eta.default_constraint()); } } - [ - Constraint::Max(70), // Name - Constraint::Length(5), // - Constraint::Length(11), // Size - Constraint::Length(progress_width), // Progress - Constraint::Length(eta_width), // ETA - Constraint::Length(download_width), // Download - Constraint::Length(upload_width), // Upload - Constraint::Max(70), // Download directory - ] + let mut constraints = vec![]; + + for header in headers { + constraints.push(map.remove(header).expect("this header exists")) + } + + constraints } } diff --git a/rm-main/src/utils.rs b/rm-main/src/utils.rs index 992c984..f9c2677 100644 --- a/rm-main/src/utils.rs +++ b/rm-main/src/utils.rs @@ -64,14 +64,14 @@ pub fn seconds_to_human_format(seconds: i64) -> String { pub fn download_speed_format(download_speed: &str) -> String { if download_speed.len() > 0 { - return format!("▼ {}", download_speed); + return format!(" {}", download_speed); } download_speed.to_string() } pub fn upload_speed_format(upload_speed: &str) -> String { if upload_speed.len() > 0 { - return format!("▲ {}", upload_speed); + return format!(" {}", upload_speed); } upload_speed.to_string() }