From 5179b9a827cd889fdf72d014aa9cee0c2cc6f0f1 Mon Sep 17 00:00:00 2001 From: sigoden Date: Sun, 5 Mar 2023 00:01:45 +0800 Subject: feat: add `.editor` command (#9) * feat: add `.editor` command Use `.editor` to edit/input multiline text * support paste --- src/editor.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/render.rs | 1 - src/repl.rs | 26 +++++++------- 4 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 src/editor.rs (limited to 'src') 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 { + 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 { + 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 { + 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, } -- cgit v1.2.3