Skip to content

Commit 638b56d

Browse files
ansasakiclaude
andcommitted
feat(keylimectl): add progress spinners and optional color output
Add animated progress spinners using indicatif for long-running operations (attestation polling, key derivation retry) and optional color output via console. Spinners auto-detect TTY on stderr and fall back to plain text when piped. Colors apply to stderr only, keeping stdout clean for machine consumption. New --color flag (auto|always|never) controls color output. The OutputHandler now supports start_wait() which returns an RAII WaitHandle for polling loops with live status updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b617dc6 commit 638b56d

11 files changed

Lines changed: 439 additions & 311 deletions

File tree

Cargo.lock

Lines changed: 51 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

keylimectl/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ bzip2 = "0.5"
4848
zstd = { version = "0.13", default-features = false }
4949
toml = "0.8"
5050
zeroize = "1"
51+
indicatif = "0.17"
52+
console = "0.15"
5153

5254
[lints.clippy]
5355
all = "deny"

keylimectl/src/commands/agent/add.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,10 @@ async fn poll_attestation_status(
589589
let timeout = std::time::Duration::from_secs(timeout_secs);
590590
let poll_interval = std::time::Duration::from_secs(2);
591591

592+
let wait_handle = output.start_wait(format!(
593+
"Waiting for attestation of agent {agent_id}..."
594+
));
595+
592596
loop {
593597
if start.elapsed() > timeout {
594598
return Err(CommandError::agent_operation_failed(
@@ -647,13 +651,22 @@ async fn poll_attestation_status(
647651
);
648652
}
649653
Some("PASS") => {
654+
drop(wait_handle);
650655
output.info(format!(
651656
"Agent {agent_id} attestation successful"
652657
));
653658
return Ok("PASS".to_string());
654659
}
655660
_ => {
656-
// PENDING or missing — keep polling
661+
// PENDING or missing — update spinner and keep polling
662+
let elapsed = start.elapsed().as_secs();
663+
let state_str = operational_state
664+
.as_deref()
665+
.unwrap_or("pending");
666+
wait_handle.set_message(format!(
667+
"Waiting for attestation of agent {agent_id} \
668+
({state_str}, {elapsed}s elapsed)"
669+
));
657670
debug!(
658671
"Agent attestation status: {:?}, operational_state: {:?}, waiting...",
659672
attestation_status, operational_state

keylimectl/src/commands/agent/attestation.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,11 @@ pub(super) async fn verify_key_derivation(
328328
let max_retries = 12;
329329
let base_interval = std::time::Duration::from_secs(1);
330330

331+
let wait_handle = output
332+
.start_wait("Verifying key derivation (attempt 1/12)");
333+
331334
for attempt in 0..max_retries {
332-
output.progress(format!(
335+
wait_handle.set_message(format!(
333336
"Verifying key derivation (attempt {}/{})",
334337
attempt + 1,
335338
max_retries
@@ -340,6 +343,7 @@ pub(super) async fn verify_key_derivation(
340343
.await
341344
{
342345
Ok(true) => {
346+
drop(wait_handle);
343347
output
344348
.info("Key derivation verification successful");
345349
return Ok(());
@@ -356,17 +360,17 @@ pub(super) async fn verify_key_derivation(
356360
),
357361
));
358362
}
359-
let wait = base_interval * 2u32.saturating_pow(
363+
let delay = base_interval * 2u32.saturating_pow(
360364
attempt.min(4) as u32,
361365
);
362366
debug!(
363367
"Key derivation not yet complete (attempt {}/{}), \
364368
retrying in {:?}",
365369
attempt + 1,
366370
max_retries,
367-
wait
371+
delay
368372
);
369-
tokio::time::sleep(wait).await;
373+
tokio::time::sleep(delay).await;
370374
}
371375
Err(e) => {
372376
// Network/protocol error — also retry
@@ -379,17 +383,17 @@ pub(super) async fn verify_key_derivation(
379383
),
380384
));
381385
}
382-
let wait = base_interval * 2u32.saturating_pow(
386+
let delay = base_interval * 2u32.saturating_pow(
383387
attempt.min(4) as u32,
384388
);
385389
debug!(
386390
"Verification request failed (attempt {}/{}): {e}, \
387391
retrying in {:?}",
388392
attempt + 1,
389393
max_retries,
390-
wait
394+
delay
391395
);
392-
tokio::time::sleep(wait).await;
396+
tokio::time::sleep(delay).await;
393397
}
394398
}
395399
}

keylimectl/src/commands/agent/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ mod tests {
354354

355355
/// Create a test output handler
356356
fn _create_test_output() -> OutputHandler {
357-
OutputHandler::new(crate::OutputFormat::Json, true) // Quiet mode for tests
357+
OutputHandler::new(crate::OutputFormat::Json, true, crate::ColorMode::Never) // Quiet mode for tests
358358
}
359359

360360
#[test]

keylimectl/src/commands/measured_boot.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ mod tests {
528528

529529
/// Create a test output handler
530530
fn create_test_output() -> OutputHandler {
531-
OutputHandler::new(crate::OutputFormat::Json, true) // Quiet mode for tests
531+
OutputHandler::new(crate::OutputFormat::Json, true, crate::ColorMode::Never) // Quiet mode for tests
532532
}
533533

534534
/// Create a test measured boot policy file

keylimectl/src/commands/policy/crud.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ mod tests {
383383

384384
/// Create a test output handler
385385
fn create_test_output() -> OutputHandler {
386-
OutputHandler::new(crate::OutputFormat::Json, true) // Quiet mode for tests
386+
OutputHandler::new(crate::OutputFormat::Json, true, crate::ColorMode::Never) // Quiet mode for tests
387387
}
388388

389389
/// Create a test runtime policy file

keylimectl/src/commands/verify/evidence.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ mod tests {
244244
let output = OutputHandler::new(
245245
crate::OutputFormat::Json,
246246
false,
247+
crate::ColorMode::Never,
247248
);
248249
let result =
249250
format_evidence_result(&response, &output)
@@ -274,6 +275,7 @@ mod tests {
274275
let output = OutputHandler::new(
275276
crate::OutputFormat::Json,
276277
false,
278+
crate::ColorMode::Never,
277279
);
278280
let result =
279281
format_evidence_result(&response, &output)

keylimectl/src/config_main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,7 @@ mod tests {
658658
timeout: None,
659659
verbose: 0,
660660
quiet: false,
661+
color: crate::ColorMode::Never,
661662
format: crate::OutputFormat::Json,
662663
command: Some(crate::Commands::Agent {
663664
action: crate::AgentAction::List {

0 commit comments

Comments
 (0)