You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aichat/src/render/repl.rs

136 lines
4.4 KiB
Rust

use super::MarkdownRender;
use crate::repl::{ReplyStreamEvent, SharedAbortSignal};
use anyhow::Result;
use crossbeam::channel::Receiver;
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyModifiers},
queue, style,
terminal::{self, disable_raw_mode, enable_raw_mode},
};
use std::{
io::{self, Stdout, Write},
time::{Duration, Instant},
};
use unicode_width::UnicodeWidthStr;
pub fn repl_render_stream(
rx: Receiver<ReplyStreamEvent>,
light_theme: bool,
abort: SharedAbortSignal,
) -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
let ret = repl_render_stream_inner(rx, light_theme, abort, &mut stdout);
disable_raw_mode()?;
ret
}
fn repl_render_stream_inner(
rx: Receiver<ReplyStreamEvent>,
light_theme: bool,
abort: SharedAbortSignal,
writer: &mut Stdout,
) -> Result<()> {
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(100);
let mut buffer = String::new();
let mut markdown_render = MarkdownRender::new(light_theme);
let terminal_columns = terminal::size()?.0;
loop {
if abort.aborted() {
return Ok(());
}
if let Ok(evt) = rx.try_recv() {
recover_cursor(writer, terminal_columns, &buffer)?;
match evt {
ReplyStreamEvent::Text(text) => {
if text.contains('\n') {
let text = format!("{buffer}{text}");
let mut lines: Vec<&str> = text.split('\n').collect();
buffer = lines.pop().unwrap_or_default().to_string();
let output = markdown_render.render(&lines.join("\n"));
for line in output.split('\n') {
queue!(
writer,
style::Print(line),
style::Print("\n"),
cursor::MoveLeft(terminal_columns),
)?;
}
queue!(writer, style::Print(&buffer),)?;
} else {
buffer = format!("{buffer}{text}");
let output = markdown_render.render_line_stateless(&buffer);
queue!(writer, style::Print(&output))?;
}
writer.flush()?;
}
ReplyStreamEvent::Done => {
let output = markdown_render.render_line_stateless(&buffer);
let trimed_output = output.trim_end();
if !trimed_output.is_empty() {
queue!(writer, style::Print(output.trim_end()))?;
writer.flush()?;
}
let (_, row) = cursor::position()?;
queue!(writer, cursor::MoveTo(0, row), style::Print("\n\n"))?;
writer.flush()?;
break;
}
}
continue;
}
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
abort.set_ctrlc();
return Ok(());
}
KeyCode::Char('d') if key.modifiers == KeyModifiers::CONTROL => {
abort.set_ctrld();
return Ok(());
}
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
last_tick = Instant::now();
}
}
Ok(())
}
fn recover_cursor(writer: &mut Stdout, terminal_columns: u16, buffer: &str) -> Result<()> {
let buffer_rows = (buffer.width() as u16 + terminal_columns - 1) / terminal_columns;
let (_, row) = cursor::position()?;
if buffer_rows == 0 {
queue!(writer, cursor::MoveTo(0, row))?;
} else if row + 1 >= buffer_rows {
queue!(writer, cursor::MoveTo(0, row + 1 - buffer_rows))?;
} else {
queue!(
writer,
terminal::ScrollUp(buffer_rows - 1 - row),
cursor::MoveTo(0, 0)
)?;
}
Ok(())
}