summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/editor.rs114
-rw-r--r--src/main.rs1
-rw-r--r--src/render.rs1
-rw-r--r--src/repl.rs26
4 files changed, 127 insertions, 15 deletions
diff --git a/src/editor.rs b/src/editor.rs
new file mode 100644
index 0000000..8355c01
--- /dev/null
+++ b/src/editor.rs
@@ -0,0 +1,114 @@
+use std::io::{self, Stdout, Write};
+
+use anyhow::{anyhow, Result};
+use copypasta::{ClipboardContext, ClipboardProvider};
+
+use crossterm::{
+ cursor,
+ event::{self, Event, KeyCode, KeyModifiers},
+ queue, style,
+ terminal::{self, disable_raw_mode, enable_raw_mode},
+};
+
+pub fn edit() -> Result<String> {
+ enable_raw_mode()?;
+ let mut stdout = io::stdout();
+
+ let ret = edit_inner(&mut stdout);
+
+ // restore terminal
+ disable_raw_mode()?;
+
+ ret
+}
+
+fn edit_inner(writer: &mut Stdout) -> Result<String> {
+ let mut session = Session::new(writer);
+
+ loop {
+ let evt = event::read()?;
+ if let Event::Key(key) = evt {
+ match key.code {
+ KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
+ // quit
+ return Ok(String::new());
+ }
+ KeyCode::Char('d') if key.modifiers == KeyModifiers::CONTROL => {
+ // submit
+ return Ok(session.buffer);
+ }
+ KeyCode::Char('v') if key.modifiers == KeyModifiers::CONTROL => {
+ let content = paste()?;
+ session.push_str(&content)?;
+ }
+ KeyCode::Char(c)
+ if matches!(key.modifiers, KeyModifiers::NONE | KeyModifiers::SHIFT) =>
+ {
+ session.push(c)?;
+ }
+ KeyCode::Enter => {
+ session.push('\n')?;
+ }
+ _ => {}
+ }
+ }
+ session.flush()?;
+ }
+}
+
+struct Session<'a, T: Write> {
+ writer: &'a mut T,
+ buffer: String,
+ dirty: bool,
+}
+
+impl<'a, T: Write> Session<'a, T> {
+ fn new<'b: 'a>(writer: &'b mut T) -> Self {
+ Self {
+ buffer: String::new(),
+ writer,
+ dirty: false,
+ }
+ }
+ fn push(&mut self, ch: char) -> io::Result<()> {
+ if ch == '\n' {
+ self.new_line()?;
+ } else {
+ queue!(self.writer, style::Print(ch))?;
+ }
+ self.buffer.push(ch);
+ self.dirty = true;
+ Ok(())
+ }
+ fn push_str(&mut self, text: &str) -> io::Result<()> {
+ for line in text.lines() {
+ if !line.is_empty() {
+ queue!(self.writer, style::Print(line))?;
+ }
+ self.new_line()?;
+ }
+
+ Ok(())
+ }
+ fn new_line(&mut self) -> io::Result<()> {
+ let (_, y) = cursor::position()?;
+ let (_, h) = terminal::size()?;
+ if y == h - 1 {
+ queue!(self.writer, terminal::ScrollUp(1), cursor::MoveTo(0, y))?;
+ } else {
+ queue!(self.writer, cursor::MoveToNextLine(1))?;
+ }
+ Ok(())
+ }
+ fn flush(&mut self) -> io::Result<()> {
+ if self.dirty {
+ return self.writer.flush();
+ }
+ Ok(())
+ }
+}
+
+fn paste() -> Result<String> {
+ let mut ctx = ClipboardContext::new().map_err(|err| anyhow!("{err}"))?;
+ ctx.get_contents().map_err(|err| anyhow!("{err}"))
+}
diff --git a/src/main.rs b/src/main.rs
index 72a6d12..add1487 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
mod cli;
mod client;
mod config;
+mod editor;
mod render;
mod repl;
diff --git a/src/render.rs b/src/render.rs
index 4af0d59..b049051 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -27,7 +27,6 @@ pub fn render_stream(
}
if let Ok(evt) = rx.try_recv() {
match evt {
- RenderStreamEvent::Start(_) => {}
RenderStreamEvent::Text(text) => {
buffer.push_str(&text);
if text.contains('\n') {
diff --git a/src/repl.rs b/src/repl.rs
index 3ccc7e4..127ec72 100644
--- a/src/repl.rs
+++ b/src/repl.rs
@@ -1,5 +1,6 @@
use crate::client::ChatGptClient;
use crate::config::{Config, Role};
+use crate::editor;
use crate::render::{self, MarkdownRender};
use anyhow::{anyhow, Result};
use crossbeam::channel::{unbounded, Sender};
@@ -16,10 +17,11 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::spawn;
-const REPL_COMMANDS: [(&str, &str); 7] = [
+const REPL_COMMANDS: [(&str, &str); 8] = [
(".clear", "Clear the screen"),
(".clear-history", "Clear the history"),
(".clear-role", "Clear the role status"),
+ (".editor", "Enter multiline editor"),
(".exit", "Exit the REPL"),
(".help", "Print this help message"),
(".history", "Print the history"),
@@ -129,6 +131,15 @@ impl Repl {
handler.handle(ReplCmd::UnsetRole)?;
dump("", 1);
}
+ ".editor" => {
+ dump(
+ "// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)",
+ 1,
+ );
+ let content = editor::edit()?;
+ dump("", 1);
+ handler.handle(ReplCmd::Input(content))?;
+ }
_ => dump_unknown_command(),
}
} else {
@@ -242,7 +253,6 @@ impl ReplCmdHandler {
} else {
ReplyReceiver::new(None)
};
- receiver.start(&input);
self.client
.acquire_stream(&input, prompt, &mut receiver, self.ctrlc.clone())?;
Config::save_message(
@@ -283,17 +293,6 @@ impl ReplyReceiver {
}
}
- fn start(&self, input: &str) {
- match self.sender.as_ref() {
- Some(tx) => {
- let _ = tx.send(RenderStreamEvent::Start(input.to_string()));
- }
- None => {
- dump("", 2);
- }
- }
- }
-
pub fn text(&mut self, text: &str) {
match self.sender.as_ref() {
Some(tx) => {
@@ -319,7 +318,6 @@ impl ReplyReceiver {
}
pub enum RenderStreamEvent {
- Start(String),
Text(String),
Done,
}