mirror of
https://github.com/sigoden/aichat
synced 2024-11-16 06:15:26 +00:00
refactor: no need tui, just render stream directly (#8)
This commit is contained in:
parent
afe3f23831
commit
069583098f
91
Cargo.lock
generated
91
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
267
src/render.rs
267
src/render.rs
@ -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 {
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user