From 00afd66006a510348ec583797cff53e1d8b849b8 Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:13:10 -0400 Subject: [PATCH] refactor: share implementation for pecentage-based time graphs (#1736) * refactor: move components to a 'drawing' folder * Revert "refactor: move components to a 'drawing' folder" This reverts commit a1316bdf3aa4437bed2ca786896c2b387ccf5f0e. * move stuff out of constants because it sucks * move more things! * cleanup * some restructuring * refactor percent time graph to common impl * wow thanks copilot --- src/app.rs | 30 +++--- src/canvas.rs | 5 + .../{components.rs => components/mod.rs} | 2 +- .../{time_graph.rs => time_graph/base.rs} | 4 +- .../time_graph/{ => base}/time_chart.rs | 0 .../{ => base}/time_chart/canvas.rs | 0 .../time_graph/{ => base}/time_chart/grid.rs | 0 .../{ => base}/time_chart/points.rs | 0 src/canvas/components/time_graph/mod.rs | 4 + .../time_graph/variants/auto_y_axis.rs | 2 + .../components/time_graph/variants/mod.rs | 15 +++ .../components/time_graph/variants/percent.rs | 96 +++++++++++++++++++ src/canvas/{dialogs.rs => dialogs/mod.rs} | 0 src/canvas/drawing_utils.rs | 17 +++- src/canvas/widgets/cpu_graph.rs | 40 +++----- src/canvas/widgets/mem_graph.rs | 38 +++----- src/canvas/{widgets.rs => widgets/mod.rs} | 0 src/canvas/widgets/network_graph.rs | 2 +- src/constants.rs | 28 +----- src/options.rs | 7 ++ 20 files changed, 184 insertions(+), 106 deletions(-) rename src/canvas/{components.rs => components/mod.rs} (55%) rename src/canvas/components/{time_graph.rs => time_graph/base.rs} (98%) rename src/canvas/components/time_graph/{ => base}/time_chart.rs (100%) rename src/canvas/components/time_graph/{ => base}/time_chart/canvas.rs (100%) rename src/canvas/components/time_graph/{ => base}/time_chart/grid.rs (100%) rename src/canvas/components/time_graph/{ => base}/time_chart/points.rs (100%) create mode 100644 src/canvas/components/time_graph/mod.rs create mode 100644 src/canvas/components/time_graph/variants/auto_y_axis.rs create mode 100644 src/canvas/components/time_graph/variants/mod.rs create mode 100644 src/canvas/components/time_graph/variants/percent.rs rename src/canvas/{dialogs.rs => dialogs/mod.rs} (100%) rename src/canvas/{widgets.rs => widgets/mod.rs} (100%) diff --git a/src/app.rs b/src/app.rs index c114708c..3c18b310 100644 --- a/src/app.rs +++ b/src/app.rs @@ -26,6 +26,8 @@ use crate::{ widgets::{ProcWidgetColumn, ProcWidgetMode}, }; +const STALE_MIN_MILLISECONDS: u64 = 30 * 1000; // Lowest is 30 seconds + #[derive(Debug, Clone, Eq, PartialEq, Default, Copy)] pub enum AxisScaling { #[default] @@ -1091,13 +1093,15 @@ impl App { return; } + const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u64 = 1000; + // Forbid any char key presses when showing a dialog box... if !self.ignore_normal_keybinds() { let current_key_press_inst = Instant::now(); if current_key_press_inst .duration_since(self.last_key_press) .as_millis() - > constants::MAX_KEY_TIMEOUT_IN_MILLISECONDS.into() + > MAX_KEY_TIMEOUT_IN_MILLISECONDS.into() { self.reset_multi_tap_keys(); } @@ -2315,15 +2319,13 @@ impl App { .current_display_time .saturating_sub(self.app_config_fields.time_interval); - if new_time >= constants::STALE_MIN_MILLISECONDS { + if new_time >= STALE_MIN_MILLISECONDS { cpu_widget_state.current_display_time = new_time; if self.app_config_fields.autohide_time { cpu_widget_state.autohide_timer = Some(Instant::now()); } - } else if cpu_widget_state.current_display_time - != constants::STALE_MIN_MILLISECONDS - { - cpu_widget_state.current_display_time = constants::STALE_MIN_MILLISECONDS; + } else if cpu_widget_state.current_display_time != STALE_MIN_MILLISECONDS { + cpu_widget_state.current_display_time = STALE_MIN_MILLISECONDS; if self.app_config_fields.autohide_time { cpu_widget_state.autohide_timer = Some(Instant::now()); } @@ -2341,15 +2343,13 @@ impl App { .current_display_time .saturating_sub(self.app_config_fields.time_interval); - if new_time >= constants::STALE_MIN_MILLISECONDS { + if new_time >= STALE_MIN_MILLISECONDS { mem_widget_state.current_display_time = new_time; if self.app_config_fields.autohide_time { mem_widget_state.autohide_timer = Some(Instant::now()); } - } else if mem_widget_state.current_display_time - != constants::STALE_MIN_MILLISECONDS - { - mem_widget_state.current_display_time = constants::STALE_MIN_MILLISECONDS; + } else if mem_widget_state.current_display_time != STALE_MIN_MILLISECONDS { + mem_widget_state.current_display_time = STALE_MIN_MILLISECONDS; if self.app_config_fields.autohide_time { mem_widget_state.autohide_timer = Some(Instant::now()); } @@ -2367,15 +2367,13 @@ impl App { .current_display_time .saturating_sub(self.app_config_fields.time_interval); - if new_time >= constants::STALE_MIN_MILLISECONDS { + if new_time >= STALE_MIN_MILLISECONDS { net_widget_state.current_display_time = new_time; if self.app_config_fields.autohide_time { net_widget_state.autohide_timer = Some(Instant::now()); } - } else if net_widget_state.current_display_time - != constants::STALE_MIN_MILLISECONDS - { - net_widget_state.current_display_time = constants::STALE_MIN_MILLISECONDS; + } else if net_widget_state.current_display_time != STALE_MIN_MILLISECONDS { + net_widget_state.current_display_time = STALE_MIN_MILLISECONDS; if self.app_config_fields.autohide_time { net_widget_state.autohide_timer = Some(Instant::now()); } diff --git a/src/canvas.rs b/src/canvas.rs index c86703f1..7ce47290 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -1,3 +1,8 @@ +//! Code related to drawing. +//! +//! Note that eventually this should not contain any widget-specific draw code, but rather just generic code +//! or components. + pub mod components; mod dialogs; mod drawing_utils; diff --git a/src/canvas/components.rs b/src/canvas/components/mod.rs similarity index 55% rename from src/canvas/components.rs rename to src/canvas/components/mod.rs index ee3b159c..9de30696 100644 --- a/src/canvas/components.rs +++ b/src/canvas/components/mod.rs @@ -1,4 +1,4 @@ -//! Lower-level components used throughout bottom. +//! Lower-level or shared drawing components used throughout bottom. pub mod data_table; pub mod pipe_gauge; diff --git a/src/canvas/components/time_graph.rs b/src/canvas/components/time_graph/base.rs similarity index 98% rename from src/canvas/components/time_graph.rs rename to src/canvas/components/time_graph/base.rs index c6829bc3..4e372f6c 100644 --- a/src/canvas/components/time_graph.rs +++ b/src/canvas/components/time_graph/base.rs @@ -141,9 +141,7 @@ impl TimeGraph<'_> { /// graph. /// - Expects `graph_data`, which represents *what* data to draw, and /// various details like style and optional legends. - pub fn draw_time_graph( - &self, f: &mut Frame<'_>, draw_loc: Rect, graph_data: Vec>, - ) { + pub fn draw(&self, f: &mut Frame<'_>, draw_loc: Rect, graph_data: Vec>) { // TODO: (points_rework_v1) can we reduce allocations in the underlying graph by saving some sort of state? let x_axis = self.generate_x_axis(); diff --git a/src/canvas/components/time_graph/time_chart.rs b/src/canvas/components/time_graph/base/time_chart.rs similarity index 100% rename from src/canvas/components/time_graph/time_chart.rs rename to src/canvas/components/time_graph/base/time_chart.rs diff --git a/src/canvas/components/time_graph/time_chart/canvas.rs b/src/canvas/components/time_graph/base/time_chart/canvas.rs similarity index 100% rename from src/canvas/components/time_graph/time_chart/canvas.rs rename to src/canvas/components/time_graph/base/time_chart/canvas.rs diff --git a/src/canvas/components/time_graph/time_chart/grid.rs b/src/canvas/components/time_graph/base/time_chart/grid.rs similarity index 100% rename from src/canvas/components/time_graph/time_chart/grid.rs rename to src/canvas/components/time_graph/base/time_chart/grid.rs diff --git a/src/canvas/components/time_graph/time_chart/points.rs b/src/canvas/components/time_graph/base/time_chart/points.rs similarity index 100% rename from src/canvas/components/time_graph/time_chart/points.rs rename to src/canvas/components/time_graph/base/time_chart/points.rs diff --git a/src/canvas/components/time_graph/mod.rs b/src/canvas/components/time_graph/mod.rs new file mode 100644 index 00000000..beba361e --- /dev/null +++ b/src/canvas/components/time_graph/mod.rs @@ -0,0 +1,4 @@ +mod base; +pub mod variants; + +pub(crate) use base::*; diff --git a/src/canvas/components/time_graph/variants/auto_y_axis.rs b/src/canvas/components/time_graph/variants/auto_y_axis.rs new file mode 100644 index 00000000..432f6b24 --- /dev/null +++ b/src/canvas/components/time_graph/variants/auto_y_axis.rs @@ -0,0 +1,2 @@ +//! A variant of a [`crate::canvas::components::time_graph::TimeGraph`] that +//! automatically adjusts the y-axis based on the data provided. diff --git a/src/canvas/components/time_graph/variants/mod.rs b/src/canvas/components/time_graph/variants/mod.rs new file mode 100644 index 00000000..2efeaf75 --- /dev/null +++ b/src/canvas/components/time_graph/variants/mod.rs @@ -0,0 +1,15 @@ +use tui::style::Style; + +use crate::options::config::style::Styles; + +pub(crate) mod auto_y_axis; +pub(crate) mod percent; + +fn get_border_style(styles: &Styles, widget_id: u64, selected_widget_id: u64) -> Style { + let is_on_widget = widget_id == selected_widget_id; + if is_on_widget { + styles.highlighted_border_style + } else { + styles.border_style + } +} diff --git a/src/canvas/components/time_graph/variants/percent.rs b/src/canvas/components/time_graph/variants/percent.rs new file mode 100644 index 00000000..9382a2d8 --- /dev/null +++ b/src/canvas/components/time_graph/variants/percent.rs @@ -0,0 +1,96 @@ +//! A variant of a [`TimeGraph`] that expects data to be in a percentage format, from 0.0 to 100.0. + +use std::borrow::Cow; + +use tui::{layout::Constraint, symbols::Marker}; + +use crate::{ + app::AppConfigFields, + canvas::components::time_graph::{ + AxisBound, ChartScaling, LegendPosition, TimeGraph, variants::get_border_style, + }, + options::config::style::Styles, +}; + +/// Acts as a wrapper for a [`TimeGraph`] that expects data to be in a percentage format, +pub(crate) struct PercentTimeGraph<'a> { + /// The total display range of the graph in milliseconds. + /// + /// TODO: Make this a [`std::time::Duration`]. + pub(crate) display_range: u64, + + /// Whether to hide the x-axis labels. + pub(crate) hide_x_labels: bool, + + /// The app config fields. + /// + /// This is mostly used as a shared mutability workaround due to [`App`] + /// being a giant state struct. + pub(crate) app_config_fields: &'a AppConfigFields, + + /// The current widget selected by the app. + /// + /// This is mostly used as a shared mutability workaround due to [`App`] + /// being a giant state struct. + pub(crate) current_widget: u64, + + /// Whether the current widget is expanded. + /// + /// This is mostly used as a shared mutability workaround due to [`App`] + /// being a giant state struct. + pub(crate) is_expanded: bool, + + /// The title of the graph. + pub(crate) title: Cow<'a, str>, + + /// A reference to the styles. + pub(crate) styles: &'a Styles, + + /// The widget ID corresponding to this graph. + pub(crate) widget_id: u64, + + /// The position of the legend. + pub(crate) legend_position: Option, + + /// The constraints for the legend. + pub(crate) legend_constraints: Option<(Constraint, Constraint)>, +} + +impl<'a> PercentTimeGraph<'a> { + /// Return the final [`TimeGraph`]. + pub fn build(self) -> TimeGraph<'a> { + const Y_BOUNDS: AxisBound = AxisBound::Max(100.5); + const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed(" 0%"), Cow::Borrowed("100%")]; + + let x_min = -(self.display_range as f64); + + let marker = if self.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }; + + let graph_style = self.styles.graph_style; + let border_style = get_border_style(self.styles, self.widget_id, self.current_widget); + let title_style = self.styles.widget_title_style; + let border_type = self.styles.border_type; + + TimeGraph { + x_min, + hide_x_labels: self.hide_x_labels, + y_bounds: Y_BOUNDS, + y_labels: &Y_LABELS, + graph_style, + border_style, + border_type, + title: self.title, + is_selected: self.current_widget == self.widget_id, + is_expanded: self.is_expanded, + title_style, + legend_position: self.legend_position, + legend_constraints: self.legend_constraints, + marker, + scaling: ChartScaling::Linear, + } + } +} diff --git a/src/canvas/dialogs.rs b/src/canvas/dialogs/mod.rs similarity index 100% rename from src/canvas/dialogs.rs rename to src/canvas/dialogs/mod.rs diff --git a/src/canvas/drawing_utils.rs b/src/canvas/drawing_utils.rs index d2d6dd93..ff7ccd23 100644 --- a/src/canvas/drawing_utils.rs +++ b/src/canvas/drawing_utils.rs @@ -5,13 +5,14 @@ use tui::{ widgets::{Block, BorderType, Borders}, }; -use super::SIDE_BORDERS; +pub const SIDE_BORDERS: Borders = Borders::LEFT.union(Borders::RIGHT); +pub const AUTOHIDE_TIMEOUT_MILLISECONDS: u64 = 5000; // 5 seconds to autohide /// Determine whether a graph x-label should be hidden. pub fn should_hide_x_label( always_hide_time: bool, autohide_time: bool, timer: &mut Option, draw_loc: Rect, ) -> bool { - use crate::constants::*; + const TIME_LABEL_HEIGHT_LIMIT: u16 = 7; if always_hide_time || (autohide_time && timer.is_none()) { true @@ -62,8 +63,6 @@ mod test { use tui::layout::Rect; - use crate::constants::*; - let rect = Rect::new(0, 0, 10, 10); let small_rect = Rect::new(0, 0, 10, 6); @@ -91,4 +90,14 @@ mod test { )); assert!(over_timer.is_none()); } + + /// This test exists because previously, [`SIDE_BORDERS`] was set + /// incorrectly after I moved from tui-rs to ratatui. + #[test] + fn assert_side_border_bits_match() { + assert_eq!( + SIDE_BORDERS, + Borders::ALL.difference(Borders::TOP.union(Borders::BOTTOM)) + ) + } } diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index e8933c61..86c92d3a 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -1,9 +1,6 @@ -use std::borrow::Cow; - use tui::{ Frame, layout::{Constraint, Direction, Layout, Rect}, - symbols::Marker, }; use crate::{ @@ -12,7 +9,7 @@ use crate::{ Painter, components::{ data_table::{DrawInfo, SelectionState}, - time_graph::{AxisBound, GraphData, TimeGraph}, + time_graph::{GraphData, variants::percent::PercentTimeGraph}, }, drawing_utils::should_hide_x_label, }, @@ -120,7 +117,7 @@ impl Painter { } fn generate_points<'a>( - &self, cpu_widget_state: &'a mut CpuWidgetState, data: &'a StoredData, show_avg_cpu: bool, + &self, cpu_widget_state: &'a CpuWidgetState, data: &'a StoredData, show_avg_cpu: bool, ) -> Vec> { let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 }; let current_scroll_position = cpu_widget_state.table.state.current_index; @@ -172,15 +169,10 @@ impl Painter { fn draw_cpu_graph( &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { - const Y_BOUNDS: AxisBound = AxisBound::Max(100.5); - const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed(" 0%"), Cow::Borrowed("100%")]; - if let Some(cpu_widget_state) = app_state.states.cpu_state.widget_states.get_mut(&widget_id) { let data = app_state.data_store.get_data(); - let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id); - let x_min = -(cpu_widget_state.current_display_time as f64); let hide_x_labels = should_hide_x_label( app_state.app_config_fields.hide_time, app_state.app_config_fields.autohide_time, @@ -212,30 +204,20 @@ impl Painter { } }; - let marker = if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }; - - TimeGraph { - x_min, + PercentTimeGraph { + display_range: cpu_widget_state.current_display_time, hide_x_labels, - y_bounds: Y_BOUNDS, - y_labels: &Y_LABELS, - graph_style: self.styles.graph_style, - border_style, - border_type: self.styles.border_type, - title, - is_selected: app_state.current_widget.widget_id == widget_id, + app_config_fields: &app_state.app_config_fields, + current_widget: app_state.current_widget.widget_id, is_expanded: app_state.is_expanded, - title_style: self.styles.widget_title_style, + title, + styles: &self.styles, + widget_id, legend_position: None, legend_constraints: None, - marker, - scaling: Default::default(), } - .draw_time_graph(f, draw_loc, graph_data); + .build() + .draw(f, draw_loc, graph_data); } } diff --git a/src/canvas/widgets/mem_graph.rs b/src/canvas/widgets/mem_graph.rs index 2f13e817..a3475ef0 100644 --- a/src/canvas/widgets/mem_graph.rs +++ b/src/canvas/widgets/mem_graph.rs @@ -1,17 +1,16 @@ -use std::{borrow::Cow, time::Instant}; +use std::time::Instant; use tui::{ Frame, layout::{Constraint, Rect}, style::Style, - symbols::Marker, }; use crate::{ app::{App, data::Values}, canvas::{ Painter, - components::time_graph::{AxisBound, GraphData, TimeGraph}, + components::time_graph::{GraphData, variants::percent::PercentTimeGraph}, drawing_utils::should_hide_x_label, }, collection::memory::MemData, @@ -57,12 +56,7 @@ impl Painter { pub fn draw_memory_graph( &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { - const Y_BOUNDS: AxisBound = AxisBound::Max(100.5); - const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed(" 0%"), Cow::Borrowed("100%")]; - if let Some(mem_state) = app_state.states.mem_state.widget_states.get_mut(&widget_id) { - let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id); - let x_min = -(mem_state.current_display_time as f64); let hide_x_labels = should_hide_x_label( app_state.app_config_fields.hide_time, app_state.app_config_fields.autohide_time, @@ -170,30 +164,20 @@ impl Painter { points }; - let marker = if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }; - - TimeGraph { - x_min, + PercentTimeGraph { + display_range: mem_state.current_display_time, hide_x_labels, - y_bounds: Y_BOUNDS, - y_labels: &Y_LABELS, - graph_style: self.styles.graph_style, - border_style, - border_type: self.styles.border_type, - title: " Memory ".into(), - is_selected: app_state.current_widget.widget_id == widget_id, + app_config_fields: &app_state.app_config_fields, + current_widget: app_state.current_widget.widget_id, is_expanded: app_state.is_expanded, - title_style: self.styles.widget_title_style, + title: " Memory ".into(), + styles: &self.styles, + widget_id, legend_position: app_state.app_config_fields.memory_legend_position, legend_constraints: Some((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))), - marker, - scaling: Default::default(), } - .draw_time_graph(f, draw_loc, graph_data); + .build() + .draw(f, draw_loc, graph_data); } if app_state.should_get_widget_bounds() { diff --git a/src/canvas/widgets.rs b/src/canvas/widgets/mod.rs similarity index 100% rename from src/canvas/widgets.rs rename to src/canvas/widgets/mod.rs diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs index 3e5f1dd8..4cacb01e 100644 --- a/src/canvas/widgets/network_graph.rs +++ b/src/canvas/widgets/network_graph.rs @@ -240,7 +240,7 @@ impl Painter { marker, scaling, } - .draw_time_graph(f, draw_loc, graph_data); + .draw(f, draw_loc, graph_data); } } diff --git a/src/constants.rs b/src/constants.rs index c52c9142..a28cb3a3 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,25 +1,13 @@ -use tui::widgets::Borders; +//! A bunch of constants used throughout the application. +//! +//! FIXME: Move these to where it makes more sense. // Default widget ID pub const DEFAULT_WIDGET_ID: u64 = 56709; -// How much data is SHOWN -pub const DEFAULT_TIME_MILLISECONDS: u64 = 60 * 1000; // Defaults to 1 min. -pub const STALE_MIN_MILLISECONDS: u64 = 30 * 1000; // Lowest is 30 seconds -pub const TIME_CHANGE_MILLISECONDS: u64 = 15 * 1000; // How much to increment each time -pub const AUTOHIDE_TIMEOUT_MILLISECONDS: u64 = 5000; // 5 seconds to autohide - -// How fast the screen refreshes -pub const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u64 = 1000; -pub const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u64 = 1000; - // Limits for when we should stop showing table gaps/labels (anything less means // not shown) pub const TABLE_GAP_HEIGHT_LIMIT: u16 = 7; -pub const TIME_LABEL_HEIGHT_LIMIT: u16 = 7; - -// Side borders -pub const SIDE_BORDERS: Borders = Borders::LEFT.union(Borders::RIGHT); // Help text const HELP_CONTENTS_TEXT: [&str; 10] = [ @@ -581,16 +569,6 @@ mod test { } } - /// This test exists because previously, [`SIDE_BORDERS`] was set - /// incorrectly after I moved from tui-rs to ratatui. - #[test] - fn assert_side_border_bits_match() { - assert_eq!( - SIDE_BORDERS, - Borders::ALL.difference(Borders::TOP.union(Borders::BOTTOM)) - ) - } - /// Checks that the default config is valid. #[test] #[cfg(feature = "default")] diff --git a/src/options.rs b/src/options.rs index 10946346..e9537a59 100644 --- a/src/options.rs +++ b/src/options.rs @@ -670,8 +670,11 @@ macro_rules! parse_ms_option { }}; } +/// How fast the screen refreshes #[inline] fn get_update_rate(args: &BottomArgs, config: &Config) -> OptionResult { + const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u64 = 1000; + parse_ms_option!( &args.general.rate, config.flags.as_ref().and_then(|flags| flags.rate.as_ref()), @@ -735,6 +738,8 @@ fn get_dedicated_avg_row(config: &Config) -> bool { fn get_default_time_value( args: &BottomArgs, config: &Config, retention_ms: u64, ) -> OptionResult { + const DEFAULT_TIME_MILLISECONDS: u64 = 60 * 1000; // Defaults to 1 min. + parse_ms_option!( &args.general.default_time_value, config @@ -750,6 +755,8 @@ fn get_default_time_value( #[inline] fn get_time_interval(args: &BottomArgs, config: &Config, retention_ms: u64) -> OptionResult { + const TIME_CHANGE_MILLISECONDS: u64 = 15 * 1000; // How much to increment each time + parse_ms_option!( &args.general.time_delta, config