refactor: no need tui, just render stream directly (#8)

This commit is contained in:
sigoden 2023-03-04 22:18:42 +08:00 committed by GitHub
parent afe3f23831
commit 069583098f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 37 additions and 342 deletions

91
Cargo.lock generated
View File

@ -8,17 +8,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.20"
@ -32,12 +21,10 @@ dependencies = [
name = "aichat"
version = "0.2.0"
dependencies = [
"ansi-to-tui",
"anyhow",
"bytes",
"clap",
"crossbeam",
"crossterm 0.26.1",
"dirs",
"eventsource-stream",
"futures-util",
@ -51,9 +38,7 @@ dependencies = [
"serde_json",
"serde_yaml",
"syntect",
"textwrap",
"tokio",
"tui",
]
[[package]]
@ -80,17 +65,6 @@ dependencies = [
"libc",
]
[[package]]
name = "ansi-to-tui"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3460d7beaf8b192c09a55933da038ccd514f00efdb37d7d87f3ce078336b47e9"
dependencies = [
"nom",
"thiserror",
"tui",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
@ -231,12 +205,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.79"
@ -451,22 +419,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "crossterm"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
dependencies = [
"bitflags",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.0"
@ -854,9 +806,6 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "heck"
@ -1989,12 +1938,6 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "socket2"
version = "0.4.7"
@ -2144,17 +2087,6 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "textwrap"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.38"
@ -2404,19 +2336,6 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633"
[[package]]
name = "tui"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
dependencies = [
"bitflags",
"cassowary",
"crossterm 0.25.0",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "unicase"
version = "2.6.0"
@ -2456,16 +2375,6 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
[[package]]
name = "unicode-linebreak"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown",
"regex",
]
[[package]]
name = "unicode-normalization"
version = "0.1.22"

View File

@ -16,7 +16,6 @@ keywords = ["chatgpt", "ai"]
anyhow = "1.0.69"
bytes = "1.4.0"
clap = { version = "4.1.8", features = ["derive", "string"] }
crossterm = "0.26.1"
dirs = "4.0.0"
eventsource-stream = "0.2.3"
futures-util = "0.3.26"
@ -31,9 +30,6 @@ tokio = { version = "1.26.0", features = ["full"] }
mdcat = { version = "1.1.0", default_features = false, features =["static"] }
pulldown-cmark = { version = "0.9.2", default-features = false, features = ['simd'] }
crossbeam = "0.8.2"
tui = "0.19.0"
ansi-to-tui = "2.0.0"
textwrap = "0.16.0"
[dependencies.syntect]
version = "5.0.0"

View File

@ -60,10 +60,10 @@ The default config dir is as follows, You can override config dir with `$AICHAT_
aichat may generate the following files in the config dir:
- `config.yaml`: the config file.
- `roles.yaml`: the roles definition file.
- `history.txt`: the repl history file.
- `messages.md`: the chat messages storage file.
- `config.yaml`: configuration file.
- `roles.yaml`: role definition file.
- `history.txt`: repl history file.
- `messages.md`: chat messages file.
### Roles

View File

@ -1,24 +1,14 @@
use anyhow::Result;
use crossbeam::channel::Receiver;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
style::{self, Color},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use mdcat::{
push_tty,
terminal::{TerminalProgram, TerminalSize},
Environment, ResourceAccess, Settings,
};
use pulldown_cmark::Parser;
use std::{
io,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::{Duration, Instant},
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use syntect::parsing::SyntaxSet;
@ -29,235 +19,44 @@ pub fn render_stream(
ctrlc: Arc<AtomicBool>,
markdown_render: Arc<MarkdownRender>,
) -> Result<()> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = tui::backend::CrosstermBackend::new(stdout);
let mut terminal = tui::Terminal::new(backend)?;
// create app and run it
let app = render_stream_tui::App::new(ctrlc, markdown_render);
let res = render_stream_tui::run(&mut terminal, app, rx);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
res
}
mod render_stream_tui {
use super::*;
use ansi_to_tui::IntoText;
use crossterm::event::MouseEventKind;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout},
style::Style,
widgets::{List, ListItem, ListState},
Frame, Terminal,
};
pub fn run<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
rx: Receiver<RenderStreamEvent>,
) -> Result<()> {
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(250);
loop {
terminal.draw(|f| ui(f, &mut app))?;
if app.ctrlc.load(Ordering::SeqCst) {
return Ok(());
}
if let Ok(evt) = rx.try_recv() {
let want_to_flush = app.handle(evt)?;
if !want_to_flush {
continue;
}
}
if app.is_done() {
return Ok(());
}
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
match event::read()? {
Event::Key(key) => match key.code {
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
app.quit();
return Ok(());
}
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
_ => {}
},
Event::Mouse(ev) => match ev.kind {
MouseEventKind::ScrollDown => app.next(),
MouseEventKind::ScrollUp => app.previous(),
_ => {}
},
_ => {}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
let mut buffer = String::new();
let mut line_index = 0;
loop {
if ctrlc.load(Ordering::SeqCst) {
return Ok(());
}
}
pub struct App {
buffer: String,
items: Vec<String>,
list_state: ListState,
finish_at: Option<Instant>,
ctrlc: Arc<AtomicBool>,
markdown_render: Arc<MarkdownRender>,
auto_scroll: bool,
num_rows: usize,
num_cols: usize,
entry_index: usize,
}
impl App {
pub fn new(ctrlc: Arc<AtomicBool>, markdown_render: Arc<MarkdownRender>) -> Self {
Self {
buffer: String::new(),
ctrlc,
finish_at: None,
markdown_render,
num_rows: 0,
num_cols: 0,
auto_scroll: true,
entry_index: 0,
items: vec![],
list_state: ListState::default(),
}
}
pub fn handle(&mut self, evt: RenderStreamEvent) -> Result<bool> {
let want_to_flush = match evt {
RenderStreamEvent::Start(question) => {
let mut buf = Vec::with_capacity(8);
execute!(
buf,
style::SetForegroundColor(Color::Cyan),
style::Print(""),
style::ResetColor
)?;
let indicator = String::from_utf8_lossy(&buf);
self.buffer.push_str(&format!("{indicator}{question}\n"));
true
}
if let Ok(evt) = rx.try_recv() {
match evt {
RenderStreamEvent::Start(_) => {}
RenderStreamEvent::Text(text) => {
self.buffer.push_str(&text);
text.contains('\n')
}
RenderStreamEvent::Done => {
self.finish_at = Some(Instant::now());
true
}
};
let wrapped_buffer = textwrap::wrap(&self.buffer, self.num_cols).join("\n");
let markdown = self.markdown_render.render(&wrapped_buffer)?;
self.items = markdown.split('\n').map(|v| v.to_string()).collect();
if self.auto_scroll {
self.scroll_to_end();
}
Ok(want_to_flush)
}
pub fn quit(&mut self) {
self.ctrlc.store(true, Ordering::SeqCst);
}
pub fn is_done(&mut self) -> bool {
if self.auto_scroll {
if let Some(finish_at) = self.finish_at {
if finish_at.elapsed() >= Duration::from_secs(1) {
return true;
buffer.push_str(&text);
if text.contains('\n') {
let markdown = markdown_render.render(&buffer)?;
let lines: Vec<&str> = markdown.lines().collect();
let (_, print_lines) = lines.split_at(line_index);
let mut print_lines = print_lines.to_vec();
print_lines.pop();
if !print_lines.is_empty() {
line_index += print_lines.len();
dump(print_lines.join("\n").to_string(), 1);
}
}
}
RenderStreamEvent::Done => {
let markdown = markdown_render.render(&buffer)?;
let tail = markdown
.lines()
.skip(line_index)
.collect::<Vec<&str>>()
.join("\n");
dump(tail, 2);
break;
}
}
false
}
pub fn set_size(&mut self, rows: u16, cols: u16) {
self.num_rows = rows as usize;
self.num_cols = cols as usize;
}
pub fn next(&mut self) {
self.auto_scroll = false;
let index = if self.entry_index < self.num_rows {
self.num_rows.min(self.items.len() - 1)
} else {
self.entry_index + 1
};
self.entry_index = index;
self.list_state.select(Some(index));
}
pub fn previous(&mut self) {
self.auto_scroll = false;
let index = self
.entry_index
.saturating_sub(1)
.min(self.items.len().saturating_sub(self.num_rows + 1));
self.entry_index = index;
self.list_state.select(Some(index));
}
pub fn scroll_to_end(&mut self) {
let len = self.items.len();
self.entry_index = if len < self.num_rows {
0
} else {
len - self.num_rows
};
self.list_state.select(Some(len - 1))
}
pub fn on_tick(&mut self) {}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
// Create two chunks with equal horizontal screen space
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(100)].as_ref())
.split(f.size());
let items: Vec<ListItem> = app
.items
.iter()
.map(|line| {
let text = line.into_text().unwrap_or_default();
ListItem::new(text)
})
.collect();
let area = chunks[0];
app.set_size(area.height, area.width);
let items = List::new(items).highlight_style(Style::default());
f.render_stateful_widget(items, area, &mut app.list_state);
}
Ok(())
}
pub struct MarkdownRender {

View File

@ -251,15 +251,6 @@ impl ReplCmdHandler {
&receiver.output,
);
wg.wait();
match self.render.clone() {
Some(markdown_render) => {
markdown_render.print(&receiver.output)?;
dump("", 1);
}
None => {
dump(&receiver.output, 2);
}
}
self.state.borrow_mut().output = receiver.output;
}
ReplCmd::SetRole(name) => match self.config.find_role(&name) {