summaryrefslogtreecommitdiffstats
path: root/src/render/repl.rs
diff options
context:
space:
mode:
authorsigoden <sigoden@gmail.com>2023-03-07 22:33:34 +0800
committerGitHub <noreply@github.com>2023-03-07 22:33:34 +0800
commit360264121ca2931cb7f2fa40594ba590b245ace5 (patch)
tree71e81208cf50b02116c89974aca17a3cc762b1fc /src/render/repl.rs
parentc7fcdb174440817706b818154a6bbcaf58948caa (diff)
downloadaichat-360264121ca2931cb7f2fa40594ba590b245ace5.tar.gz
feat: command mode supports stream out (#31)
* feat: command mode supports stream out * update cli
Diffstat (limited to 'src/render/repl.rs')
-rw-r--r--src/render/repl.rs123
1 files changed, 123 insertions, 0 deletions
diff --git a/src/render/repl.rs b/src/render/repl.rs
new file mode 100644
index 0000000..96c6364
--- /dev/null
+++ b/src/render/repl.rs
@@ -0,0 +1,123 @@
+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>, abort: SharedAbortSignal) -> Result<()> {
+ enable_raw_mode()?;
+ let mut stdout = io::stdout();
+ queue!(stdout, event::DisableMouseCapture)?;
+
+ let ret = repl_render_stream_inner(rx, abort, &mut stdout);
+
+ queue!(stdout, event::DisableMouseCapture)?;
+ disable_raw_mode()?;
+
+ ret
+}
+
+fn repl_render_stream_inner(
+ rx: Receiver<ReplyStreamEvent>,
+ 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();
+ 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);
+ queue!(writer, style::Print(output), style::Print("\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(())
+}