mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
wip
This commit is contained in:
@@ -5,6 +5,7 @@ use crate::file_search::FileSearchManager;
|
||||
use crate::get_git_diff::get_git_diff;
|
||||
use crate::git_warning_screen::GitWarningOutcome;
|
||||
use crate::git_warning_screen::GitWarningScreen;
|
||||
use crate::shimmer_text::init_process_start;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::tui;
|
||||
use codex_core::config::Config;
|
||||
@@ -21,13 +22,10 @@ use ratatui::layout::Offset;
|
||||
use ratatui::prelude::Backend;
|
||||
use ratatui::text::Line;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Time window for debouncing redraw requests.
|
||||
const REDRAW_DEBOUNCE: Duration = Duration::from_millis(10);
|
||||
@@ -55,9 +53,6 @@ pub(crate) struct App<'a> {
|
||||
|
||||
file_search: FileSearchManager,
|
||||
|
||||
/// True when a redraw has been scheduled but not yet executed.
|
||||
pending_redraw: Arc<AtomicBool>,
|
||||
|
||||
pending_history_lines: Vec<Line<'static>>,
|
||||
|
||||
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
|
||||
@@ -65,6 +60,10 @@ pub(crate) struct App<'a> {
|
||||
chat_args: Option<ChatWidgetArgs>,
|
||||
|
||||
enhanced_keys_supported: bool,
|
||||
|
||||
/// Channel to schedule one-shot animation frames; coalesced by a single
|
||||
/// scheduler thread.
|
||||
frame_schedule_tx: std::sync::mpsc::Sender<Instant>,
|
||||
}
|
||||
|
||||
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
||||
@@ -86,7 +85,6 @@ impl App<'_> {
|
||||
) -> Self {
|
||||
let (app_event_tx, app_event_rx) = channel();
|
||||
let app_event_tx = AppEventSender::new(app_event_tx);
|
||||
let pending_redraw = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let enhanced_keys_supported = supports_keyboard_enhancement().unwrap_or(false);
|
||||
|
||||
@@ -133,6 +131,9 @@ impl App<'_> {
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize process start time for synchronized animations.
|
||||
init_process_start();
|
||||
|
||||
let (app_state, chat_args) = if show_git_warning {
|
||||
(
|
||||
AppState::GitWarning {
|
||||
@@ -162,6 +163,50 @@ impl App<'_> {
|
||||
};
|
||||
|
||||
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
|
||||
|
||||
// Spawn a single scheduler thread that coalesces both debounced redraw
|
||||
// requests and animation frame requests, and emits a single Redraw event
|
||||
// at the earliest requested time.
|
||||
let (frame_tx, frame_rx) = channel::<Instant>();
|
||||
{
|
||||
let app_event_tx = app_event_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
use std::sync::mpsc::RecvTimeoutError;
|
||||
let mut next_deadline: Option<Instant> = None;
|
||||
loop {
|
||||
// If no scheduled deadline, block until we get one.
|
||||
if next_deadline.is_none() {
|
||||
match frame_rx.recv() {
|
||||
Ok(deadline) => next_deadline = Some(deadline),
|
||||
Err(_) => break, // channel closed; exit thread
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
let deadline = next_deadline.expect("set above");
|
||||
let now = Instant::now();
|
||||
let timeout = if deadline > now {
|
||||
deadline - now
|
||||
} else {
|
||||
Duration::from_millis(0)
|
||||
};
|
||||
|
||||
match frame_rx.recv_timeout(timeout) {
|
||||
Ok(new_deadline) => {
|
||||
// Coalesce by keeping the earliest deadline.
|
||||
next_deadline =
|
||||
Some(next_deadline.map_or(new_deadline, |d| d.min(new_deadline)));
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
// Fire once, then clear the deadline.
|
||||
app_event_tx.send(AppEvent::Redraw);
|
||||
next_deadline = None;
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Self {
|
||||
app_event_tx,
|
||||
pending_history_lines: Vec::new(),
|
||||
@@ -169,9 +214,9 @@ impl App<'_> {
|
||||
app_state,
|
||||
config,
|
||||
file_search,
|
||||
pending_redraw,
|
||||
chat_args,
|
||||
enhanced_keys_supported,
|
||||
frame_schedule_tx: frame_tx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,32 +226,13 @@ impl App<'_> {
|
||||
self.app_event_tx.clone()
|
||||
}
|
||||
|
||||
/// Schedule a redraw if one is not already pending.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn schedule_redraw(&self) {
|
||||
// Attempt to set the flag to `true`. If it was already `true`, another
|
||||
// redraw is already pending so we can return early.
|
||||
if self
|
||||
.pending_redraw
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let tx = self.app_event_tx.clone();
|
||||
let pending_redraw = self.pending_redraw.clone();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(REDRAW_DEBOUNCE);
|
||||
tx.send(AppEvent::Redraw);
|
||||
pending_redraw.store(false, Ordering::SeqCst);
|
||||
});
|
||||
fn schedule_frame_in(&self, dur: Duration) {
|
||||
let _ = self.frame_schedule_tx.send(Instant::now() + dur);
|
||||
}
|
||||
|
||||
pub(crate) fn run(&mut self, terminal: &mut tui::Tui) -> Result<()> {
|
||||
// Insert an event to trigger the first render.
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
app_event_tx.send(AppEvent::RequestRedraw);
|
||||
// Trigger the first render immediately via the frame scheduler.
|
||||
let _ = self.frame_schedule_tx.send(Instant::now());
|
||||
|
||||
while let Ok(event) = self.app_event_rx.recv() {
|
||||
match event {
|
||||
@@ -215,7 +241,10 @@ impl App<'_> {
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
AppEvent::RequestRedraw => {
|
||||
self.schedule_redraw();
|
||||
self.schedule_frame_in(REDRAW_DEBOUNCE);
|
||||
}
|
||||
AppEvent::ScheduleFrameIn(dur) => {
|
||||
self.schedule_frame_in(dur);
|
||||
}
|
||||
AppEvent::Redraw => {
|
||||
std::io::stdout().sync_update(|_| self.draw_next_frame(terminal))??;
|
||||
|
||||
@@ -2,6 +2,7 @@ use codex_core::protocol::Event;
|
||||
use codex_file_search::FileMatch;
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::text::Line;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::slash_command::SlashCommand;
|
||||
|
||||
@@ -15,6 +16,11 @@ pub(crate) enum AppEvent {
|
||||
/// Actually draw the next frame.
|
||||
Redraw,
|
||||
|
||||
/// Schedule periodic frames from the main loop. The first frame will be
|
||||
/// scheduled roughly after the provided duration and continue at that
|
||||
/// cadence until the application exits.
|
||||
ScheduleFrameIn(Duration),
|
||||
|
||||
KeyEvent(KeyEvent),
|
||||
|
||||
/// Text pasted from the terminal clipboard.
|
||||
|
||||
@@ -31,15 +31,12 @@ use ratatui::widgets::Wrap;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use tracing::error;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::shimmer_text::shimmer_spans;
|
||||
|
||||
pub(crate) struct CommandOutput {
|
||||
pub(crate) exit_code: i32,
|
||||
@@ -686,104 +683,22 @@ impl WidgetRef for &HistoryCell {
|
||||
|
||||
pub(crate) struct ActiveExecCommandView {
|
||||
command: String,
|
||||
frame_idx: Arc<AtomicUsize>,
|
||||
running: Arc<AtomicBool>,
|
||||
_app_event_tx: AppEventSender,
|
||||
}
|
||||
|
||||
impl ActiveExecCommandView {
|
||||
fn new(command: String, app_event_tx: AppEventSender) -> Self {
|
||||
let frame_idx = Arc::new(AtomicUsize::new(0));
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Animation thread to drive shimmer and trigger redraws.
|
||||
{
|
||||
let frame_idx_clone = Arc::clone(&frame_idx);
|
||||
let running_clone = Arc::clone(&running);
|
||||
let app_event_tx_clone = app_event_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut counter = 0usize;
|
||||
while running_clone.load(Ordering::Relaxed) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
counter = counter.wrapping_add(1);
|
||||
frame_idx_clone.store(counter, Ordering::Relaxed);
|
||||
app_event_tx_clone.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
command,
|
||||
frame_idx,
|
||||
running,
|
||||
_app_event_tx: app_event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
fn shimmering_header_spans(&self) -> Vec<Span<'static>> {
|
||||
let header_text = "command";
|
||||
let header_chars: Vec<char> = header_text.chars().collect();
|
||||
|
||||
let padding = 4usize; // virtual padding around the word for smoother loop
|
||||
let period = header_chars.len() + padding * 2;
|
||||
let pos = self.frame_idx.load(Ordering::Relaxed) % period;
|
||||
|
||||
let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout)
|
||||
.map(|level| level.has_16m)
|
||||
.unwrap_or(false);
|
||||
|
||||
// Width of the bright band (in characters).
|
||||
let band_half_width = 2.0;
|
||||
|
||||
let mut header_spans: Vec<Span<'static>> = Vec::new();
|
||||
for (i, ch) in header_chars.iter().enumerate() {
|
||||
let i_pos = i as isize + padding as isize;
|
||||
let pos = pos as isize;
|
||||
let dist = (i_pos - pos).abs() as f32;
|
||||
|
||||
let t = if dist <= band_half_width {
|
||||
let x = std::f32::consts::PI * (dist / band_half_width);
|
||||
0.5 * (1.0 + x.cos())
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let brightness = 0.4 + 0.6 * t;
|
||||
let level = (brightness * 255.0).clamp(0.0, 255.0) as u8;
|
||||
let style = if has_true_color {
|
||||
Style::default()
|
||||
.fg(Color::Rgb(level, level, level))
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
// Bold makes dark gray and gray look the same, so don't use it
|
||||
// when true color is not supported.
|
||||
Style::default().fg(color_for_level(level))
|
||||
};
|
||||
|
||||
header_spans.push(Span::styled(ch.to_string(), style));
|
||||
}
|
||||
|
||||
header_spans
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ActiveExecCommandView {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl DynamicHeightWidgetRef for &ActiveExecCommandView {
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let mut spans = self.shimmering_header_spans();
|
||||
spans.push(Span::styled(
|
||||
" ",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::styled("running...", Style::default().dim()));
|
||||
|
||||
let lines: Vec<Line<'static>> = vec![
|
||||
Line::from(spans),
|
||||
Line::from("Running command"),
|
||||
Line::from(format!("$ {}", self.command)),
|
||||
Line::from(""),
|
||||
];
|
||||
@@ -796,16 +711,11 @@ impl DynamicHeightWidgetRef for &ActiveExecCommandView {
|
||||
}
|
||||
impl WidgetRef for &ActiveExecCommandView {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let mut spans = self.shimmering_header_spans();
|
||||
spans.push(Span::styled(
|
||||
" ",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::styled("running...", Style::default().dim()));
|
||||
// Schedule a one-shot next frame to continue the shimmer.
|
||||
self._app_event_tx
|
||||
.send(AppEvent::ScheduleFrameIn(Duration::from_millis(100)));
|
||||
let lines: Vec<Line<'static>> = vec![
|
||||
Line::from(spans),
|
||||
Line::from(shimmer_spans("Running command")),
|
||||
Line::from(format!("$ {}", self.command)),
|
||||
Line::from(""),
|
||||
];
|
||||
@@ -815,16 +725,6 @@ impl WidgetRef for &ActiveExecCommandView {
|
||||
}
|
||||
}
|
||||
|
||||
fn color_for_level(level: u8) -> Color {
|
||||
if level < 128 {
|
||||
Color::DarkGray
|
||||
} else if level < 192 {
|
||||
Color::Gray
|
||||
} else {
|
||||
Color::White
|
||||
}
|
||||
}
|
||||
|
||||
fn create_diff_summary(changes: HashMap<PathBuf, FileChange>) -> Vec<String> {
|
||||
// Build a concise, human‑readable summary list similar to the
|
||||
// `git status` short format so the user can reason about the
|
||||
|
||||
@@ -34,6 +34,7 @@ mod history_cell;
|
||||
mod insert_history;
|
||||
mod log_layer;
|
||||
mod markdown;
|
||||
mod shimmer_text;
|
||||
mod slash_command;
|
||||
mod status_indicator_widget;
|
||||
mod text_block;
|
||||
|
||||
97
codex-rs/tui/src/shimmer_text.rs
Normal file
97
codex-rs/tui/src/shimmer_text.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::Span;
|
||||
|
||||
static PROCESS_START: OnceLock<Instant> = OnceLock::new();
|
||||
|
||||
/// Ensure the process start time is initialized. Call early in app startup
|
||||
/// so all animations key off a common origin.
|
||||
pub(crate) fn init_process_start() {
|
||||
let _ = PROCESS_START.set(Instant::now());
|
||||
}
|
||||
|
||||
fn elapsed_since_start() -> Duration {
|
||||
let start = PROCESS_START.get_or_init(Instant::now);
|
||||
start.elapsed()
|
||||
}
|
||||
|
||||
/// Compute grayscale shimmer spans for the provided text based on elapsed
|
||||
/// time since process start. Uses a cosine falloff across a small band to
|
||||
/// achieve a smooth highlight that sweeps across the text.
|
||||
pub(crate) fn shimmer_spans(text: &str) -> Vec<Span<'static>> {
|
||||
let header_chars: Vec<char> = text.chars().collect();
|
||||
|
||||
// Synchronize the shimmer so that all instances start at the beginning
|
||||
// and reach the end at the same time, regardless of length. We achieve
|
||||
// this by mapping elapsed time into a global sweep fraction in [0, 1),
|
||||
// then scaling that fraction across the character indices of this text.
|
||||
// The bright band width (in characters) remains constant.
|
||||
let len = header_chars.len();
|
||||
if len == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Width of the bright band (in characters).
|
||||
let band_half_width = (len as f32) / 4.0;
|
||||
|
||||
// Use character-based padding: pretend the string is longer by
|
||||
// `PADDING * 2` characters and move at a constant velocity over time.
|
||||
// We compute the cycle duration in time (including pre/post time derived
|
||||
// from character padding at constant velocity) and wrap using time modulo
|
||||
// rather than modulo on character distance.
|
||||
const SWEEP_SECONDS: f32 = 1.5; // time to traverse the visible text
|
||||
let PADDING: f32 = band_half_width;
|
||||
let elapsed = elapsed_since_start().as_secs_f32();
|
||||
let pos = (elapsed % SWEEP_SECONDS) / SWEEP_SECONDS * (len as f32 + PADDING * 2.0) - PADDING;
|
||||
|
||||
let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout)
|
||||
.map(|level| level.has_16m)
|
||||
.unwrap_or(false);
|
||||
|
||||
let mut header_spans: Vec<Span<'static>> = Vec::with_capacity(header_chars.len());
|
||||
for (i, ch) in header_chars.iter().enumerate() {
|
||||
let i_pos = i as f32;
|
||||
let dist = (i_pos - pos).abs();
|
||||
|
||||
let t = if dist <= band_half_width {
|
||||
let x = std::f32::consts::PI * (dist / band_half_width);
|
||||
0.5 * (1.0 + x.cos())
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let brightness = 0.4 + 0.6 * t;
|
||||
let level = (brightness * 255.0).clamp(0.0, 255.0) as u8;
|
||||
let style = if has_true_color {
|
||||
Style::default()
|
||||
.fg(Color::Rgb(level, level, level))
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
// Bold makes dark gray and gray look the same, so don't use it
|
||||
// when true color is not supported.
|
||||
Style::default().fg(color_for_level(level))
|
||||
};
|
||||
|
||||
header_spans.push(Span::styled(ch.to_string(), style));
|
||||
}
|
||||
|
||||
header_spans
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Utility used for 16-color terminals to approximate grayscale.
|
||||
pub(crate) fn color_for_level(level: u8) -> Color {
|
||||
if level < 128 {
|
||||
Color::DarkGray
|
||||
} else if level < 192 {
|
||||
Color::Gray
|
||||
} else {
|
||||
Color::White
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
//! A live status indicator that shows the *latest* log line emitted by the
|
||||
//! application while the agent is processing a long‑running task.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
@@ -26,6 +21,7 @@ use ratatui::widgets::WidgetRef;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::shimmer_text::shimmer_spans;
|
||||
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
|
||||
@@ -33,42 +29,15 @@ pub(crate) struct StatusIndicatorWidget {
|
||||
/// Latest text to display (truncated to the available width at render
|
||||
/// time).
|
||||
text: String,
|
||||
|
||||
frame_idx: Arc<AtomicUsize>,
|
||||
running: Arc<AtomicBool>,
|
||||
// Keep one sender alive to prevent the channel from closing while the
|
||||
// animation thread is still running. The field itself is currently not
|
||||
// accessed anywhere, therefore the leading underscore silences the
|
||||
// `dead_code` warning without affecting behavior.
|
||||
// Keep one sender alive for scheduling frames.
|
||||
_app_event_tx: AppEventSender,
|
||||
}
|
||||
|
||||
impl StatusIndicatorWidget {
|
||||
/// Create a new status indicator and start the animation timer.
|
||||
pub(crate) fn new(app_event_tx: AppEventSender) -> Self {
|
||||
let frame_idx = Arc::new(AtomicUsize::new(0));
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Animation thread.
|
||||
{
|
||||
let frame_idx_clone = Arc::clone(&frame_idx);
|
||||
let running_clone = Arc::clone(&running);
|
||||
let app_event_tx_clone = app_event_tx.clone();
|
||||
thread::spawn(move || {
|
||||
let mut counter = 0usize;
|
||||
while running_clone.load(Ordering::Relaxed) {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
counter = counter.wrapping_add(1);
|
||||
frame_idx_clone.store(counter, Ordering::Relaxed);
|
||||
app_event_tx_clone.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
text: String::from("waiting for logs…"),
|
||||
frame_idx,
|
||||
running,
|
||||
_app_event_tx: app_event_tx,
|
||||
}
|
||||
}
|
||||
@@ -83,63 +52,19 @@ impl StatusIndicatorWidget {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StatusIndicatorWidget {
|
||||
fn drop(&mut self) {
|
||||
use std::sync::atomic::Ordering;
|
||||
self.running.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for StatusIndicatorWidget {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
// Schedule the next animation frame.
|
||||
self._app_event_tx
|
||||
.send(AppEvent::ScheduleFrameIn(Duration::from_millis(100)));
|
||||
|
||||
let widget_style = Style::default();
|
||||
let block = Block::default()
|
||||
.padding(Padding::new(1, 0, 0, 0))
|
||||
.borders(Borders::LEFT)
|
||||
.border_type(BorderType::QuadrantOutside)
|
||||
.border_style(widget_style.dim());
|
||||
let idx = self.frame_idx.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let header_text = "Working";
|
||||
let header_chars: Vec<char> = header_text.chars().collect();
|
||||
|
||||
let padding = 4usize; // virtual padding around the word for smoother loop
|
||||
let period = header_chars.len() + padding * 2;
|
||||
let pos = idx % period;
|
||||
|
||||
let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout)
|
||||
.map(|level| level.has_16m)
|
||||
.unwrap_or(false);
|
||||
|
||||
// Width of the bright band (in characters).
|
||||
let band_half_width = 2.0;
|
||||
|
||||
let mut header_spans: Vec<Span<'static>> = Vec::new();
|
||||
for (i, ch) in header_chars.iter().enumerate() {
|
||||
let i_pos = i as isize + padding as isize;
|
||||
let pos = pos as isize;
|
||||
let dist = (i_pos - pos).abs() as f32;
|
||||
|
||||
let t = if dist <= band_half_width {
|
||||
let x = std::f32::consts::PI * (dist / band_half_width);
|
||||
0.5 * (1.0 + x.cos())
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let brightness = 0.4 + 0.6 * t;
|
||||
let level = (brightness * 255.0).clamp(0.0, 255.0) as u8;
|
||||
let style = if has_true_color {
|
||||
Style::default()
|
||||
.fg(Color::Rgb(level, level, level))
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
// Bold makes dark gray and gray look the same, so don't use it
|
||||
// when true color is not supported.
|
||||
Style::default().fg(color_for_level(level))
|
||||
};
|
||||
|
||||
header_spans.push(Span::styled(ch.to_string(), style));
|
||||
}
|
||||
let mut header_spans: Vec<Span<'static>> = shimmer_spans("Working");
|
||||
|
||||
header_spans.push(Span::styled(
|
||||
" ",
|
||||
@@ -194,13 +119,3 @@ impl WidgetRef for StatusIndicatorWidget {
|
||||
paragraph.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn color_for_level(level: u8) -> Color {
|
||||
if level < 128 {
|
||||
Color::DarkGray
|
||||
} else if level < 192 {
|
||||
Color::Gray
|
||||
} else {
|
||||
Color::White
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user