Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
310
crates/codegen/xai-ratatui-inline/examples/inline.rs
Normal file
310
crates/codegen/xai-ratatui-inline/examples/inline.rs
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
use std::{
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ansi_width::ansi_width;
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyModifiers},
|
||||
style::Color as CColor,
|
||||
};
|
||||
use lipsum::lipsum;
|
||||
use ratatui::{
|
||||
TerminalOptions, Viewport,
|
||||
prelude::CrosstermBackend,
|
||||
style::{Color, Style},
|
||||
widgets::Block,
|
||||
};
|
||||
|
||||
use xai_ratatui_inline::{
|
||||
Terminal, emit_to_scrollback, resize_purge_rerender, resize_viewport_height,
|
||||
with_synchronized_output,
|
||||
};
|
||||
|
||||
/// Build a line with markers for visualization
|
||||
fn build_marked_line(line_num: usize, content: &str) -> String {
|
||||
format!("[[{:02}]] {} [[{:02}]]", line_num, content, line_num)
|
||||
}
|
||||
|
||||
/// Generate test content based on pattern
|
||||
fn generate_test_line(line_num: usize, terminal_width: usize) -> String {
|
||||
let i = line_num - 1;
|
||||
let mut line = match i % 3 {
|
||||
0 => lipsum(4), // Short line
|
||||
1 => lipsum(40), // Long line
|
||||
2 => {
|
||||
// Unicode string with ANSI colors and hyperlink for testing
|
||||
let unicode_with_ansi = "\x1b[31m😀\u{200D}\x1b[0m\x1b[32mé\x1b[0m中\u{0300}\x1b[34mX\x1b[0m\x1b]8;;https://example.com\x1b\\H\x1b]8;;\x1b\\";
|
||||
let visual_width = ansi_width(unicode_with_ansi);
|
||||
let marker_adjustment = if i % 6 >= 3 { 7 } else { 14 };
|
||||
let remaining_width = terminal_width * 2 - marker_adjustment - visual_width;
|
||||
format!("{unicode_with_ansi}{}", "B".repeat(remaining_width))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Add newline prefix for cases 3-5, alternating between \n and \r\n
|
||||
if i % 6 >= 3 {
|
||||
line = format!("{}\n{}\r\n{line}", lipsum(2), lipsum(8));
|
||||
}
|
||||
|
||||
line
|
||||
}
|
||||
|
||||
/// Generate tall content that's 1.5x terminal height
|
||||
fn generate_tall_content(terminal_width: usize, terminal_height: usize) -> String {
|
||||
let num_lines = terminal_height;
|
||||
let mut lines = Vec::new();
|
||||
for i in 1..=num_lines {
|
||||
lines.push(format!(
|
||||
"Line {i:03}/{num_lines:03}: {}",
|
||||
if i % 2 == 1 {
|
||||
lipsum((terminal_width / 25).max(1))
|
||||
} else {
|
||||
lipsum((terminal_width / 5).max(1))
|
||||
}
|
||||
));
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
/// Build content with ANSI color codes
|
||||
fn colorize_content(content: &str, color: CColor) -> String {
|
||||
let color_code = match color {
|
||||
CColor::Black => 30,
|
||||
CColor::Red | CColor::DarkRed => 31,
|
||||
CColor::Green | CColor::DarkGreen => 32,
|
||||
CColor::Yellow | CColor::DarkYellow => 33,
|
||||
CColor::Blue | CColor::DarkBlue => 34,
|
||||
CColor::Magenta | CColor::DarkMagenta => 35,
|
||||
CColor::Cyan | CColor::DarkCyan => 36,
|
||||
CColor::White | CColor::Grey | CColor::DarkGrey => 37,
|
||||
_ => 37, // Default to white for RGB or other colors
|
||||
};
|
||||
format!("\x1b[{}m{}\x1b[0m", color_code, content)
|
||||
}
|
||||
|
||||
fn init_terminal(inline_height: u16) -> io::Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
let hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
fn try_restore() -> io::Result<()> {
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if let Err(err) = try_restore() {
|
||||
eprintln!("Failed to restore terminal: {err}");
|
||||
}
|
||||
hook(info);
|
||||
}));
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Inline(inline_height),
|
||||
};
|
||||
let terminal = Terminal::with_options(backend, options)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::Result<()> {
|
||||
// Clear only the viewport, preserving log content above it
|
||||
terminal.clear()?;
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum KeyAction {
|
||||
Quit,
|
||||
Normal,
|
||||
Tall,
|
||||
IncreaseViewport,
|
||||
DecreaseViewport,
|
||||
}
|
||||
|
||||
/// Spinner animation state
|
||||
struct Spinner {
|
||||
frames: Vec<&'static str>,
|
||||
current: usize,
|
||||
interval: Duration,
|
||||
next_frame_time: Instant,
|
||||
}
|
||||
|
||||
impl Spinner {
|
||||
fn new() -> Self {
|
||||
let frames = vec!["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"];
|
||||
Self {
|
||||
frames,
|
||||
current: 0,
|
||||
interval: Duration::from_millis(100),
|
||||
next_frame_time: Instant::now() + Duration::from_millis(100),
|
||||
}
|
||||
}
|
||||
|
||||
fn tick(&mut self) {
|
||||
self.current = (self.current + 1) % self.frames.len();
|
||||
self.next_frame_time = Instant::now() + self.interval;
|
||||
}
|
||||
|
||||
fn current_frame(&self) -> &str {
|
||||
self.frames[self.current]
|
||||
}
|
||||
|
||||
fn time_until_next_frame(&self) -> Duration {
|
||||
self.next_frame_time
|
||||
.saturating_duration_since(Instant::now())
|
||||
}
|
||||
}
|
||||
|
||||
enum PollResult {
|
||||
KeyPressed(KeyAction),
|
||||
AnimationTick,
|
||||
Resize,
|
||||
}
|
||||
|
||||
fn poll_for_key_or_animation(spinner: &Spinner) -> io::Result<PollResult> {
|
||||
let timeout = spinner.time_until_next_frame();
|
||||
|
||||
if event::poll(timeout)? {
|
||||
match event::read()? {
|
||||
Event::Key(key) => {
|
||||
if key.code == KeyCode::Esc
|
||||
|| (key.code == KeyCode::Char('c')
|
||||
&& key.modifiers.contains(KeyModifiers::CONTROL))
|
||||
|| key.code == KeyCode::Char('q')
|
||||
|| key.code == KeyCode::Char('Q')
|
||||
{
|
||||
return Ok(PollResult::KeyPressed(KeyAction::Quit));
|
||||
} else if key.code == KeyCode::Char('t') || key.code == KeyCode::Char('T') {
|
||||
return Ok(PollResult::KeyPressed(KeyAction::Tall));
|
||||
} else if key.code == KeyCode::Char('+') || key.code == KeyCode::Char('=') {
|
||||
return Ok(PollResult::KeyPressed(KeyAction::IncreaseViewport));
|
||||
} else if key.code == KeyCode::Char('-') || key.code == KeyCode::Char('_') {
|
||||
return Ok(PollResult::KeyPressed(KeyAction::DecreaseViewport));
|
||||
} else if matches!(key.code, KeyCode::Char(_) | KeyCode::Enter) {
|
||||
return Ok(PollResult::KeyPressed(KeyAction::Normal));
|
||||
}
|
||||
// Key we don't care about, continue polling
|
||||
poll_for_key_or_animation(spinner)
|
||||
}
|
||||
Event::Resize(_, _) => Ok(PollResult::Resize),
|
||||
_ => {
|
||||
// Other event we don't care about, continue polling
|
||||
poll_for_key_or_animation(spinner)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Timeout reached, time to animate
|
||||
Ok(PollResult::AnimationTick)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_viewport(
|
||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||
spinner: &Spinner,
|
||||
viewport_height: u16,
|
||||
) -> io::Result<()> {
|
||||
terminal.draw(|frame| {
|
||||
let area = frame.area();
|
||||
let spinner_text = format!(
|
||||
" {} Inline Viewport ({} lines) {} ",
|
||||
spinner.current_frame(),
|
||||
viewport_height,
|
||||
spinner.current_frame()
|
||||
);
|
||||
let block = Block::bordered()
|
||||
.title(spinner_text)
|
||||
.border_style(Style::default().fg(Color::Cyan));
|
||||
frame.render_widget(block, area);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
const INITIAL_HEIGHT: u16 = 3;
|
||||
const MIN_HEIGHT: u16 = 2;
|
||||
|
||||
let mut terminal = init_terminal(INITIAL_HEIGHT)?;
|
||||
let mut spinner = Spinner::new();
|
||||
let mut viewport_height = INITIAL_HEIGHT;
|
||||
|
||||
render_viewport(&mut terminal, &spinner, viewport_height)?;
|
||||
|
||||
let colors = [
|
||||
CColor::Green,
|
||||
CColor::Yellow,
|
||||
CColor::Magenta,
|
||||
CColor::Blue,
|
||||
CColor::Red,
|
||||
CColor::Cyan,
|
||||
];
|
||||
|
||||
let mut line_num = 1;
|
||||
let mut scrollback_history = String::new(); // has crlf-s instead of lf-s
|
||||
|
||||
loop {
|
||||
match poll_for_key_or_animation(&spinner)? {
|
||||
PollResult::AnimationTick => {
|
||||
spinner.tick();
|
||||
render_viewport(&mut terminal, &spinner, viewport_height)?;
|
||||
}
|
||||
PollResult::Resize => {
|
||||
with_synchronized_output(&mut terminal, |terminal| {
|
||||
resize_purge_rerender(terminal, &scrollback_history)?;
|
||||
render_viewport(terminal, &spinner, viewport_height)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
PollResult::KeyPressed(action) => match action {
|
||||
KeyAction::Quit => break,
|
||||
KeyAction::IncreaseViewport | KeyAction::DecreaseViewport => {
|
||||
let new_viewport_height = if let KeyAction::IncreaseViewport = action {
|
||||
viewport_height + 1
|
||||
} else {
|
||||
viewport_height.saturating_sub(1)
|
||||
}
|
||||
.clamp(MIN_HEIGHT, terminal.size()?.height.saturating_sub(1));
|
||||
if new_viewport_height != viewport_height {
|
||||
viewport_height = new_viewport_height;
|
||||
with_synchronized_output(&mut terminal, |terminal| {
|
||||
resize_viewport_height(terminal, viewport_height)?;
|
||||
render_viewport(terminal, &spinner, viewport_height)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
KeyAction::Tall | KeyAction::Normal => {
|
||||
let area = terminal.size()?;
|
||||
let (terminal_width, terminal_height) =
|
||||
(area.width as usize, area.height as usize);
|
||||
let content = match action {
|
||||
KeyAction::Tall => generate_tall_content(terminal_width, terminal_height),
|
||||
KeyAction::Normal => generate_test_line(line_num, terminal_width),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let marked_line = build_marked_line(line_num, &content);
|
||||
let colored_content =
|
||||
colorize_content(&marked_line, colors[(line_num - 1) % colors.len()]);
|
||||
|
||||
// Save to scrollback history, replace LF with CRLF
|
||||
if !scrollback_history.is_empty() {
|
||||
scrollback_history.push_str("\r\n");
|
||||
}
|
||||
scrollback_history.push_str(&colored_content.replace('\n', "\r\n"));
|
||||
|
||||
// Emit to scrollback and render viewport
|
||||
with_synchronized_output(&mut terminal, |terminal| {
|
||||
emit_to_scrollback(terminal, &colored_content)?;
|
||||
render_viewport(terminal, &spinner, viewport_height)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
line_num += 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
restore_terminal(&mut terminal)?;
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Reference in a new issue