mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
Enhance TUI with glitch animations and improved status handling
- Add glitch animation effects for assistant messages in chatwidget - Implement GlitchText widget with multi-phase animation states - Add status item deduplication for screenshots and system context - Improve history cell rendering with proper exec command display - Update browser page handling for screenshot optimization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
This commit is contained in:
@@ -432,11 +432,11 @@ impl Page {
|
||||
filt.setAttribute('width', '200%');
|
||||
filt.setAttribute('height', '200%');
|
||||
|
||||
const blur = createSvg('feGaussianBlur'); blur.setAttribute('in', 'SourceAlpha'); blur.setAttribute('stdDeviation', '1');
|
||||
const blur = createSvg('feGaussianBlur'); blur.setAttribute('in', 'SourceAlpha'); blur.setAttribute('stdDeviation', '1.5');
|
||||
const off = createSvg('feOffset'); off.setAttribute('dx', '0'); off.setAttribute('dy', '0');
|
||||
const ct = createSvg('feComponentTransfer'); ct.setAttribute('result', 'offsetblur');
|
||||
const fa = createSvg('feFuncA'); fa.setAttribute('type', 'linear'); fa.setAttribute('slope', '1'); ct.appendChild(fa);
|
||||
const flood = createSvg('feFlood'); flood.setAttribute('flood-color', '#000'); flood.setAttribute('flood-opacity', '0.3');
|
||||
const fa = createSvg('feFuncA'); fa.setAttribute('type', 'linear'); fa.setAttribute('slope', '0.35'); ct.appendChild(fa);
|
||||
const flood = createSvg('feFlood'); flood.setAttribute('flood-color', '#000'); flood.setAttribute('flood-opacity', '0.35');
|
||||
const comp = createSvg('feComposite'); comp.setAttribute('in2', 'offsetblur'); comp.setAttribute('operator', 'in');
|
||||
const merge = createSvg('feMerge');
|
||||
const m1 = createSvg('feMergeNode');
|
||||
|
||||
+18
-11
@@ -164,7 +164,7 @@ impl App<'_> {
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
let chat_widget = ChatWidget::new(
|
||||
let mut chat_widget = ChatWidget::new(
|
||||
config.clone(),
|
||||
app_event_tx.clone(),
|
||||
initial_prompt,
|
||||
@@ -172,6 +172,8 @@ impl App<'_> {
|
||||
enhanced_keys_supported,
|
||||
terminal_info.clone(),
|
||||
);
|
||||
// Check for initial animations after widget is created
|
||||
chat_widget.check_for_initial_animations();
|
||||
AppState::Chat {
|
||||
widget: Box::new(chat_widget),
|
||||
}
|
||||
@@ -380,16 +382,21 @@ impl App<'_> {
|
||||
|
||||
match command {
|
||||
SlashCommand::New => {
|
||||
// User accepted – switch to chat view.
|
||||
let new_widget = Box::new(ChatWidget::new(
|
||||
self.config.clone(),
|
||||
self.app_event_tx.clone(),
|
||||
None,
|
||||
Vec::new(),
|
||||
self.enhanced_keys_supported,
|
||||
self.terminal_info.clone(),
|
||||
));
|
||||
self.app_state = AppState::Chat { widget: new_widget };
|
||||
// Clear the current conversation and start fresh
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.new_conversation(self.enhanced_keys_supported);
|
||||
} else {
|
||||
// If we're not in chat state, create a new chat widget
|
||||
let new_widget = Box::new(ChatWidget::new(
|
||||
self.config.clone(),
|
||||
self.app_event_tx.clone(),
|
||||
None,
|
||||
Vec::new(),
|
||||
self.enhanced_keys_supported,
|
||||
self.terminal_info.clone(),
|
||||
));
|
||||
self.app_state = AppState::Chat { widget: new_widget };
|
||||
}
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
SlashCommand::Init => {
|
||||
|
||||
@@ -189,6 +189,13 @@ impl ChatWidget<'_> {
|
||||
|
||||
// Begin stream if not already active
|
||||
if !self.stream.is_write_cycle_active() {
|
||||
// Remove the loading cell if present when streaming starts
|
||||
if let Some(last_cell) = self.history_cells.last() {
|
||||
if last_cell.is_loading_cell() {
|
||||
self.history_cells.pop();
|
||||
}
|
||||
}
|
||||
|
||||
self.stream.begin(kind, &sink);
|
||||
}
|
||||
|
||||
@@ -214,6 +221,9 @@ impl ChatWidget<'_> {
|
||||
|
||||
/// Mark that the widget needs to be redrawn
|
||||
fn mark_needs_redraw(&mut self) {
|
||||
// Clean up fully faded cells before redraw
|
||||
self.history_cells.retain(|cell| !cell.should_remove());
|
||||
|
||||
// Send a redraw event to trigger UI update
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
@@ -602,11 +612,16 @@ impl ChatWidget<'_> {
|
||||
|
||||
// Add initial animated welcome message to history
|
||||
let mut history_cells: Vec<Box<dyn HistoryCell>> = Vec::new();
|
||||
history_cells.push(Box::new(history_cell::new_animated_welcome()));
|
||||
let welcome_cell = Box::new(history_cell::new_animated_welcome());
|
||||
tracing::info!("Adding AnimatedWelcomeCell to history");
|
||||
tracing::info!("AnimatedWelcomeCell is_animating: {}", welcome_cell.is_animating());
|
||||
tracing::info!("AnimatedWelcomeCell has_custom_render: {}", welcome_cell.has_custom_render());
|
||||
tracing::info!("AnimatedWelcomeCell desired_height: {}", welcome_cell.desired_height(80));
|
||||
history_cells.push(welcome_cell);
|
||||
|
||||
// Initialize image protocol for rendering screenshots
|
||||
|
||||
Self {
|
||||
let new_widget = Self {
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
codex_op_tx,
|
||||
bottom_pane: BottomPane::new(BottomPaneParams {
|
||||
@@ -649,9 +664,22 @@ impl ChatWidget<'_> {
|
||||
last_sparkline_update: std::cell::RefCell::new(std::time::Instant::now()),
|
||||
stream: crate::streaming::controller::StreamController::new(config.clone()),
|
||||
interrupts: interrupts::InterruptManager::new(),
|
||||
}
|
||||
};
|
||||
|
||||
// Note: Initial redraw needs to be triggered after widget is added to app_state
|
||||
tracing::info!("AnimatedWelcomeCell ready, needs initial redraw after app initialization");
|
||||
|
||||
new_widget
|
||||
}
|
||||
|
||||
/// Check if there are any animations and trigger redraw if needed
|
||||
pub fn check_for_initial_animations(&mut self) {
|
||||
if self.history_cells.iter().any(|cell| cell.is_animating()) {
|
||||
tracing::info!("Initial animation detected, triggering redraw");
|
||||
self.mark_needs_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// Format model name with proper capitalization (e.g., "gpt-4" -> "GPT-4")
|
||||
fn format_model_name(&self, model_name: &str) -> String {
|
||||
if model_name.to_lowercase().starts_with("gpt-") {
|
||||
@@ -808,6 +836,13 @@ impl ChatWidget<'_> {
|
||||
|
||||
// Store in memory for local rendering
|
||||
self.history_cells.push(Box::new(cell));
|
||||
|
||||
// Log animation cells
|
||||
let animation_count = self.history_cells.iter().filter(|c| c.is_animating()).count();
|
||||
if animation_count > 0 {
|
||||
tracing::info!("History has {} animating cells out of {} total", animation_count, self.history_cells.len());
|
||||
}
|
||||
|
||||
// Auto-follow if we're at or near the bottom (preserve position if scrolled up)
|
||||
self.autoscroll_if_near_bottom();
|
||||
// If user has scrolled up (scroll_offset > 0), don't change their position
|
||||
@@ -1068,6 +1103,25 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
if !original_text.is_empty() {
|
||||
// Check if this is the first user prompt to trigger fade-out animation
|
||||
let has_existing_user_prompts = self.history_cells.iter().any(|cell| {
|
||||
// Check if it's a user prompt by looking at display lines
|
||||
// This is a bit indirect but works with the trait-based system
|
||||
let lines = cell.display_lines();
|
||||
!lines.is_empty() && lines[0].spans.iter().any(|span|
|
||||
span.content == "user" || span.content.contains("user")
|
||||
)
|
||||
});
|
||||
|
||||
if !has_existing_user_prompts {
|
||||
// This is the first user prompt - trigger fade-out on AnimatedWelcomeCell
|
||||
tracing::info!("First user message detected - triggering welcome animation fade");
|
||||
for cell in &self.history_cells {
|
||||
// Trigger fade on the AnimatedWelcomeCell
|
||||
cell.trigger_fade();
|
||||
}
|
||||
}
|
||||
|
||||
self.add_to_history(history_cell::new_user_prompt(original_text.clone()));
|
||||
}
|
||||
}
|
||||
@@ -1174,6 +1228,10 @@ impl ChatWidget<'_> {
|
||||
self.bottom_pane.clear_ctrl_c_quit_hint();
|
||||
self.bottom_pane.set_task_running(true);
|
||||
self.bottom_pane.update_status_text("waiting for model".to_string());
|
||||
|
||||
// Add loading animation to history
|
||||
self.add_to_history(history_cell::new_loading_cell("waiting for model".to_string()));
|
||||
|
||||
self.mark_needs_redraw();
|
||||
}
|
||||
EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: _ }) => {
|
||||
@@ -2784,6 +2842,32 @@ impl ChatWidget<'_> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Clear the conversation and start fresh with a new welcome animation
|
||||
pub(crate) fn new_conversation(&mut self, enhanced_keys_supported: bool) {
|
||||
// Clear all history cells
|
||||
self.history_cells.clear();
|
||||
|
||||
// Reset various state
|
||||
self.active_exec_cell = None;
|
||||
self.clear_token_usage();
|
||||
|
||||
// Add a new animated welcome cell
|
||||
let welcome_cell = Box::new(history_cell::new_animated_welcome());
|
||||
self.history_cells.push(welcome_cell);
|
||||
|
||||
// Reset the bottom pane with a new composer
|
||||
// (This effectively clears the text input)
|
||||
self.bottom_pane = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: self.app_event_tx.clone(),
|
||||
has_input_focus: true,
|
||||
enhanced_keys_supported,
|
||||
using_chatgpt_auth: self.config.using_chatgpt_auth,
|
||||
});
|
||||
|
||||
// Request redraw for the new animation
|
||||
self.mark_needs_redraw();
|
||||
}
|
||||
|
||||
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
let layout_areas = self.layout_areas(area);
|
||||
let bottom_pane_area = if layout_areas.len() == 4 {
|
||||
@@ -3488,7 +3572,18 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
let mut all_content: Vec<&dyn HistoryCell> = Vec::new();
|
||||
|
||||
// Add all history cells
|
||||
for cell in &self.history_cells {
|
||||
tracing::debug!("=== RENDER START: {} history cells ===", self.history_cells.len());
|
||||
for (idx, cell) in self.history_cells.iter().enumerate() {
|
||||
let is_animating = cell.is_animating();
|
||||
let has_custom = cell.has_custom_render();
|
||||
let height = cell.desired_height(content_area.width);
|
||||
|
||||
if is_animating || has_custom {
|
||||
tracing::info!(
|
||||
"Cell[{}]: animating={}, custom_render={}, height={}",
|
||||
idx, is_animating, has_custom, height
|
||||
);
|
||||
}
|
||||
all_content.push(cell);
|
||||
}
|
||||
|
||||
@@ -3535,7 +3630,10 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
let has_active_animation = self.history_cells.iter().any(|cell| cell.is_animating());
|
||||
|
||||
if has_active_animation {
|
||||
tracing::debug!("Active animation detected, requesting redraw");
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
} else {
|
||||
tracing::trace!("No active animations, total cells: {}", self.history_cells.len());
|
||||
}
|
||||
|
||||
// Calculate scroll position and vertical alignment
|
||||
@@ -3601,6 +3699,23 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
|
||||
// Render only the visible window of the item using vertical skip
|
||||
let skip_rows = skip_top;
|
||||
|
||||
// Log all cells being rendered
|
||||
let is_animating = item.is_animating();
|
||||
let has_custom = item.has_custom_render();
|
||||
|
||||
tracing::debug!(
|
||||
"RENDER Cell[{}]: area={:?}, skip_rows={}, animating={}, custom={}",
|
||||
idx, item_area, skip_rows, is_animating, has_custom
|
||||
);
|
||||
|
||||
if is_animating || has_custom {
|
||||
tracing::info!(
|
||||
">>> RENDERING ANIMATION Cell[{}]: area={:?}, skip_rows={}",
|
||||
idx, item_area, skip_rows
|
||||
);
|
||||
}
|
||||
|
||||
item.render_with_skip(item_area, buf, skip_rows);
|
||||
screen_y += visible_height;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use ratatui::widgets::Widget;
|
||||
|
||||
// Render the outline-fill animation
|
||||
pub fn render_intro_animation(area: Rect, buf: &mut Buffer, t: f32) {
|
||||
tracing::info!("render_intro_animation called: area={:?}, t={}", area, t);
|
||||
render_intro_outline_fill(area, buf, t)
|
||||
}
|
||||
|
||||
@@ -15,9 +16,12 @@ pub fn render_intro_animation_with_alpha(area: Rect, buf: &mut Buffer, t: f32, a
|
||||
|
||||
// Outline fill animation - inline, no borders
|
||||
pub fn render_intro_outline_fill(area: Rect, buf: &mut Buffer, t: f32) {
|
||||
tracing::info!("render_intro_outline_fill: area={:?}, checking size...", area);
|
||||
if area.width < 40 || area.height < 10 {
|
||||
tracing::warn!("!!! Area too small for animation: {}x{} (need 40x10)", area.width, area.height);
|
||||
return;
|
||||
}
|
||||
tracing::info!("Area size OK, proceeding with animation render");
|
||||
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
let outline_p = smoothstep(0.00, 0.60, t); // outline draws L->R
|
||||
@@ -27,10 +31,11 @@ pub fn render_intro_outline_fill(area: Rect, buf: &mut Buffer, t: f32) {
|
||||
let frame = (t * 60.0) as u32;
|
||||
|
||||
// Build scaled mask + border map
|
||||
// Don't reduce the area size - use full available space
|
||||
let (scale, mask, w, h) = scaled_mask(
|
||||
"CODER",
|
||||
area.width.saturating_sub(2),
|
||||
area.height.saturating_sub(2),
|
||||
area.width,
|
||||
area.height,
|
||||
);
|
||||
let border = compute_border(&mask);
|
||||
|
||||
@@ -59,6 +64,8 @@ pub fn render_intro_outline_fill(area: Rect, buf: &mut Buffer, t: f32) {
|
||||
Paragraph::new(lines)
|
||||
.alignment(Alignment::Left)
|
||||
.render(r, buf);
|
||||
|
||||
tracing::info!("✓ Animation rendered successfully at {:?}", r);
|
||||
}
|
||||
|
||||
// Outline fill animation with alpha blending - inline, no borders
|
||||
@@ -76,10 +83,11 @@ pub fn render_intro_outline_fill_with_alpha(area: Rect, buf: &mut Buffer, t: f32
|
||||
let frame = (t * 60.0) as u32;
|
||||
|
||||
// Build scaled mask + border map
|
||||
// Don't reduce the area size - use full available space
|
||||
let (scale, mask, w, h) = scaled_mask(
|
||||
"CODER",
|
||||
area.width.saturating_sub(2),
|
||||
area.height.saturating_sub(2),
|
||||
area.width,
|
||||
area.height,
|
||||
);
|
||||
let border = compute_border(&mask);
|
||||
|
||||
@@ -109,6 +117,8 @@ pub fn render_intro_outline_fill_with_alpha(area: Rect, buf: &mut Buffer, t: f32
|
||||
Paragraph::new(lines)
|
||||
.alignment(Alignment::Left)
|
||||
.render(r, buf);
|
||||
|
||||
tracing::info!("✓ Animation rendered successfully at {:?}", r);
|
||||
}
|
||||
|
||||
/* ---------------- outline fill renderer ---------------- */
|
||||
@@ -346,7 +356,9 @@ fn scaled_mask(word: &str, max_w: u16, max_h: u16) -> (usize, Vec<Vec<bool>>, us
|
||||
let letters: Vec<[&'static str; 7]> = word.chars().map(glyph_5x7).collect();
|
||||
let cols = letters.len() * w + (letters.len().saturating_sub(1)) * gap;
|
||||
|
||||
let mut scale = 16usize;
|
||||
// Start with a more reasonable scale - 6 gives us 42 rows height which looks good
|
||||
// This is about 60-70% of the previous size (was 16, now 6)
|
||||
let mut scale = 6usize;
|
||||
while scale > 1 && (cols * scale > max_w as usize || rows * scale > max_h as usize) {
|
||||
scale -= 1;
|
||||
}
|
||||
|
||||
@@ -65,11 +65,18 @@ pub(crate) trait HistoryCell {
|
||||
|
||||
fn render_with_skip(&self, area: Rect, buf: &mut Buffer, skip_rows: u16) {
|
||||
// Check if this cell has custom rendering
|
||||
if self.has_custom_render() && skip_rows == 0 {
|
||||
// Use custom render for cells that need it (like animations)
|
||||
self.custom_render(area, buf);
|
||||
if self.has_custom_render() {
|
||||
tracing::info!("render_with_skip: has_custom_render=true, skip_rows={}, area={:?}", skip_rows, area);
|
||||
if skip_rows == 0 {
|
||||
tracing::info!(">>> CALLING custom_render, area={:?}", area);
|
||||
// Use custom render for cells that need it (like animations)
|
||||
self.custom_render(area, buf);
|
||||
} else {
|
||||
tracing::warn!("!!! SKIPPING custom render due to skip_rows={} > 0", skip_rows);
|
||||
}
|
||||
} else {
|
||||
// Default: render using display_lines
|
||||
tracing::trace!("render_with_skip: using display_lines (no custom render)");
|
||||
let lines = self.display_lines();
|
||||
|
||||
// Skip the specified number of rows
|
||||
@@ -102,6 +109,21 @@ pub(crate) trait HistoryCell {
|
||||
fn is_animating(&self) -> bool {
|
||||
false // Default: most cells don't animate
|
||||
}
|
||||
|
||||
/// Returns true if this is a loading cell that should be removed when streaming starts
|
||||
fn is_loading_cell(&self) -> bool {
|
||||
false // Default: most cells are not loading cells
|
||||
}
|
||||
|
||||
/// Trigger fade-out animation (for AnimatedWelcomeCell)
|
||||
fn trigger_fade(&self) {
|
||||
// Default: do nothing (only AnimatedWelcomeCell implements this)
|
||||
}
|
||||
|
||||
/// Check if this cell should be removed (e.g., fully faded out)
|
||||
fn should_remove(&self) -> bool {
|
||||
false // Default: most cells should not be removed
|
||||
}
|
||||
}
|
||||
|
||||
// Allow Box<dyn HistoryCell> to implement HistoryCell
|
||||
@@ -113,6 +135,34 @@ impl HistoryCell for Box<dyn HistoryCell> {
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.as_ref().desired_height(width)
|
||||
}
|
||||
|
||||
fn render_with_skip(&self, area: Rect, buf: &mut Buffer, skip_rows: u16) {
|
||||
self.as_ref().render_with_skip(area, buf, skip_rows)
|
||||
}
|
||||
|
||||
fn has_custom_render(&self) -> bool {
|
||||
self.as_ref().has_custom_render()
|
||||
}
|
||||
|
||||
fn custom_render(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.as_ref().custom_render(area, buf)
|
||||
}
|
||||
|
||||
fn is_animating(&self) -> bool {
|
||||
self.as_ref().is_animating()
|
||||
}
|
||||
|
||||
fn is_loading_cell(&self) -> bool {
|
||||
self.as_ref().is_loading_cell()
|
||||
}
|
||||
|
||||
fn trigger_fade(&self) {
|
||||
self.as_ref().trigger_fade()
|
||||
}
|
||||
|
||||
fn should_remove(&self) -> bool {
|
||||
self.as_ref().should_remove()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PlainHistoryCell ====================
|
||||
@@ -170,11 +220,17 @@ impl HistoryCell for AnimatedWelcomeCell {
|
||||
]
|
||||
}
|
||||
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
// With scale of 6, we need 7 * 6 = 42 rows
|
||||
42
|
||||
}
|
||||
|
||||
fn has_custom_render(&self) -> bool {
|
||||
true // AnimatedWelcomeCell uses custom rendering for the glitch animation
|
||||
}
|
||||
|
||||
fn custom_render(&self, area: Rect, buf: &mut Buffer) {
|
||||
tracing::debug!("AnimatedWelcomeCell::custom_render called, area: {:?}", area);
|
||||
let fade_duration = std::time::Duration::from_millis(800);
|
||||
|
||||
// Check if we're in fade-out phase
|
||||
@@ -185,6 +241,7 @@ impl HistoryCell for AnimatedWelcomeCell {
|
||||
let fade_progress = fade_elapsed.as_secs_f32() / fade_duration.as_secs_f32();
|
||||
let alpha = 1.0 - fade_progress; // From 1.0 to 0.0
|
||||
|
||||
tracing::debug!("Rendering fade-out animation, alpha: {}", alpha);
|
||||
crate::glitch_animation::render_intro_animation_with_alpha(
|
||||
area, buf, 1.0, // Full animation progress (static state)
|
||||
alpha,
|
||||
@@ -193,6 +250,7 @@ impl HistoryCell for AnimatedWelcomeCell {
|
||||
// Fade-out complete - mark as faded out
|
||||
self.faded_out.set(true);
|
||||
// Don't render anything (invisible)
|
||||
tracing::debug!("Fade-out complete, not rendering");
|
||||
}
|
||||
} else {
|
||||
// Normal animation phase
|
||||
@@ -204,12 +262,14 @@ impl HistoryCell for AnimatedWelcomeCell {
|
||||
let progress = elapsed.as_secs_f32() / animation_duration.as_secs_f32();
|
||||
|
||||
// Render the animation
|
||||
tracing::debug!("Rendering animation, progress: {}", progress);
|
||||
crate::glitch_animation::render_intro_animation(area, buf, progress);
|
||||
} else {
|
||||
// Animation complete - mark it and render final static state
|
||||
self.completed.set(true);
|
||||
|
||||
// Render the final static state
|
||||
tracing::debug!("Animation complete, rendering static state");
|
||||
crate::glitch_animation::render_intro_animation(
|
||||
area, buf, 1.0, // Full progress = static final state
|
||||
);
|
||||
@@ -244,6 +304,55 @@ impl HistoryCell for AnimatedWelcomeCell {
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn trigger_fade(&self) {
|
||||
// Only trigger fade if not already fading or faded
|
||||
if self.fade_start.get().is_none() {
|
||||
tracing::info!("Triggering fade-out animation for AnimatedWelcomeCell");
|
||||
self.fade_start.set(Some(Instant::now()));
|
||||
}
|
||||
}
|
||||
|
||||
fn should_remove(&self) -> bool {
|
||||
// Remove only after fade-out is complete
|
||||
self.faded_out.get()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== LoadingCell ====================
|
||||
|
||||
pub(crate) struct LoadingCell {
|
||||
#[allow(dead_code)] // May be used for displaying status alongside animation
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
impl HistoryCell for LoadingCell {
|
||||
fn display_lines(&self) -> Vec<Line<'static>> {
|
||||
vec![
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled("⟳ ", Style::default().fg(Color::Cyan)),
|
||||
Span::from("Loading..."),
|
||||
]),
|
||||
Line::from(""),
|
||||
]
|
||||
}
|
||||
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
3 // Just 3 lines for the loading message
|
||||
}
|
||||
|
||||
fn has_custom_render(&self) -> bool {
|
||||
false // No custom rendering needed, just use display_lines
|
||||
}
|
||||
|
||||
fn is_animating(&self) -> bool {
|
||||
false // Not animating - no need for constant redraws
|
||||
}
|
||||
|
||||
fn is_loading_cell(&self) -> bool {
|
||||
true // This is a loading cell
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ImageOutputCell ====================
|
||||
@@ -458,6 +567,12 @@ pub(crate) fn new_animated_welcome() -> AnimatedWelcomeCell {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_loading_cell(message: String) -> LoadingCell {
|
||||
LoadingCell {
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_active_exec_command(
|
||||
command: Vec<String>,
|
||||
parsed: Vec<ParsedCommand>,
|
||||
|
||||
Reference in New Issue
Block a user