feat: add new yank and paste command to input

This commit is contained in:
sxyazi 2023-07-21 07:54:02 +08:00
parent f06775555b
commit a017a66a17
No known key found for this signature in database
7 changed files with 199 additions and 126 deletions

View file

@ -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" ] }
]

View file

@ -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.

31
src/core/external/clipboard.rs vendored Normal file
View file

@ -0,0 +1,31 @@
use std::process::Stdio;
use anyhow::{bail, Result};
use tokio::{io::AsyncWriteExt, process::Command};
pub async fn clipboard_get() -> Result<String> {
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")
}

View file

@ -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::*;

View file

@ -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<usize>,
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<Rect> {
if let Some((start, end)) = self.range {
let end = self
.value
.chars()
.skip(start)
.enumerate()
.take_while(|(i, _)| *i < end)
.map(|(_, c)| c)
.collect::<String>()
.width() as u16;
let start = self
.value
.chars()
.enumerate()
.take_while(|(i, _)| *i < start)
.map(|(_, c)| c)
.collect::<String>()
.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<Rect> {
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))
}
}

View file

@ -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,
},
}
}

View file

@ -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() {