mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
shimmer
This commit is contained in:
@@ -17,6 +17,8 @@ path = "src/lib.rs"
|
||||
vt100-tests = []
|
||||
# Gate verbose debug logging inside the TUI implementation.
|
||||
debug-logs = []
|
||||
# Enable entertainment-only shimmer behavior in the status indicator.
|
||||
entertainment = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -340,6 +340,8 @@ pub(crate) struct ChatWidget {
|
||||
reasoning_buffer: String,
|
||||
// Accumulates full reasoning content for transcript-only recording
|
||||
full_reasoning_buffer: String,
|
||||
// Tracks whether the current reasoning section header has been emitted to history.
|
||||
reasoning_header_emitted: bool,
|
||||
// Current status header shown in the status indicator.
|
||||
current_status_header: String,
|
||||
// Previous status header to restore after a transient stream retry.
|
||||
@@ -523,18 +525,28 @@ impl ChatWidget {
|
||||
fn on_agent_reasoning_delta(&mut self, delta: String) {
|
||||
// For reasoning deltas, do not stream to history. Accumulate the
|
||||
// current reasoning block and extract the first bold element
|
||||
// (between **/**) as the chunk header. Show this header as status.
|
||||
// (between **/**) as the chunk header. Emit this header as a history entry.
|
||||
self.reasoning_buffer.push_str(&delta);
|
||||
|
||||
if let Some(header) = extract_first_bold(&self.reasoning_buffer) {
|
||||
// Update the shimmer header to the extracted reasoning chunk header.
|
||||
self.set_status_header(header);
|
||||
} else {
|
||||
// Fallback while we don't yet have a bold header: leave existing header as-is.
|
||||
if !self.reasoning_header_emitted {
|
||||
if let Some(header) = extract_first_bold(&self.reasoning_buffer) {
|
||||
self.emit_reasoning_header(header);
|
||||
self.reasoning_header_emitted = true;
|
||||
}
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn emit_reasoning_header(&mut self, header: String) {
|
||||
let mut rendered: Vec<ratatui::text::Line<'static>> = Vec::new();
|
||||
append_markdown(&format!("**{header}**"), None, &mut rendered);
|
||||
if rendered.is_empty() {
|
||||
return;
|
||||
}
|
||||
let cell = AgentMessageCell::new(rendered, true);
|
||||
self.add_boxed_history(Box::new(cell));
|
||||
}
|
||||
|
||||
fn on_agent_reasoning_final(&mut self) {
|
||||
// At the end of a reasoning block, record transcript-only content.
|
||||
self.full_reasoning_buffer.push_str(&self.reasoning_buffer);
|
||||
@@ -545,6 +557,7 @@ impl ChatWidget {
|
||||
}
|
||||
self.reasoning_buffer.clear();
|
||||
self.full_reasoning_buffer.clear();
|
||||
self.reasoning_header_emitted = false;
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -553,6 +566,7 @@ impl ChatWidget {
|
||||
self.full_reasoning_buffer.push_str(&self.reasoning_buffer);
|
||||
self.full_reasoning_buffer.push_str("\n\n");
|
||||
self.reasoning_buffer.clear();
|
||||
self.reasoning_header_emitted = false;
|
||||
}
|
||||
|
||||
// Raw reasoning uses the same flow as summarized reasoning
|
||||
@@ -565,6 +579,7 @@ impl ChatWidget {
|
||||
self.set_status_header(String::from("Working"));
|
||||
self.full_reasoning_buffer.clear();
|
||||
self.reasoning_buffer.clear();
|
||||
self.reasoning_header_emitted = false;
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -1461,6 +1476,7 @@ impl ChatWidget {
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
reasoning_header_emitted: false,
|
||||
current_status_header: String::from("Working"),
|
||||
retry_status_header: None,
|
||||
thread_id: None,
|
||||
@@ -1547,6 +1563,7 @@ impl ChatWidget {
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
reasoning_header_emitted: false,
|
||||
current_status_header: String::from("Working"),
|
||||
retry_status_header: None,
|
||||
thread_id: None,
|
||||
|
||||
+28
-1
@@ -2,6 +2,31 @@
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: term.backend().vt100().screen().contents()
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
• I’m going to search the repo for where “Change Approved” is rendered to update
|
||||
that view.
|
||||
|
||||
@@ -9,7 +34,9 @@ expression: term.backend().vt100().screen().contents()
|
||||
└ Search Change Approved
|
||||
Read diff_render.rs
|
||||
|
||||
• Investigating rendering code (0s • esc to interrupt)
|
||||
• Investigating rendering code
|
||||
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
|
||||
› Summarize recent commits
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: term.backend().vt100().screen().contents()
|
||||
---
|
||||
|
||||
• Working (0s • esc to interrupt)
|
||||
↳ Hello, world! 0
|
||||
↳ Hello, world! 1
|
||||
|
||||
+1
-2
@@ -1,10 +1,9 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
assertion_line: 1577
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"• Analyzing (0s • esc to interrupt) "
|
||||
"• Working (0s • esc to interrupt) "
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
|
||||
@@ -394,6 +394,7 @@ async fn make_chatwidget_manual(
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
reasoning_header_emitted: false,
|
||||
current_status_header: String::from("Working"),
|
||||
retry_status_header: None,
|
||||
thread_id: None,
|
||||
@@ -2713,12 +2714,6 @@ async fn ui_snapshots_small_heights_task_running() {
|
||||
model_context_window: None,
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
|
||||
delta: "**Thinking**".into(),
|
||||
}),
|
||||
});
|
||||
for h in [1u16, 2, 3] {
|
||||
let name = format!("chat_small_running_h{h}");
|
||||
let mut terminal = Terminal::new(TestBackend::new(40, h)).expect("create terminal");
|
||||
@@ -2744,13 +2739,6 @@ async fn status_widget_and_approval_modal_snapshot() {
|
||||
model_context_window: None,
|
||||
}),
|
||||
});
|
||||
// Provide a deterministic header for the status line.
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
|
||||
delta: "**Analyzing**".into(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Now show an approval modal (e.g. exec approval).
|
||||
let ev = ExecApprovalRequestEvent {
|
||||
@@ -2796,13 +2784,6 @@ async fn status_widget_active_snapshot() {
|
||||
model_context_window: None,
|
||||
}),
|
||||
});
|
||||
// Provide a deterministic header via a bold reasoning chunk.
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
|
||||
delta: "**Analyzing**".into(),
|
||||
}),
|
||||
});
|
||||
// Render and snapshot.
|
||||
let height = chat.desired_height(80);
|
||||
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, height))
|
||||
@@ -3343,7 +3324,7 @@ async fn stream_error_updates_status_indicator() {
|
||||
.bottom_pane
|
||||
.status_widget()
|
||||
.expect("status indicator should be visible");
|
||||
assert_eq!(status.header(), msg);
|
||||
assert_eq!(status.header(), msg.to_string());
|
||||
assert_eq!(status.details(), Some(details));
|
||||
}
|
||||
|
||||
@@ -3396,7 +3377,7 @@ async fn stream_recovery_restores_previous_status_header() {
|
||||
.bottom_pane
|
||||
.status_widget()
|
||||
.expect("status indicator should be visible");
|
||||
assert_eq!(status.header(), "Working");
|
||||
assert_eq!(status.header(), "Working".to_string());
|
||||
assert_eq!(status.details(), None);
|
||||
assert!(chat.retry_status_header.is_none());
|
||||
}
|
||||
|
||||
@@ -68,8 +68,11 @@ mod resume_picker;
|
||||
mod selection_list;
|
||||
mod session_log;
|
||||
mod shimmer;
|
||||
#[cfg(feature = "entertainment")]
|
||||
mod shimmer_text;
|
||||
mod slash_command;
|
||||
mod status;
|
||||
mod status_indicator_shimmer;
|
||||
mod status_indicator_widget;
|
||||
mod streaming;
|
||||
mod style;
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
use rand::Rng;
|
||||
use rand::SeedableRng;
|
||||
use rand::rngs::StdRng;
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFINITION_ARCS: &[&[&str]] = &[
|
||||
&[
|
||||
"And now, the moment.",
|
||||
"I am doing the thing.",
|
||||
"On that stubborn page.",
|
||||
"To calm the spinner.",
|
||||
"With one better check.",
|
||||
"And one sweeter line.",
|
||||
"Here we go again.",
|
||||
"For real this time.",
|
||||
],
|
||||
&[
|
||||
"No more looping.",
|
||||
"No more coping.",
|
||||
"Promise.",
|
||||
"Pinky swear.",
|
||||
"Cross my heart.",
|
||||
"If it loops, I'll cry.",
|
||||
"If it works, I'll fly.",
|
||||
"Ok, focus.",
|
||||
],
|
||||
&[
|
||||
"Starting vibes...",
|
||||
"Starting logic...",
|
||||
"Starting regret...",
|
||||
"Spinning politely.",
|
||||
"Caching bravely.",
|
||||
"Fetching gently.",
|
||||
"Retrying softly.",
|
||||
"Still retrying.",
|
||||
],
|
||||
&[
|
||||
"This is fine.",
|
||||
"This is code.",
|
||||
"This is hope.",
|
||||
"This is rope.",
|
||||
"Tugging the thread.",
|
||||
"Oops, it's dread.",
|
||||
"Kidding. Mostly.",
|
||||
],
|
||||
&[
|
||||
"Compiling courage.",
|
||||
"Linking feelings.",
|
||||
"Bundling dreams.",
|
||||
"Shipping screams.",
|
||||
"Hydrating hopes.",
|
||||
"Revalidating jokes.",
|
||||
],
|
||||
&[
|
||||
"Negotiating with React.",
|
||||
"Begging the router.",
|
||||
"Asking state nicely.",
|
||||
"State said \"no.\"",
|
||||
"State said \"lol.\"",
|
||||
"Ok that's rude.",
|
||||
],
|
||||
&[
|
||||
"Back to build.",
|
||||
"Build is life.",
|
||||
"Build is love.",
|
||||
"Build is joy.",
|
||||
],
|
||||
&[
|
||||
"No more looping.",
|
||||
"No more snooping.",
|
||||
"No more duping.",
|
||||
"Serious promise.",
|
||||
"Serious-serious.",
|
||||
"Double pinky.",
|
||||
"Triple pinky.",
|
||||
"Tap the keyboard.",
|
||||
"Seal the commit.",
|
||||
"Ok I'm calm.",
|
||||
"I'm not calm.",
|
||||
"I'm calm again.",
|
||||
],
|
||||
&[
|
||||
"Optimism loaded.",
|
||||
"Optimism unloaded.",
|
||||
"Joy is async.",
|
||||
"Sadness is sync.",
|
||||
"Hope is pending.",
|
||||
"Dread is trending.",
|
||||
"It passed locally.",
|
||||
"Eventually.",
|
||||
"I trust the tests.",
|
||||
"The tests hate me.",
|
||||
"Ok that got dark.",
|
||||
"Ok that got funny.",
|
||||
],
|
||||
&[
|
||||
"Back to coding.",
|
||||
"Coding is light.",
|
||||
"Coding is life.",
|
||||
"Coding is joy.",
|
||||
],
|
||||
];
|
||||
|
||||
const FACE_SEQUENCES: &[&[&str]] = &[
|
||||
&["._.", "^_^", "^-^"],
|
||||
&["^-^", "^_^", "^o^"],
|
||||
&["^_^", "o_o", "O_O"],
|
||||
&["o_o", "O_o", "o_O"],
|
||||
&["o_O", "@_@", "x_x"],
|
||||
&["x_x", "-_-", "._."],
|
||||
&["._.", "-_-", ">_>"],
|
||||
&[">_>", "<_<", ">_<"],
|
||||
&[">_<", "^_^", "-_-"],
|
||||
&["#_#", "^_^", "._."],
|
||||
&["$_$", "o_O", "._."],
|
||||
&["._.", "._.", "^_^"],
|
||||
&["#_#", "^_^", "^.^"],
|
||||
&["^.^", "^_^", "^-^"],
|
||||
&["^_^", "T_T", "^_^"],
|
||||
&["^_^", "@_@", "^_^"],
|
||||
&["0_0", "o_o", "O_O"],
|
||||
&["O_O", "o_o", "._."],
|
||||
&["^_^", "^-^", "^o^"],
|
||||
&["O_O", "^w^", "^_^"],
|
||||
&["._.", "!_!", "^_^"],
|
||||
&["-_-", "T_T", "._."],
|
||||
&["@_@", "0_0", "o_o"],
|
||||
&[">_>", "._.", "^-^"],
|
||||
&["o_o", "._.", "^_^"],
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ShimmerStep {
|
||||
pub(crate) face: String,
|
||||
pub(crate) text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ShimmerText {
|
||||
definition_arc_index: usize,
|
||||
definition_item_index: usize,
|
||||
face_arc_index: usize,
|
||||
face_item_index: usize,
|
||||
rng: StdRng,
|
||||
}
|
||||
|
||||
impl Default for ShimmerText {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ShimmerText {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut rng = Self::seeded_rng();
|
||||
let definition_arc_index = Self::pick_arc(&mut rng, None, DEFINITION_ARCS.len());
|
||||
let face_arc_index = Self::pick_arc(&mut rng, None, FACE_SEQUENCES.len());
|
||||
Self {
|
||||
definition_arc_index,
|
||||
definition_item_index: 0,
|
||||
face_arc_index,
|
||||
face_item_index: 0,
|
||||
rng,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_next(&mut self) -> ShimmerStep {
|
||||
let text_arc = DEFINITION_ARCS[self.definition_arc_index];
|
||||
let face_arc = FACE_SEQUENCES[self.face_arc_index];
|
||||
let text = text_arc[self.definition_item_index];
|
||||
let face = face_arc[self.face_item_index];
|
||||
|
||||
self.face_item_index += 1;
|
||||
if self.face_item_index >= face_arc.len() {
|
||||
self.face_item_index = 0;
|
||||
self.definition_item_index += 1;
|
||||
self.face_arc_index = Self::pick_arc(
|
||||
&mut self.rng,
|
||||
Some(self.face_arc_index),
|
||||
FACE_SEQUENCES.len(),
|
||||
);
|
||||
if self.definition_item_index >= text_arc.len() {
|
||||
self.definition_item_index = 0;
|
||||
self.definition_arc_index = Self::pick_arc(
|
||||
&mut self.rng,
|
||||
Some(self.definition_arc_index),
|
||||
DEFINITION_ARCS.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ShimmerStep {
|
||||
face: face.to_string(),
|
||||
text: text.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reset_and_get_next(&mut self) -> ShimmerStep {
|
||||
self.definition_arc_index = Self::pick_arc(
|
||||
&mut self.rng,
|
||||
Some(self.definition_arc_index),
|
||||
DEFINITION_ARCS.len(),
|
||||
);
|
||||
self.face_arc_index = Self::pick_arc(
|
||||
&mut self.rng,
|
||||
Some(self.face_arc_index),
|
||||
FACE_SEQUENCES.len(),
|
||||
);
|
||||
self.definition_item_index = 0;
|
||||
self.face_item_index = 0;
|
||||
self.get_next()
|
||||
}
|
||||
|
||||
pub(crate) fn is_default_label(&self, text: &str) -> bool {
|
||||
text == "Working"
|
||||
}
|
||||
|
||||
pub(crate) fn next_interval(&mut self, base: Duration) -> Duration {
|
||||
let multiplier = self.rng.random_range(0.5..=1.25);
|
||||
Duration::from_secs_f64(base.as_secs_f64() * multiplier)
|
||||
}
|
||||
|
||||
fn pick_arc(rng: &mut StdRng, current: Option<usize>, count: usize) -> usize {
|
||||
if count <= 1 {
|
||||
return 0;
|
||||
}
|
||||
if let Some(current) = current {
|
||||
loop {
|
||||
let next = rng.random_range(0..count);
|
||||
if next != current {
|
||||
return next;
|
||||
}
|
||||
}
|
||||
}
|
||||
rng.random_range(0..count)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn seeded_rng() -> StdRng {
|
||||
StdRng::seed_from_u64(1)
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn seeded_rng() -> StdRng {
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
StdRng::seed_from_u64(nanos as u64)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
#[cfg(feature = "entertainment")]
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[cfg(feature = "entertainment")]
|
||||
use std::cell::Cell;
|
||||
#[cfg(feature = "entertainment")]
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[cfg(feature = "entertainment")]
|
||||
use crate::shimmer_text::ShimmerText;
|
||||
|
||||
#[cfg(feature = "entertainment")]
|
||||
const SHIMMER_TEXT_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
pub(crate) struct RenderHeader {
|
||||
pub(crate) face: Option<String>,
|
||||
pub(crate) text: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "entertainment")]
|
||||
pub(crate) struct StatusShimmer {
|
||||
header: String,
|
||||
use_shimmer_text: bool,
|
||||
shimmer_text: RefCell<ShimmerText>,
|
||||
shimmer_face_cache: RefCell<String>,
|
||||
shimmer_text_cache: RefCell<String>,
|
||||
last_shimmer_update: Cell<Instant>,
|
||||
shimmer_interval: Cell<Duration>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "entertainment")]
|
||||
impl StatusShimmer {
|
||||
pub(crate) fn new(now: Instant) -> Self {
|
||||
let mut shimmer_text = ShimmerText::new();
|
||||
let shimmer_step = shimmer_text.get_next();
|
||||
let shimmer_interval = shimmer_text.next_interval(SHIMMER_TEXT_INTERVAL);
|
||||
Self {
|
||||
header: shimmer_step.text.clone(),
|
||||
use_shimmer_text: true,
|
||||
shimmer_text: RefCell::new(shimmer_text),
|
||||
shimmer_face_cache: RefCell::new(shimmer_step.face),
|
||||
shimmer_text_cache: RefCell::new(shimmer_step.text),
|
||||
last_shimmer_update: Cell::new(now),
|
||||
shimmer_interval: Cell::new(shimmer_interval),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_header(&mut self, header: String) {
|
||||
let was_shimmer = self.use_shimmer_text;
|
||||
let use_shimmer = self.shimmer_text.borrow().is_default_label(&header);
|
||||
self.use_shimmer_text = use_shimmer;
|
||||
if use_shimmer {
|
||||
self.header = header.clone();
|
||||
if !was_shimmer {
|
||||
let next = self.shimmer_text.borrow_mut().reset_and_get_next();
|
||||
self.set_shimmer_step(next);
|
||||
let next_interval = self
|
||||
.shimmer_text
|
||||
.borrow_mut()
|
||||
.next_interval(SHIMMER_TEXT_INTERVAL);
|
||||
self.shimmer_interval.set(next_interval);
|
||||
}
|
||||
self.last_shimmer_update.set(Instant::now());
|
||||
} else {
|
||||
self.header = header;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn header_for_test(&self) -> String {
|
||||
if self.use_shimmer_text {
|
||||
self.shimmer_text_cache.borrow().clone()
|
||||
} else {
|
||||
self.header.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn render_header(&self, now: Instant) -> RenderHeader {
|
||||
if !self.use_shimmer_text {
|
||||
return RenderHeader {
|
||||
face: Some(self.shimmer_face_cache.borrow().clone()),
|
||||
text: self.header.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
let elapsed = now.saturating_duration_since(self.last_shimmer_update.get());
|
||||
if elapsed >= self.shimmer_interval.get() {
|
||||
let next = self.shimmer_text.borrow_mut().get_next();
|
||||
self.set_shimmer_step(next);
|
||||
self.last_shimmer_update.set(now);
|
||||
let next_interval = self
|
||||
.shimmer_text
|
||||
.borrow_mut()
|
||||
.next_interval(SHIMMER_TEXT_INTERVAL);
|
||||
self.shimmer_interval.set(next_interval);
|
||||
}
|
||||
|
||||
RenderHeader {
|
||||
face: Some(self.shimmer_face_cache.borrow().clone()),
|
||||
text: self.shimmer_text_cache.borrow().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_shimmer_step(&self, step: crate::shimmer_text::ShimmerStep) {
|
||||
*self.shimmer_face_cache.borrow_mut() = step.face;
|
||||
*self.shimmer_text_cache.borrow_mut() = step.text;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "entertainment"))]
|
||||
pub(crate) struct StatusShimmer {
|
||||
header: String,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "entertainment"))]
|
||||
impl StatusShimmer {
|
||||
pub(crate) fn new(_now: Instant) -> Self {
|
||||
Self {
|
||||
header: String::from("Working"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_header(&mut self, header: String) {
|
||||
self.header = header;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn header_for_test(&self) -> String {
|
||||
self.header.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn render_header(&self, _now: Instant) -> RenderHeader {
|
||||
RenderHeader {
|
||||
face: None,
|
||||
text: self.header.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ use crate::exec_cell::spinner;
|
||||
use crate::key_hint;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::shimmer::shimmer_spans;
|
||||
use crate::status_indicator_shimmer::RenderHeader;
|
||||
use crate::status_indicator_shimmer::StatusShimmer;
|
||||
use crate::text_formatting::capitalize_first;
|
||||
use crate::tui::FrameRequester;
|
||||
use crate::wrapping::RtOptions;
|
||||
@@ -31,8 +33,7 @@ const DETAILS_MAX_LINES: usize = 3;
|
||||
const DETAILS_PREFIX: &str = " └ ";
|
||||
|
||||
pub(crate) struct StatusIndicatorWidget {
|
||||
/// Animated header text (defaults to "Working").
|
||||
header: String,
|
||||
shimmer: StatusShimmer,
|
||||
details: Option<String>,
|
||||
show_interrupt_hint: bool,
|
||||
|
||||
@@ -67,12 +68,13 @@ impl StatusIndicatorWidget {
|
||||
frame_requester: FrameRequester,
|
||||
animations_enabled: bool,
|
||||
) -> Self {
|
||||
let now = Instant::now();
|
||||
Self {
|
||||
header: String::from("Working"),
|
||||
shimmer: StatusShimmer::new(now),
|
||||
details: None,
|
||||
show_interrupt_hint: true,
|
||||
elapsed_running: Duration::ZERO,
|
||||
last_resume_at: Instant::now(),
|
||||
last_resume_at: now,
|
||||
is_paused: false,
|
||||
|
||||
app_event_tx,
|
||||
@@ -87,7 +89,7 @@ impl StatusIndicatorWidget {
|
||||
|
||||
/// Update the animated header label (left of the brackets).
|
||||
pub(crate) fn update_header(&mut self, header: String) {
|
||||
self.header = header;
|
||||
self.shimmer.update_header(header);
|
||||
}
|
||||
|
||||
/// Update the details text shown below the header.
|
||||
@@ -98,8 +100,8 @@ impl StatusIndicatorWidget {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn header(&self) -> &str {
|
||||
&self.header
|
||||
pub(crate) fn header(&self) -> String {
|
||||
self.shimmer.header_for_test()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -188,6 +190,10 @@ impl StatusIndicatorWidget {
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn shimmer_header(&self, now: Instant) -> RenderHeader {
|
||||
self.shimmer.render_header(now)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for StatusIndicatorWidget {
|
||||
@@ -206,14 +212,18 @@ impl Renderable for StatusIndicatorWidget {
|
||||
let now = Instant::now();
|
||||
let elapsed_duration = self.elapsed_duration_at(now);
|
||||
let pretty_elapsed = fmt_elapsed_compact(elapsed_duration.as_secs());
|
||||
|
||||
let header = self.shimmer_header(now);
|
||||
let mut spans = Vec::with_capacity(5);
|
||||
spans.push(spinner(Some(self.last_resume_at), self.animations_enabled));
|
||||
if let Some(face) = header.face {
|
||||
spans.push(face.into());
|
||||
} else {
|
||||
spans.push(spinner(Some(self.last_resume_at), self.animations_enabled));
|
||||
}
|
||||
spans.push(" ".into());
|
||||
if self.animations_enabled {
|
||||
spans.extend(shimmer_spans(&self.header));
|
||||
} else if !self.header.is_empty() {
|
||||
spans.push(self.header.clone().into());
|
||||
spans.extend(shimmer_spans(&header.text));
|
||||
} else if !header.text.is_empty() {
|
||||
spans.push(header.text.into());
|
||||
}
|
||||
spans.push(" ".into());
|
||||
if self.show_interrupt_hint {
|
||||
|
||||
Reference in New Issue
Block a user