First pass at a TUI onboarding (#1876)
This sets up the scaffolding and basic flow for a TUI onboarding experience. It covers sign in with ChatGPT, env auth, as well as some safety guidance. Next up: 1. Replace the git warning screen 2. Use this to configure default approval/sandbox modes Note the shimmer flashes are from me slicing the video, not jank. https://github.com/user-attachments/assets/0fbe3479-fdde-41f3-87fb-a7a83ab895b8
This commit is contained in:
84
codex-rs/tui/src/shimmer.rs
Normal file
84
codex-rs/tui/src/shimmer.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::Span;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FrameTicker {
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl FrameTicker {
|
||||
pub(crate) fn new(app_event_tx: AppEventSender) -> Self {
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let running_clone = running.clone();
|
||||
let app_event_tx_clone = app_event_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
while running_clone.load(Ordering::Relaxed) {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
app_event_tx_clone.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
});
|
||||
Self { running }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FrameTicker {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn shimmer_spans(text: &str, frame_idx: usize) -> Vec<Span<'static>> {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let padding = 10usize;
|
||||
let period = chars.len() + padding * 2;
|
||||
let pos = frame_idx % period;
|
||||
let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout)
|
||||
.map(|level| level.has_16m)
|
||||
.unwrap_or(false);
|
||||
let band_half_width = 6.0;
|
||||
|
||||
let mut spans: Vec<Span<'static>> = Vec::with_capacity(chars.len());
|
||||
for (i, ch) in 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 {
|
||||
Style::default().fg(color_for_level(level))
|
||||
};
|
||||
spans.push(Span::styled(ch.to_string(), style));
|
||||
}
|
||||
spans
|
||||
}
|
||||
|
||||
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