summaryrefslogtreecommitdiffstats
path: root/src/editor.rs
diff options
context:
space:
mode:
authorsigoden <sigoden@gmail.com>2023-03-05 00:01:45 +0800
committerGitHub <noreply@github.com>2023-03-05 00:01:45 +0800
commit5179b9a827cd889fdf72d014aa9cee0c2cc6f0f1 (patch)
tree482d44577d4727cd127bac0b3919958fe43d34c8 /src/editor.rs
parent069583098f4d168879740b5cb37755cd5a2a568f (diff)
downloadaichat-5179b9a827cd889fdf72d014aa9cee0c2cc6f0f1.tar.gz
feat: add `.editor` command (#9)
* feat: add `.editor` command Use `.editor` to edit/input multiline text * support paste
Diffstat (limited to 'src/editor.rs')
-rw-r--r--src/editor.rs114
1 files changed, 114 insertions, 0 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}"))
+}