mirror of
https://github.com/sxyazi/yazi.git
synced 2024-12-18 14:21:32 +03:00
feat: auto-completion for input component (#324)
This commit is contained in:
parent
73941c2291
commit
5b66f6fcf5
@ -1 +1 @@
|
|||||||
{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi"],"language":"en","version":"0.2","flagWords":[]}
|
{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit"],"flagWords":[],"version":"0.2","language":"en"}
|
||||||
|
@ -237,3 +237,18 @@ keymap = [
|
|||||||
# Filtering
|
# Filtering
|
||||||
{ on = [ "/" ], exec = "filter", desc = "Apply a filter for the help items" },
|
{ on = [ "/" ], exec = "filter", desc = "Apply a filter for the help items" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[completion]
|
||||||
|
|
||||||
|
keymap = [
|
||||||
|
{ on = [ "<C-q>" ], exec = "close", desc = "Cancel completion" },
|
||||||
|
{ on = [ "<Enter>" ], exec = "close --submit", desc = "Submit the completion" },
|
||||||
|
|
||||||
|
{ on = [ "<A-k>" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||||
|
{ on = [ "<A-j>" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||||
|
|
||||||
|
{ on = [ "<Up>" ], exec = "arrow -1", desc = "Move cursor up" },
|
||||||
|
{ on = [ "<Down>" ], exec = "arrow 1", desc = "Move cursor down" },
|
||||||
|
|
||||||
|
{ on = [ "~" ], exec = "help", desc = "Open help" }
|
||||||
|
]
|
||||||
|
@ -85,6 +85,21 @@ inactive = {}
|
|||||||
# : }}}
|
# : }}}
|
||||||
|
|
||||||
|
|
||||||
|
# : Completion {{{
|
||||||
|
|
||||||
|
[completion]
|
||||||
|
border = { fg = "blue" }
|
||||||
|
active = { bg = "darkgray" }
|
||||||
|
inactive = {}
|
||||||
|
|
||||||
|
# Icons
|
||||||
|
icon_file = ""
|
||||||
|
icon_folder = ""
|
||||||
|
icon_command = ""
|
||||||
|
|
||||||
|
# : }}}
|
||||||
|
|
||||||
|
|
||||||
# : Tasks {{{
|
# : Tasks {{{
|
||||||
|
|
||||||
[tasks]
|
[tasks]
|
||||||
|
@ -102,9 +102,15 @@ impl Exec {
|
|||||||
pub fn vec(self) -> Vec<Self> { vec![self] }
|
pub fn vec(self) -> Vec<Self> { vec![self] }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn with_bool(mut self, name: &str, state: bool) -> Self {
|
pub fn with(mut self, name: impl ToString, value: impl ToString) -> Self {
|
||||||
|
self.named.insert(name.to_string(), value.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn with_bool(mut self, name: impl ToString, state: bool) -> Self {
|
||||||
if state {
|
if state {
|
||||||
self.named.insert(name.to_string(), "".to_string());
|
self.named.insert(name.to_string(), Default::default());
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use anyhow::bail;
|
|||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||||
#[serde(try_from = "String")]
|
#[serde(try_from = "String")]
|
||||||
pub struct Key {
|
pub struct Key {
|
||||||
pub code: KeyCode,
|
pub code: KeyCode,
|
||||||
|
@ -7,11 +7,12 @@ use crate::MERGED_KEYMAP;
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Keymap {
|
pub struct Keymap {
|
||||||
pub manager: Vec<Control>,
|
pub manager: Vec<Control>,
|
||||||
pub tasks: Vec<Control>,
|
pub tasks: Vec<Control>,
|
||||||
pub select: Vec<Control>,
|
pub select: Vec<Control>,
|
||||||
pub input: Vec<Control>,
|
pub input: Vec<Control>,
|
||||||
pub help: Vec<Control>,
|
pub help: Vec<Control>,
|
||||||
|
pub completion: Vec<Control>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Keymap {
|
impl<'de> Deserialize<'de> for Keymap {
|
||||||
@ -21,11 +22,12 @@ impl<'de> Deserialize<'de> for Keymap {
|
|||||||
{
|
{
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Shadow {
|
struct Shadow {
|
||||||
manager: Inner,
|
manager: Inner,
|
||||||
tasks: Inner,
|
tasks: Inner,
|
||||||
select: Inner,
|
select: Inner,
|
||||||
input: Inner,
|
input: Inner,
|
||||||
help: Inner,
|
help: Inner,
|
||||||
|
completion: Inner,
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Inner {
|
struct Inner {
|
||||||
@ -34,11 +36,12 @@ impl<'de> Deserialize<'de> for Keymap {
|
|||||||
|
|
||||||
let shadow = Shadow::deserialize(deserializer)?;
|
let shadow = Shadow::deserialize(deserializer)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
manager: shadow.manager.keymap,
|
manager: shadow.manager.keymap,
|
||||||
tasks: shadow.tasks.keymap,
|
tasks: shadow.tasks.keymap,
|
||||||
select: shadow.select.keymap,
|
select: shadow.select.keymap,
|
||||||
input: shadow.input.keymap,
|
input: shadow.input.keymap,
|
||||||
help: shadow.help.keymap,
|
help: shadow.help.keymap,
|
||||||
|
completion: shadow.completion.keymap,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,6 +59,7 @@ impl Keymap {
|
|||||||
KeymapLayer::Select => &self.select,
|
KeymapLayer::Select => &self.select,
|
||||||
KeymapLayer::Input => &self.input,
|
KeymapLayer::Input => &self.input,
|
||||||
KeymapLayer::Help => &self.help,
|
KeymapLayer::Help => &self.help,
|
||||||
|
KeymapLayer::Completion => &self.completion,
|
||||||
KeymapLayer::Which => unreachable!(),
|
KeymapLayer::Which => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,6 +73,7 @@ pub enum KeymapLayer {
|
|||||||
Select,
|
Select,
|
||||||
Input,
|
Input,
|
||||||
Help,
|
Help,
|
||||||
|
Completion,
|
||||||
Which,
|
Which,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +85,7 @@ impl Display for KeymapLayer {
|
|||||||
KeymapLayer::Select => write!(f, "select"),
|
KeymapLayer::Select => write!(f, "select"),
|
||||||
KeymapLayer::Input => write!(f, "input"),
|
KeymapLayer::Input => write!(f, "input"),
|
||||||
KeymapLayer::Help => write!(f, "help"),
|
KeymapLayer::Help => write!(f, "help"),
|
||||||
|
KeymapLayer::Completion => write!(f, "completion"),
|
||||||
KeymapLayer::Which => write!(f, "which"),
|
KeymapLayer::Which => write!(f, "which"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,17 @@ pub struct Select {
|
|||||||
pub inactive: Style,
|
pub inactive: Style,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct Completion {
|
||||||
|
pub border: Style,
|
||||||
|
pub active: Style,
|
||||||
|
pub inactive: Style,
|
||||||
|
|
||||||
|
pub icon_file: String,
|
||||||
|
pub icon_folder: String,
|
||||||
|
pub icon_command: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Tasks {
|
pub struct Tasks {
|
||||||
pub border: Style,
|
pub border: Style,
|
||||||
@ -111,13 +122,14 @@ pub struct Help {
|
|||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub manager: Manager,
|
pub manager: Manager,
|
||||||
status: Status,
|
status: Status,
|
||||||
pub input: Input,
|
pub input: Input,
|
||||||
pub select: Select,
|
pub select: Select,
|
||||||
pub tasks: Tasks,
|
pub completion: Completion,
|
||||||
pub which: Which,
|
pub tasks: Tasks,
|
||||||
pub help: Help,
|
pub which: Which,
|
||||||
|
pub help: Help,
|
||||||
|
|
||||||
// File-specific styles
|
// File-specific styles
|
||||||
#[serde(rename = "filetype", deserialize_with = "Filetype::deserialize", skip_serializing)]
|
#[serde(rename = "filetype", deserialize_with = "Filetype::deserialize", skip_serializing)]
|
||||||
|
44
yazi-core/src/completion/commands/arrow.rs
Normal file
44
yazi-core/src/completion/commands/arrow.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use yazi_config::keymap::Exec;
|
||||||
|
|
||||||
|
use crate::completion::Completion;
|
||||||
|
|
||||||
|
pub struct Opt(isize);
|
||||||
|
|
||||||
|
impl From<&Exec> for Opt {
|
||||||
|
fn from(e: &Exec) -> Self { Self(e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completion {
|
||||||
|
fn next(&mut self, step: usize) -> bool {
|
||||||
|
let len = self.cands.len();
|
||||||
|
if len == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let old = self.cursor;
|
||||||
|
self.cursor = (self.cursor + step).min(len - 1);
|
||||||
|
|
||||||
|
let limit = self.limit();
|
||||||
|
if self.cursor >= len.min(self.offset + limit) {
|
||||||
|
self.offset = len.saturating_sub(limit).min(self.offset + self.cursor - old);
|
||||||
|
}
|
||||||
|
|
||||||
|
old != self.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev(&mut self, step: usize) -> bool {
|
||||||
|
let old = self.cursor;
|
||||||
|
self.cursor = self.cursor.saturating_sub(step);
|
||||||
|
|
||||||
|
if self.cursor < self.offset {
|
||||||
|
self.offset = self.offset.saturating_sub(old - self.cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
old != self.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arrow(&mut self, opt: impl Into<Opt>) -> bool {
|
||||||
|
let step = opt.into().0;
|
||||||
|
if step > 0 { self.next(step as usize) } else { self.prev(step.unsigned_abs()) }
|
||||||
|
}
|
||||||
|
}
|
25
yazi-core/src/completion/commands/close.rs
Normal file
25
yazi-core/src/completion/commands/close.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use yazi_config::keymap::{Exec, KeymapLayer};
|
||||||
|
|
||||||
|
use crate::{completion::Completion, emit};
|
||||||
|
|
||||||
|
pub struct Opt(bool);
|
||||||
|
|
||||||
|
impl From<&Exec> for Opt {
|
||||||
|
fn from(e: &Exec) -> Self { Self(e.named.contains_key("submit")) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completion {
|
||||||
|
pub fn close(&mut self, opt: impl Into<Opt>) -> bool {
|
||||||
|
let submit = opt.into().0;
|
||||||
|
if submit {
|
||||||
|
emit!(Call(
|
||||||
|
Exec::call("complete", vec![self.selected().into()]).with("ticket", self.ticket).vec(),
|
||||||
|
KeymapLayer::Input
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.caches.clear();
|
||||||
|
self.visible = false;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
4
yazi-core/src/completion/commands/mod.rs
Normal file
4
yazi-core/src/completion/commands/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
mod arrow;
|
||||||
|
mod close;
|
||||||
|
mod show;
|
||||||
|
mod trigger;
|
62
yazi-core/src/completion/commands/show.rs
Normal file
62
yazi-core/src/completion/commands/show.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use std::ops::ControlFlow;
|
||||||
|
|
||||||
|
use yazi_config::keymap::Exec;
|
||||||
|
|
||||||
|
use crate::completion::Completion;
|
||||||
|
|
||||||
|
pub struct Opt<'a> {
|
||||||
|
cache: &'a Vec<String>,
|
||||||
|
cache_name: &'a str,
|
||||||
|
word: &'a str,
|
||||||
|
ticket: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Exec> for Opt<'a> {
|
||||||
|
fn from(e: &'a Exec) -> Self {
|
||||||
|
Self {
|
||||||
|
cache: &e.args,
|
||||||
|
cache_name: e.named.get("cache-name").map(|n| n.as_str()).unwrap_or_default(),
|
||||||
|
word: e.named.get("word").map(|w| w.as_str()).unwrap_or_default(),
|
||||||
|
ticket: e.named.get("ticket").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completion {
|
||||||
|
pub fn show<'a>(&mut self, opt: impl Into<Opt<'a>>) -> bool {
|
||||||
|
let opt = opt.into();
|
||||||
|
if self.ticket != opt.ticket {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opt.cache.is_empty() {
|
||||||
|
self.caches.insert(opt.cache_name.to_owned(), opt.cache.clone());
|
||||||
|
}
|
||||||
|
let Some(cache) = self.caches.get(opt.cache_name) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let flow = cache.iter().try_fold(Vec::with_capacity(30), |mut v, s| {
|
||||||
|
if s.contains(opt.word) && s != opt.word {
|
||||||
|
v.push(s.to_owned());
|
||||||
|
if v.len() >= 30 {
|
||||||
|
return ControlFlow::Break(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(v)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.cands = match flow {
|
||||||
|
ControlFlow::Continue(v) => v,
|
||||||
|
ControlFlow::Break(v) => v,
|
||||||
|
};
|
||||||
|
self.ticket = opt.ticket;
|
||||||
|
|
||||||
|
if !self.cands.is_empty() {
|
||||||
|
self.offset = 0;
|
||||||
|
self.cursor = 0;
|
||||||
|
self.visible = true;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
110
yazi-core/src/completion/commands/trigger.rs
Normal file
110
yazi-core/src/completion/commands/trigger.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use tokio::fs;
|
||||||
|
use yazi_config::keymap::{Exec, KeymapLayer};
|
||||||
|
|
||||||
|
use crate::{completion::Completion, emit};
|
||||||
|
|
||||||
|
pub struct Opt<'a> {
|
||||||
|
before: &'a str,
|
||||||
|
ticket: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Exec> for Opt<'a> {
|
||||||
|
fn from(e: &'a Exec) -> Self {
|
||||||
|
Self {
|
||||||
|
before: e.named.get("before").map(|s| s.as_str()).unwrap_or_default(),
|
||||||
|
ticket: e.named.get("ticket").and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completion {
|
||||||
|
#[inline]
|
||||||
|
fn split_path(s: &str) -> (String, String) {
|
||||||
|
match s.rsplit_once(|c| c == '/' || c == '\\') {
|
||||||
|
Some((p, c)) => (format!("{p}/"), c.to_owned()),
|
||||||
|
None => (".".to_owned(), s.to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger<'a>(&mut self, opt: impl Into<Opt<'a>>) -> bool {
|
||||||
|
let opt = opt.into();
|
||||||
|
if opt.ticket < self.ticket {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ticket = opt.ticket;
|
||||||
|
let (parent, child) = Self::split_path(opt.before);
|
||||||
|
|
||||||
|
if self.caches.contains_key(&parent) {
|
||||||
|
return self.show(
|
||||||
|
&Exec::call("show", vec![])
|
||||||
|
.with("cache-name", parent)
|
||||||
|
.with("word", child)
|
||||||
|
.with("ticket", opt.ticket),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ticket = self.ticket;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut dir = fs::read_dir(&parent).await?;
|
||||||
|
let mut cache = Vec::new();
|
||||||
|
while let Ok(Some(f)) = dir.next_entry().await {
|
||||||
|
let Ok(meta) = f.metadata().await else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let sep = if !meta.is_dir() {
|
||||||
|
""
|
||||||
|
} else if cfg!(windows) {
|
||||||
|
"\\"
|
||||||
|
} else {
|
||||||
|
"/"
|
||||||
|
};
|
||||||
|
cache.push(format!("{}{sep}", f.file_name().to_string_lossy()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cache.is_empty() {
|
||||||
|
emit!(Call(
|
||||||
|
Exec::call("show", cache)
|
||||||
|
.with("cache-name", parent)
|
||||||
|
.with("word", child)
|
||||||
|
.with("ticket", ticket)
|
||||||
|
.vec(),
|
||||||
|
KeymapLayer::Completion
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
mem::replace(&mut self.visible, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_explode() {
|
||||||
|
assert_eq!(Completion::split_path(""), (".".to_owned(), "".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path(" "), (".".to_owned(), " ".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("/"), ("/".to_owned(), "".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("//"), ("//".to_owned(), "".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("/foo"), ("/".to_owned(), "foo".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("/foo/"), ("/foo/".to_owned(), "".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("/foo/bar"), ("/foo/".to_owned(), "bar".to_owned()));
|
||||||
|
|
||||||
|
// Windows
|
||||||
|
assert_eq!(Completion::split_path("foo"), (".".to_owned(), "foo".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("foo\\"), ("foo/".to_owned(), "".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("foo\\bar"), ("foo/".to_owned(), "bar".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("foo\\bar\\"), ("foo\\bar/".to_owned(), "".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("C:\\"), ("C:/".to_owned(), "".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("C:\\foo"), ("C:/".to_owned(), "foo".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("C:\\foo\\"), ("C:\\foo/".to_owned(), "".to_owned()));
|
||||||
|
assert_eq!(Completion::split_path("C:\\foo\\bar"), ("C:\\foo/".to_owned(), "bar".to_owned()));
|
||||||
|
}
|
||||||
|
}
|
31
yazi-core/src/completion/completion.rs
Normal file
31
yazi-core/src/completion/completion.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Completion {
|
||||||
|
pub(super) caches: BTreeMap<String, Vec<String>>,
|
||||||
|
pub(super) cands: Vec<String>,
|
||||||
|
pub(super) offset: usize,
|
||||||
|
pub cursor: usize,
|
||||||
|
|
||||||
|
pub(super) ticket: usize,
|
||||||
|
pub visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completion {
|
||||||
|
// --- Cands
|
||||||
|
#[inline]
|
||||||
|
pub fn window(&self) -> &[String] {
|
||||||
|
let end = (self.offset + self.limit()).min(self.cands.len());
|
||||||
|
&self.cands[self.offset..end]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn limit(&self) -> usize { self.cands.len().min(10) }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn selected(&self) -> &String { &self.cands[self.cursor] }
|
||||||
|
|
||||||
|
// --- Cursor
|
||||||
|
#[inline]
|
||||||
|
pub fn rel_cursor(&self) -> usize { self.cursor - self.offset }
|
||||||
|
}
|
4
yazi-core/src/completion/mod.rs
Normal file
4
yazi-core/src/completion/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
mod commands;
|
||||||
|
mod completion;
|
||||||
|
|
||||||
|
pub(super) use completion::*;
|
@ -1,28 +1,29 @@
|
|||||||
use crossterm::terminal::WindowSize;
|
use crossterm::terminal::WindowSize;
|
||||||
use ratatui::prelude::Rect;
|
use ratatui::prelude::Rect;
|
||||||
use yazi_config::keymap::KeymapLayer;
|
|
||||||
use yazi_shared::Term;
|
use yazi_shared::Term;
|
||||||
|
|
||||||
use crate::{help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position};
|
use crate::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position};
|
||||||
|
|
||||||
pub struct Ctx {
|
pub struct Ctx {
|
||||||
pub manager: Manager,
|
pub manager: Manager,
|
||||||
pub which: Which,
|
pub tasks: Tasks,
|
||||||
pub help: Help,
|
pub select: Select,
|
||||||
pub input: Input,
|
pub input: Input,
|
||||||
pub select: Select,
|
pub help: Help,
|
||||||
pub tasks: Tasks,
|
pub completion: Completion,
|
||||||
|
pub which: Which,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ctx {
|
impl Ctx {
|
||||||
pub fn make() -> Self {
|
pub fn make() -> Self {
|
||||||
Self {
|
Self {
|
||||||
manager: Manager::make(),
|
manager: Manager::make(),
|
||||||
which: Default::default(),
|
tasks: Tasks::start(),
|
||||||
help: Default::default(),
|
select: Default::default(),
|
||||||
input: Default::default(),
|
input: Default::default(),
|
||||||
select: Default::default(),
|
help: Default::default(),
|
||||||
tasks: Tasks::start(),
|
completion: Default::default(),
|
||||||
|
which: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,29 +31,31 @@ impl Ctx {
|
|||||||
let WindowSize { columns, rows, .. } = Term::size();
|
let WindowSize { columns, rows, .. } = Term::size();
|
||||||
|
|
||||||
let (x, y) = match pos {
|
let (x, y) = match pos {
|
||||||
Position::None => return Rect::default(),
|
|
||||||
Position::Top(Rect { mut x, mut y, width, height }) => {
|
Position::Top(Rect { mut x, mut y, width, height }) => {
|
||||||
x = x.min(columns.saturating_sub(*width));
|
x = x.min(columns.saturating_sub(*width));
|
||||||
y = y.min(rows.saturating_sub(*height));
|
y = y.min(rows.saturating_sub(*height));
|
||||||
((columns / 2).saturating_sub(width / 2) + x, y)
|
((columns / 2).saturating_sub(width / 2) + x, y)
|
||||||
}
|
}
|
||||||
Position::Hovered(rect @ Rect { mut x, y, width, height }) => {
|
Position::Sticky(Rect { mut x, y, width, height }, r) => {
|
||||||
let Some(r) =
|
|
||||||
self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url))
|
|
||||||
else {
|
|
||||||
return self.area(&Position::Top(*rect));
|
|
||||||
};
|
|
||||||
|
|
||||||
x = x.min(columns.saturating_sub(*width));
|
x = x.min(columns.saturating_sub(*width));
|
||||||
if y + height + r.y + r.height > rows {
|
if y + height + r.y + r.height > rows {
|
||||||
(x + r.x, r.y.saturating_sub(height.saturating_sub(1)))
|
(x + r.x, r.y.saturating_sub(height.saturating_sub(*y)))
|
||||||
} else {
|
} else {
|
||||||
(x + r.x, y + r.y + r.height)
|
(x + r.x, y + r.y + r.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Position::Hovered(rect) => {
|
||||||
|
return self.area(&if let Some(r) =
|
||||||
|
self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url))
|
||||||
|
{
|
||||||
|
Position::Sticky(*rect, r)
|
||||||
|
} else {
|
||||||
|
Position::Top(*rect)
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (w, h) = pos.dimension().unwrap();
|
let (w, h) = pos.dimension();
|
||||||
Rect { x, y, width: w.min(columns.saturating_sub(x)), height: h.min(rows.saturating_sub(y)) }
|
Rect { x, y, width: w.min(columns.saturating_sub(x)), height: h.min(rows.saturating_sub(y)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,25 +71,8 @@ impl Ctx {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn layer(&self) -> KeymapLayer {
|
|
||||||
if self.which.visible {
|
|
||||||
KeymapLayer::Which
|
|
||||||
} else if self.help.visible {
|
|
||||||
KeymapLayer::Help
|
|
||||||
} else if self.input.visible {
|
|
||||||
KeymapLayer::Input
|
|
||||||
} else if self.select.visible {
|
|
||||||
KeymapLayer::Select
|
|
||||||
} else if self.tasks.visible {
|
|
||||||
KeymapLayer::Tasks
|
|
||||||
} else {
|
|
||||||
KeymapLayer::Manager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn image_layer(&self) -> bool {
|
pub fn image_layer(&self) -> bool {
|
||||||
!matches!(self.layer(), KeymapLayer::Which | KeymapLayer::Help | KeymapLayer::Tasks)
|
!self.which.visible && !self.help.visible && !self.tasks.visible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
45
yazi-core/src/input/commands/complete.rs
Normal file
45
yazi-core/src/input/commands/complete.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use yazi_config::keymap::Exec;
|
||||||
|
|
||||||
|
use crate::input::Input;
|
||||||
|
|
||||||
|
pub struct Opt<'a> {
|
||||||
|
word: &'a str,
|
||||||
|
ticket: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Exec> for Opt<'a> {
|
||||||
|
fn from(e: &'a Exec) -> Self {
|
||||||
|
Self {
|
||||||
|
word: e.args.first().map(|w| w.as_str()).unwrap_or_default(),
|
||||||
|
ticket: e.named.get("ticket").and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Input {
|
||||||
|
pub fn complete<'a>(&mut self, opt: impl Into<Opt<'a>>) -> bool {
|
||||||
|
let opt = opt.into();
|
||||||
|
if self.ticket != opt.ticket {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [before, after] = self.partition();
|
||||||
|
let new = if let Some((prefix, _)) = before.rsplit_once('/') {
|
||||||
|
format!("{prefix}/{}{after}", opt.word)
|
||||||
|
} else {
|
||||||
|
format!("{}{after}", opt.word)
|
||||||
|
};
|
||||||
|
|
||||||
|
let snap = self.snaps.current_mut();
|
||||||
|
if new == snap.value {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let delta = new.chars().count() as isize - snap.value.chars().count() as isize;
|
||||||
|
snap.value = new;
|
||||||
|
|
||||||
|
self.move_(delta);
|
||||||
|
self.flush_value();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
1
yazi-core/src/input/commands/mod.rs
Normal file
1
yazi-core/src/input/commands/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
mod complete;
|
@ -3,23 +3,25 @@ use std::ops::Range;
|
|||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
use yazi_config::keymap::Key;
|
use yazi_config::keymap::{Exec, Key, KeymapLayer};
|
||||||
use yazi_shared::{CharKind, InputError};
|
use yazi_shared::{CharKind, InputError};
|
||||||
|
|
||||||
use super::{mode::InputMode, op::InputOp, InputOpt, InputSnap, InputSnaps};
|
use super::{mode::InputMode, op::InputOp, InputOpt, InputSnap, InputSnaps};
|
||||||
use crate::{external, Position};
|
use crate::{emit, external, Position};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Input {
|
pub struct Input {
|
||||||
snaps: InputSnaps,
|
pub(super) snaps: InputSnaps,
|
||||||
pub visible: bool,
|
pub ticket: usize,
|
||||||
|
pub visible: bool,
|
||||||
|
|
||||||
title: String,
|
pub title: String,
|
||||||
pub position: Position,
|
pub position: Position,
|
||||||
|
|
||||||
// Typing
|
// Typing
|
||||||
callback: Option<UnboundedSender<Result<String, InputError>>>,
|
callback: Option<UnboundedSender<Result<String, InputError>>>,
|
||||||
realtime: bool,
|
realtime: bool,
|
||||||
|
completion: bool,
|
||||||
|
|
||||||
// Shell
|
// Shell
|
||||||
pub(super) highlight: bool,
|
pub(super) highlight: bool,
|
||||||
@ -37,6 +39,7 @@ impl Input {
|
|||||||
// Typing
|
// Typing
|
||||||
self.callback = Some(tx);
|
self.callback = Some(tx);
|
||||||
self.realtime = opt.realtime;
|
self.realtime = opt.realtime;
|
||||||
|
self.completion = opt.completion;
|
||||||
|
|
||||||
// Shell
|
// Shell
|
||||||
self.highlight = opt.highlight;
|
self.highlight = opt.highlight;
|
||||||
@ -48,6 +51,7 @@ impl Input {
|
|||||||
_ = cb.send(if submit { Ok(value) } else { Err(InputError::Canceled(value)) });
|
_ = cb.send(if submit { Ok(value) } else { Err(InputError::Canceled(value)) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.ticket = self.ticket.wrapping_add(1);
|
||||||
self.visible = false;
|
self.visible = false;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -64,6 +68,10 @@ impl Input {
|
|||||||
InputMode::Insert => {
|
InputMode::Insert => {
|
||||||
snap.mode = InputMode::Normal;
|
snap.mode = InputMode::Normal;
|
||||||
self.move_(-1);
|
self.move_(-1);
|
||||||
|
|
||||||
|
if self.completion {
|
||||||
|
emit!(Call(Exec::call("close", vec![]).vec(), KeymapLayer::Completion));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.snaps.tag();
|
self.snaps.tag();
|
||||||
@ -190,7 +198,8 @@ impl Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(c) = key.plain() {
|
if let Some(c) = key.plain() {
|
||||||
return self.type_char(c);
|
let mut bits = [0; 4];
|
||||||
|
return self.type_str(c.encode_utf8(&mut bits));
|
||||||
}
|
}
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
@ -199,12 +208,6 @@ impl Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn type_char(&mut self, c: char) -> bool {
|
|
||||||
let mut bits = [0; 4];
|
|
||||||
self.type_str(c.encode_utf8(&mut bits))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn type_str(&mut self, s: &str) -> bool {
|
pub fn type_str(&mut self, s: &str) -> bool {
|
||||||
let snap = self.snaps.current_mut();
|
let snap = self.snaps.current_mut();
|
||||||
if snap.cursor < 1 {
|
if snap.cursor < 1 {
|
||||||
@ -213,8 +216,9 @@ impl Input {
|
|||||||
snap.value.insert_str(snap.idx(snap.cursor).unwrap(), s);
|
snap.value.insert_str(snap.idx(snap.cursor).unwrap(), s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.move_(s.chars().count() as isize);
|
||||||
self.flush_value();
|
self.flush_value();
|
||||||
self.move_(s.chars().count() as isize)
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn backspace(&mut self) -> bool {
|
pub fn backspace(&mut self) -> bool {
|
||||||
@ -225,8 +229,9 @@ impl Input {
|
|||||||
snap.value.remove(snap.idx(snap.cursor - 1).unwrap());
|
snap.value.remove(snap.idx(snap.cursor - 1).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.move_(-1);
|
||||||
self.flush_value();
|
self.flush_value();
|
||||||
self.move_(-1)
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&mut self, cut: bool, insert: bool) -> bool {
|
pub fn delete(&mut self, cut: bool, insert: bool) -> bool {
|
||||||
@ -278,9 +283,7 @@ impl Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.insert(!before);
|
self.insert(!before);
|
||||||
for c in s.to_string_lossy().chars() {
|
self.type_str(&s.to_string_lossy());
|
||||||
self.type_char(c);
|
|
||||||
}
|
|
||||||
self.escape();
|
self.escape();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -305,10 +308,6 @@ impl Input {
|
|||||||
snap.op = InputOp::None;
|
snap.op = InputOp::None;
|
||||||
snap.mode = if insert { InputMode::Insert } else { InputMode::Normal };
|
snap.mode = if insert { InputMode::Insert } else { InputMode::Normal };
|
||||||
snap.cursor = range.start;
|
snap.cursor = range.start;
|
||||||
|
|
||||||
if self.realtime {
|
|
||||||
self.callback.as_ref().unwrap().send(Err(InputError::Typed(snap.value.clone()))).ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
InputOp::Yank(_) => {
|
InputOp::Yank(_) => {
|
||||||
let range = snap.op.range(cursor, include).unwrap();
|
let range = snap.op.range(cursor, include).unwrap();
|
||||||
@ -325,24 +324,28 @@ impl Input {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if !matches!(old.op, InputOp::None | InputOp::Select(_)) {
|
if !matches!(old.op, InputOp::None | InputOp::Select(_)) {
|
||||||
self.snaps.tag();
|
self.snaps.tag().then(|| self.flush_value());
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn flush_value(&self) {
|
pub(super) fn flush_value(&mut self) {
|
||||||
|
self.ticket = self.ticket.wrapping_add(1);
|
||||||
|
|
||||||
if self.realtime {
|
if self.realtime {
|
||||||
let value = self.snap().value.clone();
|
let value = self.snap().value.clone();
|
||||||
self.callback.as_ref().unwrap().send(Err(InputError::Typed(value))).ok();
|
self.callback.as_ref().unwrap().send(Err(InputError::Typed(value))).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.completion {
|
||||||
|
let before = self.partition()[0].to_owned();
|
||||||
|
self.callback.as_ref().unwrap().send(Err(InputError::Completed(before, self.ticket))).ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
#[inline]
|
|
||||||
pub fn title(&self) -> String { self.title.clone() }
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn value(&self) -> &str { self.snap().slice(self.snap().window()) }
|
pub fn value(&self) -> &str { self.snap().slice(self.snap().window()) }
|
||||||
|
|
||||||
@ -369,6 +372,13 @@ impl Input {
|
|||||||
Some(s..s + snap.slice(start..end).width() as u16)
|
Some(s..s + snap.slice(start..end).width() as u16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn partition(&self) -> [&str; 2] {
|
||||||
|
let snap = self.snap();
|
||||||
|
let idx = snap.idx(snap.cursor).unwrap();
|
||||||
|
[&snap.value[..idx], &snap.value[idx..]]
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn snap(&self) -> &InputSnap { self.snaps.current() }
|
fn snap(&self) -> &InputSnap { self.snaps.current() }
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
mod commands;
|
||||||
mod input;
|
mod input;
|
||||||
mod mode;
|
mod mode;
|
||||||
mod op;
|
mod op;
|
||||||
@ -10,6 +11,5 @@ pub use input::*;
|
|||||||
pub use mode::*;
|
pub use mode::*;
|
||||||
use op::*;
|
use op::*;
|
||||||
pub use option::*;
|
pub use option::*;
|
||||||
pub use shell::*;
|
|
||||||
use snap::*;
|
use snap::*;
|
||||||
use snaps::*;
|
use snaps::*;
|
||||||
|
@ -2,35 +2,33 @@ use ratatui::prelude::Rect;
|
|||||||
|
|
||||||
use crate::Position;
|
use crate::Position;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct InputOpt {
|
pub struct InputOpt {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
pub position: Position,
|
pub position: Position,
|
||||||
pub realtime: bool,
|
pub realtime: bool,
|
||||||
pub highlight: bool,
|
pub completion: bool,
|
||||||
|
pub highlight: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputOpt {
|
impl InputOpt {
|
||||||
pub fn top(title: impl AsRef<str>) -> Self {
|
pub fn top(title: impl AsRef<str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: title.as_ref().to_owned(),
|
title: title.as_ref().to_owned(),
|
||||||
value: String::new(),
|
position: Position::Top(/* TODO: hardcode */ Rect { x: 0, y: 2, width: 50, height: 3 }),
|
||||||
position: Position::Top(/* TODO: hardcode */ Rect { x: 0, y: 2, width: 50, height: 3 }),
|
..Default::default()
|
||||||
realtime: false,
|
|
||||||
highlight: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hovered(title: impl AsRef<str>) -> Self {
|
pub fn hovered(title: impl AsRef<str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: title.as_ref().to_owned(),
|
title: title.as_ref().to_owned(),
|
||||||
value: String::new(),
|
position: Position::Hovered(
|
||||||
position: Position::Hovered(
|
|
||||||
// TODO: hardcode
|
// TODO: hardcode
|
||||||
Rect { x: 0, y: 1, width: 50, height: 3 },
|
Rect { x: 0, y: 1, width: 50, height: 3 },
|
||||||
),
|
),
|
||||||
realtime: false,
|
..Default::default()
|
||||||
highlight: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +44,12 @@ impl InputOpt {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn with_completion(mut self) -> Self {
|
||||||
|
self.completion = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn with_highlight(mut self) -> Self {
|
pub fn with_highlight(mut self) -> Self {
|
||||||
self.highlight = true;
|
self.highlight = true;
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
)]
|
)]
|
||||||
|
|
||||||
mod blocker;
|
mod blocker;
|
||||||
|
pub mod completion;
|
||||||
mod context;
|
mod context;
|
||||||
mod event;
|
mod event;
|
||||||
pub mod external;
|
pub mod external;
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
use ratatui::prelude::Rect;
|
use ratatui::prelude::Rect;
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub enum Position {
|
pub enum Position {
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
Top(Rect),
|
Top(Rect),
|
||||||
|
Sticky(Rect, Rect),
|
||||||
Hovered(Rect),
|
Hovered(Rect),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Position {
|
||||||
|
fn default() -> Self { Self::Top(Rect::default()) }
|
||||||
|
}
|
||||||
|
|
||||||
impl Position {
|
impl Position {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn rect(&self) -> Option<Rect> {
|
pub fn rect(&self) -> Rect {
|
||||||
match self {
|
match self {
|
||||||
Position::None => None,
|
Position::Top(rect) => *rect,
|
||||||
Position::Top(rect) => Some(*rect),
|
Position::Sticky(rect, _) => *rect,
|
||||||
Position::Hovered(rect) => Some(*rect),
|
Position::Hovered(rect) => *rect,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn dimension(&self) -> Option<(u16, u16)> { self.rect().map(|r| (r.width, r.height)) }
|
pub fn dimension(&self) -> (u16, u16) { (self.rect().width, self.rect().height) }
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use std::mem;
|
use std::{mem, time::Duration};
|
||||||
|
|
||||||
use yazi_shared::Url;
|
use tokio::pin;
|
||||||
|
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||||
|
use yazi_config::keymap::{Exec, KeymapLayer};
|
||||||
|
use yazi_shared::{Debounce, InputError, Url};
|
||||||
|
|
||||||
use crate::{emit, files::{File, FilesOp}, input::InputOpt, tab::Tab};
|
use crate::{emit, files::{File, FilesOp}, input::InputOpt, tab::Tab};
|
||||||
|
|
||||||
@ -59,11 +62,26 @@ impl Tab {
|
|||||||
|
|
||||||
pub fn cd_interactive(&mut self, target: Url) -> bool {
|
pub fn cd_interactive(&mut self, target: Url) -> bool {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut result =
|
let rx = emit!(Input(
|
||||||
emit!(Input(InputOpt::top("Change directory:").with_value(target.to_string_lossy())));
|
InputOpt::top("Change directory:").with_value(target.to_string_lossy()).with_completion()
|
||||||
|
));
|
||||||
|
|
||||||
if let Some(Ok(s)) = result.recv().await {
|
let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50));
|
||||||
emit!(Cd(Url::from(s.trim())));
|
pin!(rx);
|
||||||
|
|
||||||
|
while let Some(result) = rx.next().await {
|
||||||
|
match result {
|
||||||
|
Ok(s) => {
|
||||||
|
emit!(Cd(Url::from(s.trim())));
|
||||||
|
}
|
||||||
|
Err(InputError::Completed(before, ticket)) => {
|
||||||
|
emit!(Call(
|
||||||
|
Exec::call("complete", vec![]).with("before", before).with("ticket", ticket).vec(),
|
||||||
|
KeymapLayer::Input
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
false
|
false
|
||||||
|
@ -59,13 +59,13 @@ impl App {
|
|||||||
|
|
||||||
fn dispatch_key(&mut self, key: KeyEvent) {
|
fn dispatch_key(&mut self, key: KeyEvent) {
|
||||||
let key = Key::from(key);
|
let key = Key::from(key);
|
||||||
if Executor::handle(&mut self.cx, key) {
|
if Executor::new(&mut self.cx).handle(key) {
|
||||||
emit!(Render);
|
emit!(Render);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_paste(&mut self, str: String) {
|
fn dispatch_paste(&mut self, str: String) {
|
||||||
if self.cx.layer() == KeymapLayer::Input {
|
if self.cx.input.visible {
|
||||||
let input = &mut self.cx.input;
|
let input = &mut self.cx.input;
|
||||||
if input.mode() == InputMode::Insert && input.type_str(&str) {
|
if input.mode() == InputMode::Insert && input.type_str(&str) {
|
||||||
emit!(Render);
|
emit!(Render);
|
||||||
@ -112,7 +112,7 @@ impl App {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn dispatch_call(&mut self, exec: Vec<Exec>, layer: KeymapLayer) {
|
fn dispatch_call(&mut self, exec: Vec<Exec>, layer: KeymapLayer) {
|
||||||
if Executor::dispatch(&mut self.cx, &exec, layer) {
|
if Executor::new(&mut self.cx).dispatch(&exec, layer) {
|
||||||
emit!(Render);
|
emit!(Render);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
62
yazi-fm/src/completion/completion.rs
Normal file
62
yazi-fm/src/completion/completion.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}};
|
||||||
|
use yazi_config::THEME;
|
||||||
|
use yazi_core::{Ctx, Position};
|
||||||
|
|
||||||
|
pub(crate) struct Completion<'a> {
|
||||||
|
cx: &'a Ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Completion<'a> {
|
||||||
|
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for Completion<'a> {
|
||||||
|
fn render(self, rect: Rect, buf: &mut Buffer) {
|
||||||
|
let items = self
|
||||||
|
.cx
|
||||||
|
.completion
|
||||||
|
.window()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, x)| {
|
||||||
|
let icon = if x.ends_with('/') || x.ends_with('\\') {
|
||||||
|
&THEME.completion.icon_folder
|
||||||
|
} else {
|
||||||
|
&THEME.completion.icon_file
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut item = ListItem::new(format!(" {} {}", icon, x));
|
||||||
|
if i == self.cx.completion.rel_cursor() {
|
||||||
|
item = item.style(THEME.completion.active.into());
|
||||||
|
} else {
|
||||||
|
item = item.style(THEME.completion.inactive.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
item
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let input_area = self.cx.area(&self.cx.input.position);
|
||||||
|
let mut area = self.cx.area(&Position::Sticky(
|
||||||
|
Rect { x: 1, y: 0, width: 20, height: items.len() as u16 + 2 },
|
||||||
|
input_area,
|
||||||
|
));
|
||||||
|
|
||||||
|
if area.y > input_area.y {
|
||||||
|
area.y = area.y.saturating_sub(1);
|
||||||
|
} else {
|
||||||
|
area.y = rect.height.min(area.y + 1);
|
||||||
|
area.height = rect.height.saturating_sub(area.y).min(area.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
Clear.render(area, buf);
|
||||||
|
List::new(items)
|
||||||
|
.block(
|
||||||
|
Block::new()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(THEME.completion.border.into()),
|
||||||
|
)
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
3
yazi-fm/src/completion/mod.rs
Normal file
3
yazi-fm/src/completion/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod completion;
|
||||||
|
|
||||||
|
pub(super) use completion::*;
|
@ -2,78 +2,99 @@ use yazi_config::{keymap::{Control, Exec, Key, KeymapLayer}, KEYMAP};
|
|||||||
use yazi_core::{emit, input::InputMode, tab::FinderCase, Ctx};
|
use yazi_core::{emit, input::InputMode, tab::FinderCase, Ctx};
|
||||||
use yazi_shared::{optional_bool, Url};
|
use yazi_shared::{optional_bool, Url};
|
||||||
|
|
||||||
pub(super) struct Executor;
|
pub(super) struct Executor<'a> {
|
||||||
|
cx: &'a mut Ctx,
|
||||||
|
}
|
||||||
|
|
||||||
impl Executor {
|
impl<'a> Executor<'a> {
|
||||||
pub(super) fn handle(cx: &mut Ctx, key: Key) -> bool {
|
#[inline]
|
||||||
let layer = cx.layer();
|
pub(super) fn new(cx: &'a mut Ctx) -> Self { Self { cx } }
|
||||||
if layer == KeymapLayer::Which {
|
|
||||||
return cx.which.press(key);
|
pub(super) fn handle(&mut self, key: Key) -> bool {
|
||||||
|
if self.cx.which.visible {
|
||||||
|
return self.cx.which.press(key);
|
||||||
}
|
}
|
||||||
|
if self.cx.input.visible && self.cx.input.type_(&key) {
|
||||||
if layer == KeymapLayer::Input && cx.input.type_(&key) {
|
return true;
|
||||||
|
}
|
||||||
|
if self.cx.help.visible && self.cx.help.type_(&key) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if layer == KeymapLayer::Help && cx.help.type_(&key) {
|
let b = if self.cx.completion.visible {
|
||||||
return true;
|
self.matches(KeymapLayer::Completion, key).or_else(|| self.matches(KeymapLayer::Input, key))
|
||||||
}
|
} else if self.cx.help.visible {
|
||||||
|
self.matches(KeymapLayer::Help, key)
|
||||||
|
} else if self.cx.input.visible {
|
||||||
|
self.matches(KeymapLayer::Input, key)
|
||||||
|
} else if self.cx.select.visible {
|
||||||
|
self.matches(KeymapLayer::Select, key)
|
||||||
|
} else if self.cx.tasks.visible {
|
||||||
|
self.matches(KeymapLayer::Tasks, key)
|
||||||
|
} else {
|
||||||
|
self.matches(KeymapLayer::Manager, key)
|
||||||
|
};
|
||||||
|
b == Some(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn matches(&mut self, layer: KeymapLayer, key: Key) -> Option<bool> {
|
||||||
for Control { on, exec, .. } in KEYMAP.get(layer) {
|
for Control { on, exec, .. } in KEYMAP.get(layer) {
|
||||||
if on.is_empty() || on[0] != key {
|
if on.is_empty() || on[0] != key {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return if on.len() > 1 {
|
return Some(if on.len() > 1 {
|
||||||
cx.which.show(&key, layer)
|
self.cx.which.show(&key, layer)
|
||||||
} else {
|
} else {
|
||||||
Self::dispatch(cx, exec, layer)
|
self.dispatch(exec, layer)
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
false
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn dispatch(cx: &mut Ctx, exec: &[Exec], layer: KeymapLayer) -> bool {
|
pub(super) fn dispatch(&mut self, exec: &[Exec], layer: KeymapLayer) -> bool {
|
||||||
let mut render = false;
|
let mut render = false;
|
||||||
for e in exec {
|
for e in exec {
|
||||||
render |= match layer {
|
render |= match layer {
|
||||||
KeymapLayer::Manager => Self::manager(cx, e),
|
KeymapLayer::Manager => self.manager(e),
|
||||||
KeymapLayer::Tasks => Self::tasks(cx, e),
|
KeymapLayer::Tasks => self.tasks(e),
|
||||||
KeymapLayer::Select => Self::select(cx, e),
|
KeymapLayer::Select => self.select(e),
|
||||||
KeymapLayer::Input => Self::input(cx, e),
|
KeymapLayer::Input => self.input(e),
|
||||||
KeymapLayer::Help => Self::help(cx, e),
|
KeymapLayer::Help => self.help(e),
|
||||||
|
KeymapLayer::Completion => self.completion(e),
|
||||||
KeymapLayer::Which => unreachable!(),
|
KeymapLayer::Which => unreachable!(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
render
|
render
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manager(cx: &mut Ctx, exec: &Exec) -> bool {
|
fn manager(&mut self, exec: &Exec) -> bool {
|
||||||
match exec.cmd.as_str() {
|
match exec.cmd.as_str() {
|
||||||
"escape" => cx.manager.active_mut().escape(exec),
|
"escape" => self.cx.manager.active_mut().escape(exec),
|
||||||
"quit" => cx.manager.quit(&cx.tasks, exec.named.contains_key("no-cwd-file")),
|
"quit" => self.cx.manager.quit(&self.cx.tasks, exec.named.contains_key("no-cwd-file")),
|
||||||
"close" => cx.manager.close(&cx.tasks),
|
"close" => self.cx.manager.close(&self.cx.tasks),
|
||||||
"suspend" => cx.manager.suspend(),
|
"suspend" => self.cx.manager.suspend(),
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
"arrow" => {
|
"arrow" => {
|
||||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or_default();
|
let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or_default();
|
||||||
cx.manager.active_mut().arrow(step)
|
self.cx.manager.active_mut().arrow(step)
|
||||||
}
|
}
|
||||||
"peek" => {
|
"peek" => {
|
||||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
cx.manager.active_mut().preview.arrow(step);
|
self.cx.manager.active_mut().preview.arrow(step);
|
||||||
cx.manager.peek(true, cx.image_layer())
|
self.cx.manager.peek(true, self.cx.image_layer())
|
||||||
}
|
}
|
||||||
"leave" => cx.manager.active_mut().leave(),
|
"leave" => self.cx.manager.active_mut().leave(),
|
||||||
"enter" => cx.manager.active_mut().enter(),
|
"enter" => self.cx.manager.active_mut().enter(),
|
||||||
"back" => cx.manager.active_mut().back(),
|
"back" => self.cx.manager.active_mut().back(),
|
||||||
"forward" => cx.manager.active_mut().forward(),
|
"forward" => self.cx.manager.active_mut().forward(),
|
||||||
"cd" => {
|
"cd" => {
|
||||||
let url = exec.args.get(0).map(Url::from).unwrap_or_default();
|
let url = exec.args.first().map(Url::from).unwrap_or_default();
|
||||||
if exec.named.contains_key("interactive") {
|
if exec.named.contains_key("interactive") {
|
||||||
cx.manager.active_mut().cd_interactive(url)
|
self.cx.manager.active_mut().cd_interactive(url)
|
||||||
} else {
|
} else {
|
||||||
emit!(Cd(url));
|
emit!(Cd(url));
|
||||||
false
|
false
|
||||||
@ -83,64 +104,68 @@ impl Executor {
|
|||||||
// Selection
|
// Selection
|
||||||
"select" => {
|
"select" => {
|
||||||
let state = exec.named.get("state").cloned().unwrap_or("none".to_string());
|
let state = exec.named.get("state").cloned().unwrap_or("none".to_string());
|
||||||
cx.manager.active_mut().select(optional_bool(&state))
|
self.cx.manager.active_mut().select(optional_bool(&state))
|
||||||
}
|
}
|
||||||
"select_all" => {
|
"select_all" => {
|
||||||
let state = exec.named.get("state").cloned().unwrap_or("none".to_string());
|
let state = exec.named.get("state").cloned().unwrap_or("none".to_string());
|
||||||
cx.manager.active_mut().select_all(optional_bool(&state))
|
self.cx.manager.active_mut().select_all(optional_bool(&state))
|
||||||
}
|
}
|
||||||
"visual_mode" => cx.manager.active_mut().visual_mode(exec.named.contains_key("unset")),
|
"visual_mode" => self.cx.manager.active_mut().visual_mode(exec.named.contains_key("unset")),
|
||||||
|
|
||||||
// Operation
|
// Operation
|
||||||
"open" => cx.manager.open(exec.named.contains_key("interactive")),
|
"open" => self.cx.manager.open(exec.named.contains_key("interactive")),
|
||||||
"yank" => cx.manager.yank(exec.named.contains_key("cut")),
|
"yank" => self.cx.manager.yank(exec.named.contains_key("cut")),
|
||||||
"paste" => {
|
"paste" => {
|
||||||
let dest = cx.manager.cwd();
|
let dest = self.cx.manager.cwd();
|
||||||
let (cut, ref src) = cx.manager.yanked;
|
let (cut, ref src) = self.cx.manager.yanked;
|
||||||
|
|
||||||
let force = exec.named.contains_key("force");
|
let force = exec.named.contains_key("force");
|
||||||
if cut { cx.tasks.file_cut(src, dest, force) } else { cx.tasks.file_copy(src, dest, force) }
|
if cut {
|
||||||
|
self.cx.tasks.file_cut(src, dest, force)
|
||||||
|
} else {
|
||||||
|
self.cx.tasks.file_copy(src, dest, force)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"link" => {
|
"link" => {
|
||||||
let (cut, ref src) = cx.manager.yanked;
|
let (cut, ref src) = self.cx.manager.yanked;
|
||||||
!cut
|
!cut
|
||||||
&& cx.tasks.file_link(
|
&& self.cx.tasks.file_link(
|
||||||
src,
|
src,
|
||||||
cx.manager.cwd(),
|
self.cx.manager.cwd(),
|
||||||
exec.named.contains_key("relative"),
|
exec.named.contains_key("relative"),
|
||||||
exec.named.contains_key("force"),
|
exec.named.contains_key("force"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"remove" => {
|
"remove" => {
|
||||||
let targets = cx.manager.selected().into_iter().map(|f| f.url()).collect();
|
let targets = self.cx.manager.selected().into_iter().map(|f| f.url()).collect();
|
||||||
let force = exec.named.contains_key("force");
|
let force = exec.named.contains_key("force");
|
||||||
let permanently = exec.named.contains_key("permanently");
|
let permanently = exec.named.contains_key("permanently");
|
||||||
cx.tasks.file_remove(targets, force, permanently)
|
self.cx.tasks.file_remove(targets, force, permanently)
|
||||||
}
|
}
|
||||||
"create" => cx.manager.create(exec.named.contains_key("force")),
|
"create" => self.cx.manager.create(exec.named.contains_key("force")),
|
||||||
"rename" => cx.manager.rename(exec.named.contains_key("force")),
|
"rename" => self.cx.manager.rename(exec.named.contains_key("force")),
|
||||||
"copy" => cx.manager.active().copy(exec.args.get(0).map(|s| s.as_str()).unwrap_or("")),
|
"copy" => self.cx.manager.active().copy(exec.args.first().map(|s| s.as_str()).unwrap_or("")),
|
||||||
"shell" => cx.manager.active().shell(
|
"shell" => self.cx.manager.active().shell(
|
||||||
exec.args.get(0).map(|e| e.as_str()).unwrap_or(""),
|
exec.args.first().map(|e| e.as_str()).unwrap_or(""),
|
||||||
exec.named.contains_key("block"),
|
exec.named.contains_key("block"),
|
||||||
exec.named.contains_key("confirm"),
|
exec.named.contains_key("confirm"),
|
||||||
),
|
),
|
||||||
"hidden" => cx.manager.active_mut().hidden(exec),
|
"hidden" => self.cx.manager.active_mut().hidden(exec),
|
||||||
"linemode" => cx.manager.active_mut().linemode(exec),
|
"linemode" => self.cx.manager.active_mut().linemode(exec),
|
||||||
"search" => match exec.args.get(0).map(|s| s.as_str()).unwrap_or("") {
|
"search" => match exec.args.first().map(|s| s.as_str()).unwrap_or("") {
|
||||||
"rg" => cx.manager.active_mut().search(true),
|
"rg" => self.cx.manager.active_mut().search(true),
|
||||||
"fd" => cx.manager.active_mut().search(false),
|
"fd" => self.cx.manager.active_mut().search(false),
|
||||||
_ => cx.manager.active_mut().search_stop(),
|
_ => self.cx.manager.active_mut().search_stop(),
|
||||||
},
|
},
|
||||||
"jump" => match exec.args.get(0).map(|s| s.as_str()).unwrap_or("") {
|
"jump" => match exec.args.first().map(|s| s.as_str()).unwrap_or("") {
|
||||||
"fzf" => cx.manager.active_mut().jump(true),
|
"fzf" => self.cx.manager.active_mut().jump(true),
|
||||||
"zoxide" => cx.manager.active_mut().jump(false),
|
"zoxide" => self.cx.manager.active_mut().jump(false),
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Find
|
// Find
|
||||||
"find" => {
|
"find" => {
|
||||||
let query = exec.args.get(0).map(|s| s.as_str());
|
let query = exec.args.first().map(|s| s.as_str());
|
||||||
let prev = exec.named.contains_key("previous");
|
let prev = exec.named.contains_key("previous");
|
||||||
let case = match (exec.named.contains_key("smart"), exec.named.contains_key("insensitive"))
|
let case = match (exec.named.contains_key("smart"), exec.named.contains_key("insensitive"))
|
||||||
{
|
{
|
||||||
@ -148,131 +173,160 @@ impl Executor {
|
|||||||
(_, false) => FinderCase::Sensitive,
|
(_, false) => FinderCase::Sensitive,
|
||||||
(_, true) => FinderCase::Insensitive,
|
(_, true) => FinderCase::Insensitive,
|
||||||
};
|
};
|
||||||
cx.manager.active_mut().find(query, prev, case)
|
self.cx.manager.active_mut().find(query, prev, case)
|
||||||
}
|
}
|
||||||
"find_arrow" => cx.manager.active_mut().find_arrow(exec.named.contains_key("previous")),
|
"find_arrow" => self.cx.manager.active_mut().find_arrow(exec.named.contains_key("previous")),
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
"sort" => {
|
"sort" => {
|
||||||
let b = cx.manager.active_mut().sort(exec);
|
let b = self.cx.manager.active_mut().sort(exec);
|
||||||
cx.tasks.precache_size(&cx.manager.current().files);
|
self.cx.tasks.precache_size(&self.cx.manager.current().files);
|
||||||
b
|
b
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
"tab_create" => {
|
"tab_create" => {
|
||||||
let path = if exec.named.contains_key("current") {
|
let path = if exec.named.contains_key("current") {
|
||||||
cx.manager.cwd().to_owned()
|
self.cx.manager.cwd().to_owned()
|
||||||
} else {
|
} else {
|
||||||
exec.args.get(0).map(Url::from).unwrap_or_else(|| Url::from("/"))
|
exec.args.first().map(Url::from).unwrap_or_else(|| Url::from("/"))
|
||||||
};
|
};
|
||||||
cx.manager.tabs.create(&path)
|
self.cx.manager.tabs.create(&path)
|
||||||
}
|
}
|
||||||
"tab_close" => {
|
"tab_close" => {
|
||||||
let idx = exec.args.get(0).and_then(|i| i.parse().ok()).unwrap_or(0);
|
let idx = exec.args.first().and_then(|i| i.parse().ok()).unwrap_or(0);
|
||||||
cx.manager.tabs.close(idx)
|
self.cx.manager.tabs.close(idx)
|
||||||
}
|
}
|
||||||
"tab_switch" => {
|
"tab_switch" => {
|
||||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
let rel = exec.named.contains_key("relative");
|
let rel = exec.named.contains_key("relative");
|
||||||
cx.manager.tabs.switch(step, rel)
|
self.cx.manager.tabs.switch(step, rel)
|
||||||
}
|
}
|
||||||
"tab_swap" => {
|
"tab_swap" => {
|
||||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
cx.manager.tabs.swap(step)
|
self.cx.manager.tabs.swap(step)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
"tasks_show" => cx.tasks.toggle(),
|
"tasks_show" => self.cx.tasks.toggle(),
|
||||||
|
|
||||||
// Help
|
// Help
|
||||||
"help" => cx.help.toggle(cx.layer()),
|
"help" => self.cx.help.toggle(KeymapLayer::Manager),
|
||||||
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tasks(cx: &mut Ctx, exec: &Exec) -> bool {
|
fn tasks(&mut self, exec: &Exec) -> bool {
|
||||||
match exec.cmd.as_str() {
|
match exec.cmd.as_str() {
|
||||||
"close" => cx.tasks.toggle(),
|
"close" => self.cx.tasks.toggle(),
|
||||||
|
|
||||||
"arrow" => {
|
"arrow" => {
|
||||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
if step > 0 { cx.tasks.next() } else { cx.tasks.prev() }
|
if step > 0 { self.cx.tasks.next() } else { self.cx.tasks.prev() }
|
||||||
}
|
}
|
||||||
|
|
||||||
"inspect" => cx.tasks.inspect(),
|
"inspect" => self.cx.tasks.inspect(),
|
||||||
"cancel" => cx.tasks.cancel(),
|
"cancel" => self.cx.tasks.cancel(),
|
||||||
|
|
||||||
"help" => cx.help.toggle(cx.layer()),
|
"help" => self.cx.help.toggle(KeymapLayer::Tasks),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select(cx: &mut Ctx, exec: &Exec) -> bool {
|
fn select(&mut self, exec: &Exec) -> bool {
|
||||||
match exec.cmd.as_str() {
|
match exec.cmd.as_str() {
|
||||||
"close" => cx.select.close(exec.named.contains_key("submit")),
|
"close" => self.cx.select.close(exec.named.contains_key("submit")),
|
||||||
|
|
||||||
"arrow" => {
|
"arrow" => {
|
||||||
let step: isize = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
let step: isize = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
if step > 0 { cx.select.next(step as usize) } else { cx.select.prev(step.unsigned_abs()) }
|
if step > 0 {
|
||||||
|
self.cx.select.next(step as usize)
|
||||||
|
} else {
|
||||||
|
self.cx.select.prev(step.unsigned_abs())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"help" => cx.help.toggle(cx.layer()),
|
"help" => self.cx.help.toggle(KeymapLayer::Select),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input(cx: &mut Ctx, exec: &Exec) -> bool {
|
fn input(&mut self, exec: &Exec) -> bool {
|
||||||
match exec.cmd.as_str() {
|
match exec.cmd.as_str() {
|
||||||
"close" => return cx.input.close(exec.named.contains_key("submit")),
|
"close" => return self.cx.input.close(exec.named.contains_key("submit")),
|
||||||
"escape" => return cx.input.escape(),
|
"escape" => return self.cx.input.escape(),
|
||||||
|
|
||||||
"move" => {
|
"move" => {
|
||||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
let in_operating = exec.named.contains_key("in-operating");
|
let in_operating = exec.named.contains_key("in-operating");
|
||||||
return if in_operating { cx.input.move_in_operating(step) } else { cx.input.move_(step) };
|
return if in_operating {
|
||||||
|
self.cx.input.move_in_operating(step)
|
||||||
|
} else {
|
||||||
|
self.cx.input.move_(step)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
"complete" => {
|
||||||
|
return if exec.args.is_empty() {
|
||||||
|
self.cx.completion.trigger(exec)
|
||||||
|
} else {
|
||||||
|
self.cx.input.complete(exec)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match cx.input.mode() {
|
match self.cx.input.mode() {
|
||||||
InputMode::Normal => match exec.cmd.as_str() {
|
InputMode::Normal => match exec.cmd.as_str() {
|
||||||
"insert" => cx.input.insert(exec.named.contains_key("append")),
|
"insert" => self.cx.input.insert(exec.named.contains_key("append")),
|
||||||
"visual" => cx.input.visual(),
|
"visual" => self.cx.input.visual(),
|
||||||
|
|
||||||
"backward" => cx.input.backward(),
|
"backward" => self.cx.input.backward(),
|
||||||
"forward" => cx.input.forward(exec.named.contains_key("end-of-word")),
|
"forward" => self.cx.input.forward(exec.named.contains_key("end-of-word")),
|
||||||
"delete" => {
|
"delete" => {
|
||||||
cx.input.delete(exec.named.contains_key("cut"), exec.named.contains_key("insert"))
|
self.cx.input.delete(exec.named.contains_key("cut"), exec.named.contains_key("insert"))
|
||||||
}
|
}
|
||||||
|
|
||||||
"yank" => cx.input.yank(),
|
"yank" => self.cx.input.yank(),
|
||||||
"paste" => cx.input.paste(exec.named.contains_key("before")),
|
"paste" => self.cx.input.paste(exec.named.contains_key("before")),
|
||||||
|
|
||||||
"undo" => cx.input.undo(),
|
"undo" => self.cx.input.undo(),
|
||||||
"redo" => cx.input.redo(),
|
"redo" => self.cx.input.redo(),
|
||||||
|
|
||||||
"help" => cx.help.toggle(cx.layer()),
|
"help" => self.cx.help.toggle(KeymapLayer::Input),
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
InputMode::Insert => false,
|
InputMode::Insert => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn help(cx: &mut Ctx, exec: &Exec) -> bool {
|
fn help(&mut self, exec: &Exec) -> bool {
|
||||||
match exec.cmd.as_str() {
|
match exec.cmd.as_str() {
|
||||||
"close" => cx.help.toggle(cx.layer()),
|
"close" => self.cx.help.toggle(KeymapLayer::Help),
|
||||||
"escape" => cx.help.escape(),
|
"escape" => self.cx.help.escape(),
|
||||||
|
|
||||||
"arrow" => {
|
"arrow" => {
|
||||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
cx.help.arrow(step)
|
self.cx.help.arrow(step)
|
||||||
}
|
}
|
||||||
|
|
||||||
"filter" => cx.help.filter(),
|
"filter" => self.cx.help.filter(),
|
||||||
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn completion(&mut self, exec: &Exec) -> bool {
|
||||||
|
match exec.cmd.as_str() {
|
||||||
|
"trigger" => self.cx.completion.trigger(exec),
|
||||||
|
"show" => self.cx.completion.show(exec),
|
||||||
|
"close" => self.cx.completion.close(exec),
|
||||||
|
|
||||||
|
"arrow" => self.cx.completion.arrow(exec),
|
||||||
|
|
||||||
|
"help" => self.cx.help.toggle(KeymapLayer::Completion),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ impl<'a> Widget for Input<'a> {
|
|||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(THEME.input.border.into())
|
.border_style(THEME.input.border.into())
|
||||||
.title({
|
.title({
|
||||||
let mut line = Line::from(input.title());
|
let mut line = Line::from(input.title.as_str());
|
||||||
line.patch_style(THEME.input.title.into());
|
line.patch_style(THEME.input.title.into());
|
||||||
line
|
line
|
||||||
}),
|
}),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#![allow(clippy::module_inception)]
|
#![allow(clippy::module_inception)]
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
mod completion;
|
||||||
mod executor;
|
mod executor;
|
||||||
mod help;
|
mod help;
|
||||||
mod input;
|
mod input;
|
||||||
|
@ -2,7 +2,7 @@ use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, wid
|
|||||||
use yazi_core::Ctx;
|
use yazi_core::Ctx;
|
||||||
use yazi_plugin::components;
|
use yazi_plugin::components;
|
||||||
|
|
||||||
use super::{input, select, tasks, which};
|
use super::{completion, input, select, tasks, which};
|
||||||
use crate::help;
|
use crate::help;
|
||||||
|
|
||||||
pub(super) struct Root<'a> {
|
pub(super) struct Root<'a> {
|
||||||
@ -40,6 +40,10 @@ impl<'a> Widget for Root<'a> {
|
|||||||
help::Layout::new(self.cx).render(area, buf);
|
help::Layout::new(self.cx).render(area, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.cx.completion.visible {
|
||||||
|
completion::Completion::new(self.cx).render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
if self.cx.which.visible {
|
if self.cx.which.visible {
|
||||||
which::Which::new(self.cx).render(area, buf);
|
which::Which::new(self.cx).render(area, buf);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ mod tasks;
|
|||||||
|
|
||||||
pub use active::*;
|
pub use active::*;
|
||||||
pub use bindings::*;
|
pub use bindings::*;
|
||||||
|
#[allow(unused_imports)]
|
||||||
pub use files::*;
|
pub use files::*;
|
||||||
pub use shared::*;
|
pub use shared::*;
|
||||||
pub use tabs::*;
|
pub use tabs::*;
|
||||||
|
@ -3,6 +3,7 @@ use std::{error::Error, fmt::{self, Display}};
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum InputError {
|
pub enum InputError {
|
||||||
Typed(String),
|
Typed(String),
|
||||||
|
Completed(String, usize),
|
||||||
Canceled(String),
|
Canceled(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ impl Display for InputError {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Typed(text) => write!(f, "Typed error: {text}"),
|
Self::Typed(text) => write!(f, "Typed error: {text}"),
|
||||||
|
Self::Completed(text, _) => write!(f, "Completed error: {text}"),
|
||||||
Self::Canceled(text) => write!(f, "Canceled error: {text}"),
|
Self::Canceled(text) => write!(f, "Canceled error: {text}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
#![allow(clippy::module_inception)]
|
||||||
|
|
||||||
mod cursor;
|
mod cursor;
|
||||||
mod term;
|
mod term;
|
||||||
|
|
||||||
pub use cursor::*;
|
|
||||||
pub use term::*;
|
pub use term::*;
|
||||||
|
Loading…
Reference in New Issue
Block a user