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 --- Cargo.lock | 320 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + src/editor.rs | 114 +++++++++++++++++++++ src/main.rs | 1 + src/render.rs | 1 - src/repl.rs | 26 +++-- 6 files changed, 447 insertions(+), 17 deletions(-) create mode 100644 src/editor.rs diff --git a/Cargo.lock b/Cargo.lock index ec57f28..549539e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,9 @@ dependencies = [ "anyhow", "bytes", "clap", + "copypasta", "crossbeam", + "crossterm 0.26.1", "dirs", "eventsource-stream", "futures-util", @@ -160,6 +162,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "brotli" version = "3.3.4" @@ -278,6 +286,16 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -294,6 +312,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "copypasta" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133fc8675ee3a4ec9aa513584deda9aa0faeda3586b87f7f0f2ba082c66fb172" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -363,7 +395,7 @@ dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset", + "memoffset 0.8.0", "scopeguard", ] @@ -419,6 +451,22 @@ 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" @@ -504,6 +552,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "dyn-clone" version = "1.0.11" @@ -730,6 +793,16 @@ dependencies = [ "slab", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "gethostname" version = "0.4.1" @@ -1063,6 +1136,12 @@ dependencies = [ "arrayvec 0.7.2", ] +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + [[package]] name = "lazy_static" version = "1.4.0" @@ -1081,6 +1160,16 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "link-cplusplus" version = "1.0.8" @@ -1115,6 +1204,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1136,7 +1234,7 @@ dependencies = [ "clap", "clap_complete", "env_proxy", - "gethostname", + "gethostname 0.4.1", "image", "libc", "mime", @@ -1168,6 +1266,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.8.0" @@ -1238,6 +1345,18 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + [[package]] name = "nom" version = "7.1.3" @@ -1298,6 +1417,35 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -1389,6 +1537,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + [[package]] name = "png" version = "0.17.7" @@ -1750,6 +1904,12 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -1938,6 +2098,34 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smithay-client-toolkit" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f307c47d32d2715eb2e0ece5589057820e0e5e70d07c247d1063e844e107f454" +dependencies = [ + "bitflags", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix", + "pkg-config", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + +[[package]] +name = "smithay-clipboard" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a345c870a1fae0b1b779085e81b51e614767c239e93503588e54c5b17f4b0e8" +dependencies = [ + "smithay-client-toolkit", + "wayland-client", +] + [[package]] name = "socket2" version = "0.4.7" @@ -2615,6 +2803,79 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-client" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +dependencies = [ + "bitflags", + "downcast-rs", + "libc", + "nix", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" +dependencies = [ + "nix", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +dependencies = [ + "nix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +dependencies = [ + "bitflags", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.61" @@ -2675,6 +2936,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2786,6 +3056,52 @@ dependencies = [ "winapi", ] +[[package]] +name = "x11-clipboard" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0827f86aa910c4e73329a4f619deabe88ebb4b042370bf023c2d5d8b4eb54695" +dependencies = [ + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +dependencies = [ + "gethostname 0.2.3", + "nix", + "winapi", + "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +dependencies = [ + "nix", +] + +[[package]] +name = "xcursor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" +dependencies = [ + "nom", +] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + [[package]] name = "xmlparser" version = "0.13.5" diff --git a/Cargo.toml b/Cargo.toml index 1879e77..92c63eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ 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" +crossterm = "0.26.1" +copypasta = "0.8.2" [dependencies.syntect] version = "5.0.0" 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