refactor(app): clarify application and binary roles
Rename the application Cargo package from irtt-cli to irtt-rs (crates/irtt-cli -> crates/irtt-app), and hard-rename its scriptable client binary from irtt-cli to irtt-client, now that the application also ships a first-class server and the old name reads as an umbrella CLI for both roles. Give each dedicated binary (irtt-client, irtt-tui, irtt-server) its own entry point instead of funneling through the multicall dispatcher's argv0/subcommand logic, so a dedicated binary only links the applet code for the role it implements and always runs that role regardless of the name it's invoked or copied under. irtt-rs remains the feature-selectable multicall dispatcher and is now the only binary that inspects argv0 or subcommands. Move future application releases to the irtt-rs/vX.Y.Z tag namespace (regenerated via `dist generate`), and bump the application package to 0.6.0 as a breaking distribution change. Library crate versions are unaffected.
This commit is contained in:
49 files changed
+365
-213
No files matched your search
@@ -0,0 +1,287 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::shared::client::TimestampArg;
|
||||
use crate::shared::client::{
|
||||
parse_target, parse_test_duration, prepare_managed_run, CommonClientArgs, GroupPacingArg,
|
||||
ManagedRunSetup, TargetArg, TargetSelection,
|
||||
};
|
||||
|
||||
pub const DEFAULT_TUI_DURATION: Duration = Duration::ZERO;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[command(name = "irtt-tui", about = "Minimal IRTT-compatible TUI client")]
|
||||
pub struct TuiArgs {
|
||||
/// Server address/host, optionally prefixed with LABEL=. Repeat for multi-target mode.
|
||||
#[arg(
|
||||
value_name = "TARGET",
|
||||
num_args = 1..,
|
||||
required = true,
|
||||
value_parser = parse_target,
|
||||
long_help = "Server address/host, optionally prefixed with LABEL=. Repeat for multi-target mode.\n\nExplicit labels are used in the legend and status table.\n\nExamples:\n irtt-tui host.example\n irtt-tui eu=host.example\n irtt-tui eu=host-a.example us=host-b.example"
|
||||
)]
|
||||
pub targets: Vec<TargetArg>,
|
||||
|
||||
/// Managed group pacing for multi-target mode.
|
||||
#[arg(
|
||||
long,
|
||||
value_enum,
|
||||
default_value_t = GroupPacingArg::Staggered,
|
||||
long_help = "Managed group pacing for multi-target mode.\n\nstaggered spaces active targets across the probe interval. burst sends one probe to every active target back-to-back once per interval."
|
||||
)]
|
||||
pub pacing: GroupPacingArg,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "0",
|
||||
value_parser = parse_test_duration,
|
||||
help = "Test duration; use 0 for continuous mode",
|
||||
long_help = "Test duration; use 0 for continuous mode. The TUI defaults to continuous mode."
|
||||
)]
|
||||
pub duration: Duration,
|
||||
|
||||
#[command(flatten)]
|
||||
pub common: CommonClientArgs,
|
||||
}
|
||||
|
||||
impl TuiArgs {
|
||||
/// Validate the selected targets and prepare the managed run.
|
||||
///
|
||||
/// The TUI always requires a target: the parser rejects an empty target
|
||||
/// list, and this rejects a target set that cannot be labelled uniquely.
|
||||
pub fn prepare(&self) -> Result<ManagedRunSetup, String> {
|
||||
prepare_managed_run(
|
||||
&self.common,
|
||||
self.duration,
|
||||
TargetSelection {
|
||||
targets: &self.targets,
|
||||
pacing: self.pacing,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_continuous(&self) -> bool {
|
||||
self.duration == Duration::ZERO
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn timestamp_mode(&self) -> TimestampArg {
|
||||
self.common.tstamp
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for TuiArgs {
|
||||
type Target = CommonClientArgs;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.common
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::{CommandFactory, Parser};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct SharedOnlyArgs {
|
||||
#[command(flatten)]
|
||||
common: CommonClientArgs,
|
||||
}
|
||||
|
||||
fn parse(args: &[&str]) -> Result<TuiArgs, clap::Error> {
|
||||
let mut argv = vec!["irtt-tui"];
|
||||
argv.extend_from_slice(args);
|
||||
TuiArgs::try_parse_from(argv)
|
||||
}
|
||||
|
||||
fn parse_shared(args: &[&str]) -> Result<SharedOnlyArgs, clap::Error> {
|
||||
let mut argv = vec!["shared-only"];
|
||||
argv.extend_from_slice(args);
|
||||
SharedOnlyArgs::try_parse_from(argv)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_parser_defaults_to_continuous_and_has_no_output_option() {
|
||||
let args = parse(&["127.0.0.1:2112"]).unwrap();
|
||||
assert_eq!(args.targets.len(), 1);
|
||||
assert_eq!(args.targets[0].label, None);
|
||||
assert_eq!(args.targets[0].addr, "127.0.0.1:2112");
|
||||
assert_eq!(args.pacing, GroupPacingArg::Staggered);
|
||||
assert_eq!(args.duration, DEFAULT_TUI_DURATION);
|
||||
assert!(args.is_continuous());
|
||||
assert_eq!(args.prepare().unwrap().client.duration, None);
|
||||
|
||||
let finite = parse(&["--duration", "30s", "127.0.0.1:2112"]).unwrap();
|
||||
assert_eq!(finite.duration, Duration::from_secs(30));
|
||||
assert_eq!(
|
||||
finite.prepare().unwrap().client.duration,
|
||||
Some(Duration::from_secs(30))
|
||||
);
|
||||
|
||||
assert!(parse(&["--output", "human", "127.0.0.1:2112"]).is_err());
|
||||
let help = TuiArgs::command().render_help().to_string();
|
||||
assert!(!help.contains("--output"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_positional_targets_parse() {
|
||||
let args = parse(&["host-a:2112", "host-b:2112"]).unwrap();
|
||||
let specs = args.prepare().unwrap().targets;
|
||||
|
||||
assert_eq!(specs[0].label, "host-a:2112");
|
||||
assert_eq!(specs[0].managed.server_addr, "host-a:2112");
|
||||
assert_eq!(specs[1].label, "host-b:2112");
|
||||
assert_eq!(specs[1].managed.server_addr, "host-b:2112");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_labeled_targets_parse() {
|
||||
let args = parse(&["ams=ams.example.com:2112", "sg=sg.example.com:2112"]).unwrap();
|
||||
let specs = args.prepare().unwrap().targets;
|
||||
|
||||
assert_eq!(specs[0].label, "ams");
|
||||
assert_eq!(specs[0].managed.server_addr, "ams.example.com:2112");
|
||||
assert_eq!(specs[1].label, "sg");
|
||||
assert_eq!(specs[1].managed.server_addr, "sg.example.com:2112");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_least_one_target_is_required() {
|
||||
// The TUI has no target-free mode, so this is rejected at parse time
|
||||
// rather than during preparation.
|
||||
assert!(parse(&[]).is_err());
|
||||
assert!(parse(&["ams=ams.example.com:2112"]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_labels_are_rejected() {
|
||||
let args = parse(&["host-a:2112", "host-a:2112=host-b:2112"]).unwrap();
|
||||
let err = args.prepare().unwrap_err();
|
||||
|
||||
assert!(err.contains("duplicate target label"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_positional_target_strings_get_stable_suffixes() {
|
||||
let args = parse(&["host-a:2112", "host-a:2112"]).unwrap();
|
||||
let specs = args.prepare().unwrap().targets;
|
||||
|
||||
assert_eq!(specs[0].label, "host-a:2112");
|
||||
assert_eq!(specs[1].label, "host-a:2112#2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_target_endpoints_are_allowed() {
|
||||
let args = parse(&["127.0.0.1:2112", "127.0.0.1"]).unwrap();
|
||||
assert_eq!(args.prepare().unwrap().target_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_labelled_target_syntax_is_rejected() {
|
||||
assert!(parse(&["=127.0.0.1:2112"]).is_err());
|
||||
assert!(parse(&["label="]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_target_option_is_rejected() {
|
||||
assert!(parse(&["--target", "eu=host.example"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pacing_option_accepts_supported_values() {
|
||||
assert_eq!(
|
||||
parse(&["--pacing", "staggered", "127.0.0.1:2112"])
|
||||
.unwrap()
|
||||
.pacing,
|
||||
GroupPacingArg::Staggered
|
||||
);
|
||||
assert_eq!(
|
||||
parse(&["--pacing", "burst", "127.0.0.1:2112"])
|
||||
.unwrap()
|
||||
.pacing,
|
||||
GroupPacingArg::Burst
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_client_options_match_tui_config_mapping() {
|
||||
let shared = [
|
||||
"--interval",
|
||||
"250ms",
|
||||
"--length",
|
||||
"128",
|
||||
"--hmac",
|
||||
"secret",
|
||||
"--clock",
|
||||
"monotonic",
|
||||
"--tstamp",
|
||||
"receive",
|
||||
"--stats",
|
||||
"count",
|
||||
"--sfill",
|
||||
"abc",
|
||||
"--dscp",
|
||||
"46",
|
||||
"--ttl",
|
||||
"64",
|
||||
"--loose",
|
||||
];
|
||||
let tui_args = [
|
||||
"--duration",
|
||||
"30s",
|
||||
"--interval",
|
||||
"250ms",
|
||||
"--length",
|
||||
"128",
|
||||
"--hmac",
|
||||
"secret",
|
||||
"--clock",
|
||||
"monotonic",
|
||||
"--tstamp",
|
||||
"receive",
|
||||
"--stats",
|
||||
"count",
|
||||
"--sfill",
|
||||
"abc",
|
||||
"--dscp",
|
||||
"46",
|
||||
"--ttl",
|
||||
"64",
|
||||
"--loose",
|
||||
"127.0.0.1:2112",
|
||||
];
|
||||
let tui = parse(&tui_args).unwrap().prepare().unwrap().client;
|
||||
let common = parse_shared(&shared).unwrap().common;
|
||||
let shared = common.to_client_config(Duration::from_secs(30));
|
||||
|
||||
assert_eq!(shared.duration, tui.duration);
|
||||
assert_eq!(shared.interval, tui.interval);
|
||||
assert_eq!(shared.length, tui.length);
|
||||
assert_eq!(shared.received_stats, tui.received_stats);
|
||||
assert_eq!(shared.stamp_at, tui.stamp_at);
|
||||
assert_eq!(shared.clock, tui.clock);
|
||||
assert_eq!(shared.dscp, tui.dscp);
|
||||
assert_eq!(shared.hmac_key, tui.hmac_key);
|
||||
assert_eq!(shared.server_fill, tui.server_fill);
|
||||
assert_eq!(shared.negotiation_policy, tui.negotiation_policy);
|
||||
assert_eq!(shared.socket_config.ttl, tui.socket_config.ttl);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_hmac_key_is_rejected_via_the_shared_args() {
|
||||
// CommonClientArgs is flattened into both the client and TUI parsers,
|
||||
// so proving rejection once here covers both without duplicating it.
|
||||
let err = parse(&["--hmac", "", "127.0.0.1:2112"]).unwrap_err();
|
||||
assert!(err.to_string().contains("HMAC key must not be empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_help_lists_multi_target_options() {
|
||||
let help = TuiArgs::command().render_help().to_string();
|
||||
assert!(!help.contains("--target "));
|
||||
assert!(help.contains("--pacing <PACING>"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod args;
|
||||
pub mod run;
|
||||
mod ui;
|
||||
|
||||
pub use args::TuiArgs;
|
||||
pub use run::run_tui;
|
||||
@@ -0,0 +1,303 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
io,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use super::ui::{should_render, TuiConfig, TuiState, TuiStatus, TuiTerminal};
|
||||
use crate::{
|
||||
cmd::tui::args::TuiArgs,
|
||||
shared::client::{
|
||||
is_shutdown_requested,
|
||||
session::{
|
||||
drain_managed_events, peer_close_run_error, request_managed_stop_for_peer_close,
|
||||
request_managed_stop_once, ManagedDrainState,
|
||||
},
|
||||
},
|
||||
};
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use irtt_client::managed::{
|
||||
BlockingManagedClient, ManagedEndReason, ManagedEvent, ManagedEventSubscription,
|
||||
ManagedEventTryRecvError, TargetInstance,
|
||||
};
|
||||
|
||||
const RENDER_INTERVAL: Duration = Duration::from_millis(250);
|
||||
const TUI_WAIT_SLICE: Duration = Duration::from_millis(20);
|
||||
const INPUT_EVENT_WORK_BUDGET: usize = 128;
|
||||
|
||||
pub fn run_tui(
|
||||
args: TuiArgs,
|
||||
shutdown_requested: &AtomicBool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let setup = args
|
||||
.prepare()
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||
let continuous = args.is_continuous();
|
||||
let mut terminal = TuiTerminal::enter()?;
|
||||
let mut state = TuiState::with_target_labels(
|
||||
TuiConfig::from_args(&args, &setup),
|
||||
setup.targets.iter().map(|target| target.label.clone()),
|
||||
);
|
||||
let mut next_render = Instant::now();
|
||||
if is_shutdown_requested(shutdown_requested) {
|
||||
return Ok(());
|
||||
}
|
||||
state.set_status(TuiStatus::Opening);
|
||||
render_if_due(&mut terminal, &state, &mut next_render, true)?;
|
||||
let (owner, mut events) = match BlockingManagedClient::start_with_subscription(
|
||||
setup.managed_config(),
|
||||
setup.managed_targets(),
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
state.set_error(error.to_string());
|
||||
render_if_due(&mut terminal, &state, &mut next_render, true)?;
|
||||
return Err(Box::new(error));
|
||||
}
|
||||
};
|
||||
let handle = owner.handle();
|
||||
let mut interrupted = false;
|
||||
let mut stop_requested = false;
|
||||
let mut terminal_targets = HashSet::new();
|
||||
let mut dropped_events = 0;
|
||||
let mut subscription_closed = false;
|
||||
loop {
|
||||
if is_shutdown_requested(shutdown_requested) {
|
||||
interrupted = true;
|
||||
if request_managed_stop_once(&mut stop_requested) {
|
||||
drop(handle.stop());
|
||||
}
|
||||
}
|
||||
if handle_input(&mut state, shutdown_requested)? {
|
||||
render_if_due(&mut terminal, &state, &mut next_render, true)?;
|
||||
}
|
||||
if state.quit_requested {
|
||||
interrupted = true;
|
||||
if request_managed_stop_once(&mut stop_requested) {
|
||||
drop(handle.stop());
|
||||
}
|
||||
}
|
||||
if request_managed_stop_for_peer_close(
|
||||
continuous,
|
||||
interrupted,
|
||||
handle.status().peer_closed_target_outcomes,
|
||||
&mut stop_requested,
|
||||
) {
|
||||
drop(handle.stop());
|
||||
}
|
||||
let drain_state = drain_tui_events(
|
||||
&mut events,
|
||||
&mut state,
|
||||
&mut terminal_targets,
|
||||
&mut dropped_events,
|
||||
);
|
||||
match drain_state {
|
||||
ManagedDrainState::Empty => {
|
||||
thread::sleep(managed_tui_wait_duration(&next_render, state.paused));
|
||||
}
|
||||
ManagedDrainState::Closed => subscription_closed = true,
|
||||
ManagedDrainState::BudgetExhausted => {}
|
||||
}
|
||||
if handle.status().final_outcome.is_some() || subscription_closed {
|
||||
break;
|
||||
}
|
||||
render_if_due(&mut terminal, &state, &mut next_render, false)?;
|
||||
}
|
||||
if interrupted {
|
||||
state.set_status(TuiStatus::Interrupted);
|
||||
render_if_due(&mut terminal, &state, &mut next_render, true)?;
|
||||
}
|
||||
state.set_status(TuiStatus::Closing);
|
||||
render_if_due(&mut terminal, &state, &mut next_render, true)?;
|
||||
let outcome = owner.join()?;
|
||||
drain_final_tui_events(
|
||||
&mut events,
|
||||
&mut state,
|
||||
&mut terminal_targets,
|
||||
&mut dropped_events,
|
||||
);
|
||||
state.mark_dropped_managed_events(dropped_events);
|
||||
if outcome.discarded_target_outcomes != 0 {
|
||||
state.set_run_error(format!(
|
||||
"{} final target outcomes were discarded",
|
||||
outcome.discarded_target_outcomes
|
||||
));
|
||||
}
|
||||
for target in outcome.recent_target_outcomes.iter() {
|
||||
if terminal_targets.insert(target.target.clone()) {
|
||||
state.process_target_outcome(target);
|
||||
}
|
||||
}
|
||||
interrupted |= is_shutdown_requested(shutdown_requested);
|
||||
let error = match &outcome.end_reason {
|
||||
ManagedEndReason::DriverFailed(failure) => {
|
||||
Some(format!("managed driver failed: {failure}"))
|
||||
}
|
||||
_ => peer_close_run_error(continuous, interrupted, outcome.peer_closed_target_outcomes)
|
||||
.or_else(|| {
|
||||
(!interrupted
|
||||
&& outcome.successful_target_outcomes == 0
|
||||
&& outcome.failed_target_outcomes > 0)
|
||||
.then(|| {
|
||||
format!(
|
||||
"no managed target completed successfully ({} failed)",
|
||||
outcome.failed_target_outcomes
|
||||
)
|
||||
})
|
||||
}),
|
||||
};
|
||||
if let Some(error) = error {
|
||||
state.set_run_error(error.clone());
|
||||
render_if_due(&mut terminal, &state, &mut next_render, true)?;
|
||||
return Err(error.into());
|
||||
}
|
||||
state.set_status(TuiStatus::Complete);
|
||||
render_if_due(&mut terminal, &state, &mut next_render, true)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_tui_event(
|
||||
event: ManagedEvent,
|
||||
state: &mut TuiState,
|
||||
terminal_targets: &mut HashSet<TargetInstance>,
|
||||
) {
|
||||
match event {
|
||||
ManagedEvent::Client { target, event } => state.process_target_event(&target, &event),
|
||||
ManagedEvent::TargetFinished { outcome } => {
|
||||
terminal_targets.insert(outcome.target.clone());
|
||||
state.process_target_outcome(&outcome);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn drain_tui_events(
|
||||
events: &mut ManagedEventSubscription,
|
||||
state: &mut TuiState,
|
||||
terminal_targets: &mut HashSet<TargetInstance>,
|
||||
dropped_events: &mut u64,
|
||||
) -> ManagedDrainState {
|
||||
drain_managed_events(events, dropped_events, |event| {
|
||||
process_tui_event(event, state, terminal_targets);
|
||||
Ok::<(), std::convert::Infallible>(())
|
||||
})
|
||||
.expect("processing TUI managed events is infallible")
|
||||
}
|
||||
|
||||
fn drain_final_tui_events(
|
||||
events: &mut ManagedEventSubscription,
|
||||
state: &mut TuiState,
|
||||
terminal_targets: &mut HashSet<TargetInstance>,
|
||||
dropped_events: &mut u64,
|
||||
) {
|
||||
loop {
|
||||
match events.try_recv() {
|
||||
Ok(event) => process_tui_event(event, state, terminal_targets),
|
||||
Err(ManagedEventTryRecvError::Empty | ManagedEventTryRecvError::Closed) => break,
|
||||
Err(ManagedEventTryRecvError::Lagged(count)) => {
|
||||
*dropped_events = dropped_events.saturating_add(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn handle_input(state: &mut TuiState, shutdown_requested: &AtomicBool) -> io::Result<bool> {
|
||||
let mut force_render = false;
|
||||
for _ in 0..INPUT_EVENT_WORK_BUDGET {
|
||||
if !event::poll(Duration::ZERO)? {
|
||||
break;
|
||||
}
|
||||
let Event::Key(key) = event::read()? else {
|
||||
continue;
|
||||
};
|
||||
if key.kind == KeyEventKind::Release {
|
||||
continue;
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Char('c')
|
||||
if key.code == KeyCode::Char('q')
|
||||
|| key.modifiers.contains(KeyModifiers::CONTROL) =>
|
||||
{
|
||||
state.quit_requested = true;
|
||||
shutdown_requested.store(true, Ordering::Relaxed);
|
||||
force_render = true;
|
||||
break;
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
state.clear_visible_history();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
state.toggle_pause();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::Char('g') => {
|
||||
state.toggle_view();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
state.cycle_graph_metric();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
state.pan_graph_left();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
state.pan_graph_right();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
state.pan_graph_page_left();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
state.pan_graph_page_right();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::Home => {
|
||||
state.jump_graph_oldest();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::End => {
|
||||
state.jump_graph_live();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::Char('+') | KeyCode::Char('=') => {
|
||||
state.zoom_graph_in();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::Char('-') => {
|
||||
state.zoom_graph_out();
|
||||
force_render = true;
|
||||
}
|
||||
KeyCode::Char('0') => {
|
||||
state.reset_graph_window();
|
||||
force_render = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(force_render)
|
||||
}
|
||||
fn render_if_due(
|
||||
terminal: &mut TuiTerminal,
|
||||
state: &TuiState,
|
||||
next_render: &mut Instant,
|
||||
force: bool,
|
||||
) -> io::Result<()> {
|
||||
let now = Instant::now();
|
||||
if should_render(now, *next_render, state.paused, force) {
|
||||
terminal.draw(state)?;
|
||||
*next_render = now + RENDER_INTERVAL;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn managed_tui_wait_duration(next_render: &Instant, paused: bool) -> Duration {
|
||||
let render_wait = if paused {
|
||||
TUI_WAIT_SLICE
|
||||
} else {
|
||||
next_render.saturating_duration_since(Instant::now())
|
||||
};
|
||||
render_wait.min(TUI_WAIT_SLICE)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user