diff --git a/config/keymap.toml b/config/keymap.toml index d7014ea5..3040e4fc 100644 --- a/config/keymap.toml +++ b/config/keymap.toml @@ -130,5 +130,9 @@ keymap = [ { on = [ "d" ], exec = "delete" }, { on = [ "c" ], exec = "delete --insert" }, - { on = [ "x" ], exec = [ "delete", "move 1 --in-operating" ] } + { on = [ "x" ], exec = [ "delete", "move 1 --in-operating" ] }, + + { on = [ "y" ], exec = [ "yank" ] }, + { on = [ "p" ], exec = [ "paste" ] }, + { on = [ "P" ], exec = [ "paste --before" ] } ] diff --git a/docs/keymap.md b/docs/keymap.md index bebcce58..ec980f9f 100644 --- a/docs/keymap.md +++ b/docs/keymap.md @@ -137,8 +137,14 @@ - `--end-of-word`: Move to the end of the next word. - delete: Delete the selected characters. + - `--insert`: Delete and enter insert mode. +- yank: Copy the selected characters. +- paste: Paste the copied characters after the cursor. + + - `--before`: Paste the copied characters before the cursor. + ### Insert mode - close: Cancel input. diff --git a/src/core/external/clipboard.rs b/src/core/external/clipboard.rs new file mode 100644 index 00000000..e39813d5 --- /dev/null +++ b/src/core/external/clipboard.rs @@ -0,0 +1,31 @@ +use std::process::Stdio; + +use anyhow::{bail, Result}; +use tokio::{io::AsyncWriteExt, process::Command}; + +pub async fn clipboard_get() -> Result { + for cmd in &["pbpaste", "wl-paste"] { + let output = Command::new(cmd).kill_on_drop(true).output().await?; + if output.status.success() { + return Ok(String::from_utf8_lossy(&output.stdout).to_string()); + } + } + + bail!("failed to get clipboard") +} + +pub async fn clipboard_set(s: &str) -> Result<()> { + for cmd in &["pbcopy", "wl-copy"] { + let mut child = + Command::new(cmd).stdin(Stdio::piped()).stdout(Stdio::null()).kill_on_drop(true).spawn()?; + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(s.as_bytes()).await?; + } + if child.wait().await?.success() { + return Ok(()); + } + } + + bail!("failed to set clipboard") +} + diff --git a/src/core/external/mod.rs b/src/core/external/mod.rs index aa73bfde..7ff19ab0 100644 --- a/src/core/external/mod.rs +++ b/src/core/external/mod.rs @@ -1,3 +1,4 @@ +mod clipboard; mod fd; mod ffmpegthumbnailer; mod file; @@ -8,6 +9,7 @@ mod rg; mod unar; mod zoxide; +pub use clipboard::*; pub use fd::*; pub use ffmpegthumbnailer::*; pub use file::*; diff --git a/src/core/input/input.rs b/src/core/input/input.rs index 3ba5552e..435a3cd8 100644 --- a/src/core/input/input.rs +++ b/src/core/input/input.rs @@ -3,7 +3,7 @@ use ratatui::layout::Rect; use tokio::sync::oneshot::Sender; use unicode_width::UnicodeWidthStr; -use crate::{core::Position, misc::CharKind}; +use crate::{core::{external, Position}, misc::CharKind}; #[derive(Default)] pub struct Input { @@ -12,7 +12,7 @@ pub struct Input { position: (u16, u16), op: InputOp, - range: Option<(usize, usize)>, + start: Option, mode: InputMode, offset: usize, @@ -35,23 +35,17 @@ pub enum InputMode { Insert, } +impl InputMode { + #[inline] + fn delta(&self) -> usize { (*self != InputMode::Insert) as usize } +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum InputOp { #[default] None, - Delete(bool, bool), - Yank(bool), -} - -impl InputOp { - #[inline] - fn set_include(&mut self) { - match self { - Self::Delete(_, ref mut b) => *b = true, - Self::Yank(ref mut b) => *b = true, - _ => (), - } - } + Delete(bool), + Yank, } impl Input { @@ -77,7 +71,7 @@ impl Input { } self.op = InputOp::None; - self.range = None; + self.start = None; self.mode = InputMode::Insert; self.visible = false; @@ -87,7 +81,8 @@ impl Input { pub fn escape(&mut self) -> bool { match self.mode { InputMode::Normal => { - self.range = None; + self.op = InputOp::None; + self.start = None; } InputMode::Insert => { self.mode = InputMode::Normal; @@ -102,6 +97,8 @@ impl Input { return false; } + self.op = InputOp::None; + self.start = None; self.mode = InputMode::Insert; if append { self.move_(1); @@ -112,36 +109,30 @@ impl Input { pub fn visual(&mut self) -> bool { if self.mode != InputMode::Normal { return false; + } else if self.value.is_empty() { + return false; } - self.range = Some((self.cursor, self.cursor)); + self.start = Some(self.cursor); true } pub fn move_(&mut self, step: isize) -> bool { - let old = self.cursor; + let b = self.handle_op( + if step <= 0 { + self.cursor.saturating_sub(step.abs() as usize) + } else { + self.count().min(self.cursor + step as usize) + }, + false, + ); - if step <= 0 { - self.cursor = self.cursor.saturating_sub(step.abs() as usize); - } else { - let count = self.count(); - self.cursor += step as usize; - - if self.cursor >= count { - self.op.set_include(); - self.cursor = if self.mode == InputMode::Insert { count } else { count.saturating_sub(1) }; - } + if self.cursor < self.offset { + self.offset = self.cursor; + } else if self.cursor > self.offset + 50 { + self.offset = self.cursor.saturating_sub(50); } - - if self.cursor != old { - if self.cursor < self.offset { - self.offset = self.cursor; - } else if self.cursor > self.offset + 50 { - self.offset = self.cursor.saturating_sub(50); - } - } - - self.handle_op() || self.cursor != old + b } #[inline] @@ -151,7 +142,7 @@ impl Input { pub fn backward(&mut self) -> bool { if self.cursor == 0 { - return self.handle_op(); + return self.move_(0); } let idx = self.idx(self.cursor).unwrap_or(self.value.len()); @@ -173,10 +164,7 @@ impl Input { pub fn forward(&mut self, end: bool) -> bool { if self.value.is_empty() { - return self.handle_op(); - } - if end { - self.op.set_include(); + return self.move_(0); } let mut it = self.value.chars().skip(self.cursor).enumerate(); @@ -188,7 +176,9 @@ impl Input { } else { c != CharKind::Space && c != prev }; - if b { + if b && self.op != InputOp::None { + return self.move_(i as isize); + } else if b { return self.move_(if end { i - 1 } else { i } as isize); } prev = c; @@ -222,21 +212,17 @@ impl Input { pub fn delete(&mut self, insert: bool) -> bool { match self.op { InputOp::None => { - self.op = InputOp::Delete(insert, false); - if self.range.is_some() { - return self.handle_op(); + self.op = InputOp::Delete(insert); + if self.start.is_some() { + return self.handle_op(self.cursor, true).then(|| self.move_(0)).is_some(); } - self.range = Some((self.cursor, self.cursor)); + self.start = Some(self.cursor); false } InputOp::Delete(..) => { self.move_(-(self.value.len() as isize)); self.value.clear(); - - self.op = InputOp::None; - self.range = None; - self.mode = if insert { InputMode::Insert } else { InputMode::Normal }; true } @@ -244,32 +230,81 @@ impl Input { } } - fn handle_op(&mut self) -> bool { - if let Some(ref mut range) = self.range { - *range = (range.0.min(self.cursor), range.0.max(self.cursor)); + pub fn yank(&mut self) -> bool { + match self.op { + InputOp::None => { + self.op = InputOp::Yank; + if self.start.is_some() { + return self.handle_op(self.cursor, true).then(|| self.move_(0)).is_some(); + } + + self.start = Some(self.cursor); + false + } + InputOp::Yank => { + self.start = Some(0); + self.move_(self.value.len() as isize); + false + } + _ => false, + } + } + + pub fn paste(&mut self, before: bool) -> bool { + if self.start.is_some() { + self.op = InputOp::Delete(false); + self.handle_op(self.cursor, true); } - match self.op { - InputOp::None => return false, - InputOp::Delete(insert, include) => { - let range = self.range.take().unwrap(); - if !self.value.is_empty() { - let (start, end) = (self.idx(range.0), self.idx(range.1 + include as usize)); - self.value.drain(start.unwrap()..end.unwrap()); - } - self.mode = if insert { - self.cursor = range.0.min(self.count()); - InputMode::Insert - } else { - self.cursor = range.0.min(self.count().saturating_sub(1)); - InputMode::Normal - }; - } - InputOp::Yank(include) => {} + let str = + futures::executor::block_on(async { external::clipboard_get().await }).unwrap_or_default(); + if str.is_empty() { + return false; } + self.insert(!before); + for c in str.chars() { + self.type_(c); + } + self.escape(); + true + } + + fn handle_op(&mut self, cursor: usize, include: bool) -> bool { + let old = self.cursor; + let range = if self.op == InputOp::None { None } else { self.range(cursor, include) }; + + let b = match self.op { + InputOp::None => { + self.cursor = cursor; + false + } + InputOp::Delete(insert) => { + let range = range.unwrap(); + let old_mode = self.mode; + + let (start, end) = (self.idx(range.0), self.idx(range.1)); + self.value.drain(start.unwrap()..end.unwrap()); + self.mode = if insert { InputMode::Insert } else { InputMode::Normal }; + self.cursor = range.0; + + start != end || self.mode != old_mode + } + InputOp::Yank => { + let range = range.unwrap(); + let (start, end) = (self.idx(range.0), self.idx(range.1)); + let yanked = &self.value[start.unwrap()..end.unwrap()]; + + futures::executor::block_on(async { + external::clipboard_set(yanked).await.ok(); + }); + start != end + } + }; + self.op = InputOp::None; - true + self.cursor = self.count().saturating_sub(self.mode.delta()).min(self.cursor); + b || self.cursor != old } } @@ -303,35 +338,22 @@ impl Input { (area.x + width + 1, area.y + 1) } - pub fn range(&self) -> Option { - if let Some((start, end)) = self.range { - let end = self - .value - .chars() - .skip(start) - .enumerate() - .take_while(|(i, _)| *i < end) - .map(|(_, c)| c) - .collect::() - .width() as u16; - - let start = self - .value - .chars() - .enumerate() - .take_while(|(i, _)| *i < start) - .map(|(_, c)| c) - .collect::() - .width() as u16; - - return Some(Rect { - x: self.position.0 + 1 + start, - y: self.position.1 + 3, - width: end, - height: 1, - }); + pub fn selected(&self) -> Option { + if self.start.is_none() { + return None; } - None + + let start = self.start.unwrap(); + let (start, end) = + if start < self.cursor { (start, self.cursor) } else { (self.cursor + 1, start + 1) }; + let (start, end) = (self.idx(start).unwrap(), self.idx(end).unwrap()); + + Some(Rect { + x: self.position.0 + 1 + self.value[..start].width() as u16, + y: self.position.1 + 3, + width: self.value[start..end].width() as u16, + height: 1, + }) } #[inline] @@ -346,4 +368,13 @@ impl Input { .map(|(i, _)| i) .or_else(|| if n == self.count() { Some(self.value.len()) } else { None }) } + + #[inline] + fn range(&mut self, cursor: usize, include: bool) -> Option<(usize, usize)> { + self + .start + .take() + .map(|s| if s <= cursor { (s, cursor) } else { (cursor, s) }) + .map(|(s, e)| (s, e + include as usize)) + } } diff --git a/src/ui/dispatcher.rs b/src/ui/dispatcher.rs index d4bda72e..dc5699ff 100644 --- a/src/ui/dispatcher.rs +++ b/src/ui/dispatcher.rs @@ -1,5 +1,3 @@ -use crossterm::event::KeyCode; - use super::Ctx; use crate::{config::{keymap::{Exec, Key, Single}, KEYMAP}, core::input::InputMode, misc::optinal_bool}; @@ -35,7 +33,12 @@ impl Executor { } else if layer == 2 { render = Self::select(cx, e) || render; } else if layer == 3 { - render = Self::input(cx, Some(e), &key) || render; + if cx.input.mode() != InputMode::Insert || key.plain().is_none() { + render = Self::input(cx, Some(e), &key) || render; + } else { + render = Self::input(cx, None, &key) || render; + break; + } } } } @@ -43,6 +46,7 @@ impl Executor { if layer == 3 && !matched { render = Self::input(cx, None, &key); } + render } @@ -171,29 +175,23 @@ impl Executor { e } else { if cx.input.mode() == InputMode::Insert { - if let KeyCode::Char(c) = key.code { + if let Some(c) = key.plain() { return cx.input.type_(c); } } return false; }; - if cx.input.mode() != InputMode::Insert || key.plain().is_none() { - match exec.cmd.as_str() { - "close" => return cx.input.close(exec.named.contains_key("submit")), - "escape" => return cx.input.escape(), + match exec.cmd.as_str() { + "close" => return cx.input.close(exec.named.contains_key("submit")), + "escape" => return cx.input.escape(), - "move" => { - let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); - let in_operating = exec.named.contains_key("in-operating"); - return if in_operating { - cx.input.move_in_operating(step) - } else { - cx.input.move_(step) - }; - } - _ => {} + "move" => { + let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); + let in_operating = exec.named.contains_key("in-operating"); + return if in_operating { cx.input.move_in_operating(step) } else { cx.input.move_(step) }; } + _ => {} } match cx.input.mode() { @@ -204,13 +202,14 @@ impl Executor { "backward" => cx.input.backward(), "forward" => cx.input.forward(exec.named.contains_key("end-of-word")), "delete" => cx.input.delete(exec.named.contains_key("insert")), + + "yank" => cx.input.yank(), + "paste" => cx.input.paste(exec.named.contains_key("before")), _ => false, }, InputMode::Insert => match exec.cmd.as_str() { - _ if let Some(c) = key.plain() => cx.input.type_(c), - "backspace" => cx.input.backspace(), - _ => false + _ => false, }, } } diff --git a/src/ui/input.rs b/src/ui/input.rs index 275604dd..0407398c 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -32,8 +32,8 @@ impl<'a> Widget for Input<'a> { .style(Style::default().fg(Color::White)) .render(area, buf); - if let Some(range) = input.range() { - buf.set_style(range, Style::default().bg(Color::Rgb(72, 77, 102))) + if let Some(selected) = input.selected() { + buf.set_style(selected, Style::default().bg(Color::Rgb(72, 77, 102))) } let _ = match input.mode() {