mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: which-key and multi-key support (#4)
This commit is contained in:
parent
ca3c7c90b5
commit
1f8c170359
20 changed files with 378 additions and 105 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -382,9 +382,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.8.1"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
|
|
@ -665,15 +665,6 @@ version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
|
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "home"
|
|
||||||
version = "0.5.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
|
|
@ -1334,9 +1325,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.31"
|
version = "1.0.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
|
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
@ -2358,12 +2349,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xdg"
|
name = "xdg"
|
||||||
version = "2.5.0"
|
version = "2.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "688597db5a750e9cad4511cb94729a078e274308099a0382b5b8203bbc767fee"
|
checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
|
||||||
dependencies = [
|
|
||||||
"home",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaml-rust"
|
name = "yaml-rust"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::{collections::BTreeMap, fmt};
|
||||||
|
|
||||||
use serde::{de::{self, Visitor}, Deserializer};
|
use serde::{de::{self, Visitor}, Deserializer};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Exec {
|
pub struct Exec {
|
||||||
pub cmd: String,
|
pub cmd: String,
|
||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
|
|
@ -28,6 +28,25 @@ impl From<&str> for Exec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToString for Exec {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut s = self.cmd.clone();
|
||||||
|
for arg in &self.args {
|
||||||
|
s.push(' ');
|
||||||
|
s.push_str(arg);
|
||||||
|
}
|
||||||
|
for (name, value) in &self.named {
|
||||||
|
s.push_str(" --");
|
||||||
|
s.push_str(name);
|
||||||
|
if !value.is_empty() {
|
||||||
|
s.push('=');
|
||||||
|
s.push_str(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Exec {
|
impl Exec {
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Exec>, D::Error>
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Exec>, D::Error>
|
||||||
where
|
where
|
||||||
|
|
|
||||||
|
|
@ -102,3 +102,62 @@ impl TryFrom<String> for Key {
|
||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToString for Key {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
if let Some(c) = self.plain() {
|
||||||
|
let c = if self.shift { c.to_ascii_uppercase() } else { c };
|
||||||
|
return if c == ' ' { "<Space>".to_string() } else { c.to_string() };
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut s = "<".to_string();
|
||||||
|
if self.ctrl {
|
||||||
|
s += "C-";
|
||||||
|
}
|
||||||
|
if self.alt {
|
||||||
|
s += "A-";
|
||||||
|
}
|
||||||
|
if self.shift && !matches!(self.code, KeyCode::Char(_)) {
|
||||||
|
s += "S-";
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = match self.code {
|
||||||
|
KeyCode::Char(' ') => "Space",
|
||||||
|
KeyCode::Backspace => "Backspace",
|
||||||
|
KeyCode::Enter => "Enter",
|
||||||
|
KeyCode::Left => "Left",
|
||||||
|
KeyCode::Right => "Right",
|
||||||
|
KeyCode::Up => "Up",
|
||||||
|
KeyCode::Down => "Down",
|
||||||
|
KeyCode::Home => "Home",
|
||||||
|
KeyCode::End => "End",
|
||||||
|
KeyCode::PageUp => "PageUp",
|
||||||
|
KeyCode::PageDown => "PageDown",
|
||||||
|
KeyCode::Tab => "Tab",
|
||||||
|
KeyCode::Delete => "Delete",
|
||||||
|
KeyCode::Insert => "Insert",
|
||||||
|
KeyCode::F(1) => "F1",
|
||||||
|
KeyCode::F(2) => "F2",
|
||||||
|
KeyCode::F(3) => "F3",
|
||||||
|
KeyCode::F(4) => "F4",
|
||||||
|
KeyCode::F(5) => "F5",
|
||||||
|
KeyCode::F(6) => "F6",
|
||||||
|
KeyCode::F(7) => "F7",
|
||||||
|
KeyCode::F(8) => "F8",
|
||||||
|
KeyCode::F(9) => "F9",
|
||||||
|
KeyCode::F(10) => "F10",
|
||||||
|
KeyCode::F(11) => "F11",
|
||||||
|
KeyCode::F(12) => "F12",
|
||||||
|
KeyCode::Esc => "Esc",
|
||||||
|
|
||||||
|
KeyCode::Char(c) if c == ' ' => "Space",
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
s.push(if self.shift { c.to_ascii_uppercase() } else { c });
|
||||||
|
""
|
||||||
|
}
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
s + code + ">"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ use serde::{Deserialize, Deserializer};
|
||||||
use super::{Exec, Key};
|
use super::{Exec, Key};
|
||||||
use crate::config::MERGED_KEYMAP;
|
use crate::config::MERGED_KEYMAP;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct Single {
|
pub struct Control {
|
||||||
pub on: Vec<Key>,
|
pub on: Vec<Key>,
|
||||||
#[serde(deserialize_with = "Exec::deserialize")]
|
#[serde(deserialize_with = "Exec::deserialize")]
|
||||||
pub exec: Vec<Exec>,
|
pub exec: Vec<Exec>,
|
||||||
|
|
@ -12,10 +12,19 @@ pub struct Single {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Keymap {
|
pub struct Keymap {
|
||||||
pub manager: Vec<Single>,
|
pub manager: Vec<Control>,
|
||||||
pub tasks: Vec<Single>,
|
pub tasks: Vec<Control>,
|
||||||
pub select: Vec<Single>,
|
pub select: Vec<Control>,
|
||||||
pub input: Vec<Single>,
|
pub input: Vec<Control>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||||
|
pub enum KeymapLayer {
|
||||||
|
Manager,
|
||||||
|
Tasks,
|
||||||
|
Select,
|
||||||
|
Input,
|
||||||
|
Which,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Keymap {
|
impl<'de> Deserialize<'de> for Keymap {
|
||||||
|
|
@ -32,7 +41,7 @@ impl<'de> Deserialize<'de> for Keymap {
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Inner {
|
struct Inner {
|
||||||
keymap: Vec<Single>,
|
keymap: Vec<Control>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let shadow = Shadow::deserialize(deserializer)?;
|
let shadow = Shadow::deserialize(deserializer)?;
|
||||||
|
|
@ -47,4 +56,15 @@ impl<'de> Deserialize<'de> for Keymap {
|
||||||
|
|
||||||
impl Keymap {
|
impl Keymap {
|
||||||
pub fn new() -> Self { toml::from_str(&MERGED_KEYMAP).unwrap() }
|
pub fn new() -> Self { toml::from_str(&MERGED_KEYMAP).unwrap() }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get(&self, layer: KeymapLayer) -> &Vec<Control> {
|
||||||
|
match layer {
|
||||||
|
KeymapLayer::Manager => &self.manager,
|
||||||
|
KeymapLayer::Tasks => &self.tasks,
|
||||||
|
KeymapLayer::Select => &self.select,
|
||||||
|
KeymapLayer::Input => &self.input,
|
||||||
|
KeymapLayer::Which => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,8 @@ pub struct Theme {
|
||||||
impl Theme {
|
impl Theme {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap();
|
let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap();
|
||||||
theme.preview.syntect_theme = absolute_path(&theme.preview.syntect_theme);
|
theme.preview.syntect_theme =
|
||||||
|
futures::executor::block_on(absolute_path(&theme.preview.syntect_theme));
|
||||||
theme
|
theme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,17 @@ use crossterm::event::KeyEvent;
|
||||||
use tokio::sync::{mpsc::UnboundedSender, oneshot};
|
use tokio::sync::{mpsc::UnboundedSender, oneshot};
|
||||||
|
|
||||||
use super::{files::FilesOp, input::InputOpt, manager::PreviewData, select::SelectOpt};
|
use super::{files::FilesOp, input::InputOpt, manager::PreviewData, select::SelectOpt};
|
||||||
use crate::config::open::Opener;
|
use crate::config::{keymap::{Control, KeymapLayer}, open::Opener};
|
||||||
|
|
||||||
static mut TX: Option<UnboundedSender<Event>> = None;
|
static mut TX: Option<UnboundedSender<Event>> = None;
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Quit,
|
Quit,
|
||||||
Stop(bool, Option<oneshot::Sender<()>>),
|
|
||||||
Key(KeyEvent),
|
Key(KeyEvent),
|
||||||
Render(String),
|
Render(String),
|
||||||
Resize(u16, u16),
|
Resize(u16, u16),
|
||||||
|
Stop(bool, Option<oneshot::Sender<()>>),
|
||||||
|
Ctrl(Control, KeymapLayer),
|
||||||
|
|
||||||
// Manager
|
// Manager
|
||||||
Cd(PathBuf),
|
Cd(PathBuf),
|
||||||
|
|
@ -57,10 +58,6 @@ impl Event {
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! emit {
|
macro_rules! emit {
|
||||||
(Stop($state:expr)) => {{
|
|
||||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
||||||
$crate::core::Event::Stop($state, Some(tx)).wait(rx)
|
|
||||||
}};
|
|
||||||
(Key($key:expr)) => {
|
(Key($key:expr)) => {
|
||||||
$crate::core::Event::Key($key).emit();
|
$crate::core::Event::Key($key).emit();
|
||||||
};
|
};
|
||||||
|
|
@ -70,6 +67,13 @@ macro_rules! emit {
|
||||||
(Resize($cols:expr, $rows:expr)) => {
|
(Resize($cols:expr, $rows:expr)) => {
|
||||||
$crate::core::Event::Resize($cols, $rows).emit();
|
$crate::core::Event::Resize($cols, $rows).emit();
|
||||||
};
|
};
|
||||||
|
(Stop($state:expr)) => {{
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
$crate::core::Event::Stop($state, Some(tx)).wait(rx)
|
||||||
|
}};
|
||||||
|
(Ctrl($exec:expr, $layer:expr)) => {
|
||||||
|
$crate::core::Event::Ctrl($exec, $layer).emit();
|
||||||
|
};
|
||||||
|
|
||||||
(Cd($op:expr)) => {
|
(Cd($op:expr)) => {
|
||||||
$crate::core::Event::Cd($op).emit();
|
$crate::core::Event::Cd($op).emit();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, mem, path::PathBuf};
|
use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, mem, path::PathBuf};
|
||||||
|
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
|
|
@ -25,6 +25,8 @@ impl Manager {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh(&mut self) {
|
pub fn refresh(&mut self) {
|
||||||
|
env::set_current_dir(&self.current().cwd).ok();
|
||||||
|
|
||||||
self.watcher.trigger(&self.current().cwd);
|
self.watcher.trigger(&self.current().cwd);
|
||||||
if let Some(p) = self.parent() {
|
if let Some(p) = self.parent() {
|
||||||
self.watcher.trigger(&p.cwd);
|
self.watcher.trigger(&p.cwd);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub mod manager;
|
||||||
pub mod position;
|
pub mod position;
|
||||||
pub mod select;
|
pub mod select;
|
||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
|
pub mod which;
|
||||||
|
|
||||||
pub use blocker::*;
|
pub use blocker::*;
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
|
|
|
||||||
4
src/core/which/mod.rs
Normal file
4
src/core/which/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
mod which;
|
||||||
|
|
||||||
|
pub use which::*;
|
||||||
|
|
||||||
52
src/core/which/which.rs
Normal file
52
src/core/which/which.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use crate::{config::{keymap::{Control, Key, KeymapLayer}, KEYMAP}, emit};
|
||||||
|
|
||||||
|
pub struct Which {
|
||||||
|
layer: KeymapLayer,
|
||||||
|
pub times: usize,
|
||||||
|
pub cands: Vec<Control>,
|
||||||
|
|
||||||
|
pub visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Which {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { layer: KeymapLayer::Manager, times: 0, cands: Default::default(), visible: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Which {
|
||||||
|
pub fn show(&mut self, key: &Key, layer: KeymapLayer) -> bool {
|
||||||
|
self.layer = layer;
|
||||||
|
self.times = 1;
|
||||||
|
self.cands = KEYMAP
|
||||||
|
.get(layer)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| !s.on.is_empty() && s.on[0] == *key)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
self.visible = true;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn press(&mut self, key: Key) -> bool {
|
||||||
|
self.cands = mem::replace(&mut self.cands, Vec::new())
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| s.on.len() > self.times && s.on[self.times] == key)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if self.cands.is_empty() {
|
||||||
|
self.visible = false;
|
||||||
|
} else if self.cands.len() == 1 {
|
||||||
|
self.visible = false;
|
||||||
|
emit!(Ctrl(self.cands.remove(0), self.layer));
|
||||||
|
} else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times + 1) {
|
||||||
|
emit!(Ctrl(self.cands.remove(i), self.layer));
|
||||||
|
self.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.times += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,8 @@ use std::{env, path::{Path, PathBuf}};
|
||||||
|
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
pub fn absolute_path(p: &Path) -> PathBuf {
|
pub async fn absolute_path(p: impl AsRef<Path>) -> PathBuf {
|
||||||
|
let p = p.as_ref();
|
||||||
if p.starts_with("~") {
|
if p.starts_with("~") {
|
||||||
if let Ok(home) = env::var("HOME") {
|
if let Ok(home) = env::var("HOME") {
|
||||||
let mut expanded = PathBuf::new();
|
let mut expanded = PathBuf::new();
|
||||||
|
|
@ -11,7 +12,7 @@ pub fn absolute_path(p: &Path) -> PathBuf {
|
||||||
return expanded;
|
return expanded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.to_path_buf()
|
fs::canonicalize(p).await.unwrap_or_else(|_| p.to_path_buf())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn readable_path(p: &Path, base: &Path) -> String {
|
pub fn readable_path(p: &Path, base: &Path) -> String {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crossterm::event::KeyEvent;
|
||||||
use tokio::sync::oneshot::{self};
|
use tokio::sync::oneshot::{self};
|
||||||
|
|
||||||
use super::{root::Root, Ctx, Executor, Logs, Signals, Term};
|
use super::{root::Root, Ctx, Executor, Logs, Signals, Term};
|
||||||
use crate::{config::keymap::Key, core::{files::FilesOp, Event}, emit};
|
use crate::{config::keymap::{Control, Key, KeymapLayer}, core::{files::FilesOp, Event}, emit, misc::absolute_path};
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
cx: Ctx,
|
cx: Ctx,
|
||||||
|
|
@ -22,30 +22,17 @@ impl App {
|
||||||
while let Some(event) = app.signals.rx.recv().await {
|
while let Some(event) = app.signals.rx.recv().await {
|
||||||
match event {
|
match event {
|
||||||
Event::Quit => break,
|
Event::Quit => break,
|
||||||
Event::Stop(state, tx) => app.dispatch_stop(state, tx),
|
|
||||||
Event::Key(key) => app.dispatch_key(key),
|
Event::Key(key) => app.dispatch_key(key),
|
||||||
Event::Render(_) => app.dispatch_render(),
|
Event::Render(_) => app.dispatch_render(),
|
||||||
Event::Resize(..) => app.dispatch_resize(),
|
Event::Resize(..) => app.dispatch_resize(),
|
||||||
|
Event::Stop(state, tx) => app.dispatch_stop(state, tx),
|
||||||
|
Event::Ctrl(ctrl, layer) => app.dispatch_ctrl(ctrl, layer),
|
||||||
event => app.dispatch_module(event).await,
|
event => app.dispatch_module(event).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_stop(&mut self, state: bool, tx: Option<oneshot::Sender<()>>) {
|
|
||||||
if state {
|
|
||||||
self.signals.stop_term(true);
|
|
||||||
self.term = None;
|
|
||||||
} else {
|
|
||||||
self.term = Some(Term::start().unwrap());
|
|
||||||
self.signals.stop_term(false);
|
|
||||||
emit!(Render);
|
|
||||||
}
|
|
||||||
if let Some(tx) = tx {
|
|
||||||
tx.send(()).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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::handle(&mut self.cx, key) {
|
||||||
|
|
@ -71,12 +58,33 @@ impl App {
|
||||||
emit!(Render);
|
emit!(Render);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatch_stop(&mut self, state: bool, tx: Option<oneshot::Sender<()>>) {
|
||||||
|
if state {
|
||||||
|
self.signals.stop_term(true);
|
||||||
|
self.term = None;
|
||||||
|
} else {
|
||||||
|
self.term = Some(Term::start().unwrap());
|
||||||
|
self.signals.stop_term(false);
|
||||||
|
emit!(Render);
|
||||||
|
}
|
||||||
|
if let Some(tx) = tx {
|
||||||
|
tx.send(()).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn dispatch_ctrl(&mut self, ctrl: Control, layer: KeymapLayer) {
|
||||||
|
if Executor::dispatch(&mut self.cx, &ctrl.exec, layer) {
|
||||||
|
emit!(Render);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn dispatch_module(&mut self, event: Event) {
|
async fn dispatch_module(&mut self, event: Event) {
|
||||||
let manager = &mut self.cx.manager;
|
let manager = &mut self.cx.manager;
|
||||||
let tasks = &mut self.cx.tasks;
|
let tasks = &mut self.cx.tasks;
|
||||||
match event {
|
match event {
|
||||||
Event::Cd(path) => {
|
Event::Cd(path) => {
|
||||||
manager.active_mut().cd(path).await;
|
manager.active_mut().cd(absolute_path(path).await).await;
|
||||||
}
|
}
|
||||||
Event::Refresh => {
|
Event::Refresh => {
|
||||||
manager.refresh();
|
manager.refresh();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use crate::{core::{input::Input, manager::Manager, select::Select, tasks::Tasks, Position}, misc::tty_size};
|
use crate::{config::keymap::KeymapLayer, core::{input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position}, misc::tty_size};
|
||||||
|
|
||||||
pub struct Ctx {
|
pub struct Ctx {
|
||||||
pub cursor: Option<(u16, u16)>,
|
pub cursor: Option<(u16, u16)>,
|
||||||
|
|
||||||
pub manager: Manager,
|
pub manager: Manager,
|
||||||
|
pub which: Which,
|
||||||
pub select: Select,
|
pub select: Select,
|
||||||
pub input: Input,
|
pub input: Input,
|
||||||
pub tasks: Tasks,
|
pub tasks: Tasks,
|
||||||
|
|
@ -15,12 +16,28 @@ impl Ctx {
|
||||||
cursor: None,
|
cursor: None,
|
||||||
|
|
||||||
manager: Manager::new(),
|
manager: Manager::new(),
|
||||||
select: Select::default(),
|
which: Default::default(),
|
||||||
input: Input::default(),
|
select: Default::default(),
|
||||||
|
input: Default::default(),
|
||||||
tasks: Tasks::start(),
|
tasks: Tasks::start(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn layer(&self) -> KeymapLayer {
|
||||||
|
if self.which.visible {
|
||||||
|
KeymapLayer::Which
|
||||||
|
} else if self.input.visible {
|
||||||
|
KeymapLayer::Input
|
||||||
|
} else if self.select.visible {
|
||||||
|
KeymapLayer::Select
|
||||||
|
} else if self.tasks.visible {
|
||||||
|
KeymapLayer::Tasks
|
||||||
|
} else {
|
||||||
|
KeymapLayer::Manager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn position(&self, pos: Position) -> Position {
|
pub fn position(&self, pos: Position) -> Position {
|
||||||
match pos {
|
match pos {
|
||||||
Position::Top => Position::Coords((tty_size().ws_col / 2).saturating_sub(25), 2),
|
Position::Top => Position::Coords((tty_size().ws_col / 2).saturating_sub(25), 2),
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,49 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::Ctx;
|
use super::Ctx;
|
||||||
use crate::{config::{keymap::{Exec, Key, Single}, KEYMAP}, core::input::InputMode, misc::optional_bool};
|
use crate::{config::{keymap::{Control, Exec, Key, KeymapLayer}, KEYMAP}, core::input::InputMode, emit, misc::optional_bool};
|
||||||
|
|
||||||
pub struct Executor;
|
pub struct Executor;
|
||||||
|
|
||||||
impl Executor {
|
impl Executor {
|
||||||
pub fn handle(cx: &mut Ctx, key: Key) -> bool {
|
pub fn handle(cx: &mut Ctx, key: Key) -> bool {
|
||||||
let layer = if cx.input.visible {
|
let layer = cx.layer();
|
||||||
3
|
if layer == KeymapLayer::Which {
|
||||||
} else if cx.select.visible {
|
return cx.which.press(key);
|
||||||
2
|
}
|
||||||
} else if cx.tasks.visible {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut render = false;
|
if layer == KeymapLayer::Input && cx.input.mode() == InputMode::Insert {
|
||||||
let mut matched = false;
|
if let Some(c) = key.plain() {
|
||||||
let keymap = [&KEYMAP.manager, &KEYMAP.tasks, &KEYMAP.select, &KEYMAP.input][layer];
|
return cx.input.type_(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for Single { on, exec } in keymap {
|
for Control { on, exec } in KEYMAP.get(layer) {
|
||||||
if on.len() < 1 || on[0] != key {
|
if on.is_empty() || on[0] != key {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
matched = true;
|
return if on.len() > 1 {
|
||||||
for e in exec {
|
cx.which.show(&key, layer)
|
||||||
if layer == 0 {
|
} else {
|
||||||
render = Self::manager(cx, e) || render;
|
Self::dispatch(cx, exec, layer)
|
||||||
} else if layer == 1 {
|
};
|
||||||
render = Self::tasks(cx, e) || render;
|
|
||||||
} else if layer == 2 {
|
|
||||||
render = Self::select(cx, e) || render;
|
|
||||||
} else if layer == 3 {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
if layer == 3 && !matched {
|
#[inline]
|
||||||
render = Self::input(cx, None, &key);
|
pub fn dispatch(cx: &mut Ctx, exec: &Vec<Exec>, layer: KeymapLayer) -> bool {
|
||||||
|
let mut render = false;
|
||||||
|
for e in exec {
|
||||||
|
render |= match layer {
|
||||||
|
KeymapLayer::Manager => Self::manager(cx, e),
|
||||||
|
KeymapLayer::Tasks => Self::tasks(cx, e),
|
||||||
|
KeymapLayer::Select => Self::select(cx, e),
|
||||||
|
KeymapLayer::Input => Self::input(cx, e),
|
||||||
|
KeymapLayer::Which => unreachable!(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render
|
render
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,6 +62,11 @@ impl Executor {
|
||||||
"enter" => cx.manager.active_mut().enter(),
|
"enter" => cx.manager.active_mut().enter(),
|
||||||
"back" => cx.manager.active_mut().back(),
|
"back" => cx.manager.active_mut().back(),
|
||||||
"forward" => cx.manager.active_mut().forward(),
|
"forward" => cx.manager.active_mut().forward(),
|
||||||
|
"cd" => {
|
||||||
|
let path = exec.args.get(0).map(|s| PathBuf::from(s)).unwrap_or_default();
|
||||||
|
emit!(Cd(path));
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
"select" => {
|
"select" => {
|
||||||
|
|
@ -170,18 +172,7 @@ impl Executor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input(cx: &mut Ctx, exec: Option<&Exec>, key: &Key) -> bool {
|
fn input(cx: &mut Ctx, exec: &Exec) -> bool {
|
||||||
let exec = if let Some(e) = exec {
|
|
||||||
e
|
|
||||||
} else {
|
|
||||||
if cx.input.mode() == InputMode::Insert {
|
|
||||||
if let Some(c) = key.plain() {
|
|
||||||
return cx.input.type_(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
match exec.cmd.as_str() {
|
match exec.cmd.as_str() {
|
||||||
"close" => return cx.input.close(exec.named.contains_key("submit")),
|
"close" => return cx.input.close(exec.named.contains_key("submit")),
|
||||||
"escape" => return cx.input.escape(),
|
"escape" => return cx.input.escape(),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ impl Logs {
|
||||||
.context("failed to get XDG base directories")?
|
.context("failed to get XDG base directories")?
|
||||||
.get_state_home();
|
.get_state_home();
|
||||||
|
|
||||||
let appender = tracing_appender::rolling::hourly(root, "yazi.log");
|
let appender = tracing_appender::rolling::never(root, "yazi.log");
|
||||||
let (handle, guard) = tracing_appender::non_blocking(appender);
|
let (handle, guard) = tracing_appender::non_blocking(appender);
|
||||||
|
|
||||||
// let filter = EnvFilter::from_default_env();
|
// let filter = EnvFilter::from_default_env();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ mod signals;
|
||||||
mod status;
|
mod status;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
mod term;
|
mod term;
|
||||||
|
mod which;
|
||||||
|
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use context::*;
|
pub use context::*;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget};
|
use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget};
|
||||||
|
|
||||||
use super::{header, manager, status, tasks, Ctx, Input, Select};
|
use super::{header, manager, status, tasks, which::Which, Ctx, Input, Select};
|
||||||
|
|
||||||
pub struct Root<'a> {
|
pub struct Root<'a> {
|
||||||
cx: &'a mut Ctx,
|
cx: &'a mut Ctx,
|
||||||
|
|
@ -35,5 +35,9 @@ impl<'a> Widget for Root<'a> {
|
||||||
} else {
|
} else {
|
||||||
self.cx.cursor = None;
|
self.cx.cursor = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.cx.which.visible {
|
||||||
|
Which::new(self.cx).render(area, buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
src/ui/which/layout.rs
Normal file
47
src/ui/which/layout.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, widgets::{Clear, Widget}};
|
||||||
|
|
||||||
|
use super::Side;
|
||||||
|
use crate::ui::Ctx;
|
||||||
|
|
||||||
|
pub struct Which<'a> {
|
||||||
|
cx: &'a mut Ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Which<'a> {
|
||||||
|
pub fn new(cx: &'a mut Ctx) -> Self { Self { cx } }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Which<'_> {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let which = &self.cx.which;
|
||||||
|
let mut cands: (Vec<_>, Vec<_>, Vec<_>) = Default::default();
|
||||||
|
for (i, c) in which.cands.iter().enumerate() {
|
||||||
|
match i % 3 {
|
||||||
|
0 => cands.0.push(c),
|
||||||
|
1 => cands.1.push(c),
|
||||||
|
2 => cands.2.push(c),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let height = cands.0.len() as u16 + 2;
|
||||||
|
let area = Rect {
|
||||||
|
x: 1,
|
||||||
|
y: area.height.saturating_sub(height + 2),
|
||||||
|
width: area.width.saturating_sub(2),
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
let chunks = layout::Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(
|
||||||
|
[Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3)].as_ref(),
|
||||||
|
)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
Clear.render(area, buf);
|
||||||
|
Side::new(which.times, cands.0).render(chunks[0], buf);
|
||||||
|
Side::new(which.times, cands.1).render(chunks[1], buf);
|
||||||
|
Side::new(which.times, cands.2).render(chunks[2], buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/ui/which/mod.rs
Normal file
5
src/ui/which/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
mod layout;
|
||||||
|
mod side;
|
||||||
|
|
||||||
|
pub use layout::*;
|
||||||
|
pub use side::*;
|
||||||
49
src/ui/which/side.rs
Normal file
49
src/ui/which/side.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
use ratatui::{prelude::{Buffer, Rect}, style::{Color, Style, Stylize}, text::{Line, Span}, widgets::{Block, List, ListItem, Padding, Widget}};
|
||||||
|
|
||||||
|
use crate::config::keymap::Control;
|
||||||
|
|
||||||
|
pub struct Side<'a> {
|
||||||
|
times: usize,
|
||||||
|
cands: Vec<&'a Control>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Side<'a> {
|
||||||
|
pub fn new(times: usize, cands: Vec<&'a Control>) -> Self { Self { times, cands } }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Side<'_> {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let items = self
|
||||||
|
.cands
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| {
|
||||||
|
let mut spans = vec![];
|
||||||
|
|
||||||
|
// Keys
|
||||||
|
let keys = c.on[self.times..].iter().map(ToString::to_string).collect::<Vec<_>>();
|
||||||
|
spans.push(Span::raw(" ".repeat(10usize.saturating_sub(keys.join("").len()))));
|
||||||
|
spans.push(Span::styled(keys[0].clone(), Style::default().fg(Color::LightCyan)));
|
||||||
|
spans.extend(
|
||||||
|
keys
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.map(|k| Span::styled(k.to_string(), Style::default().fg(Color::DarkGray))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
spans.push(Span::styled(" ".to_string(), Style::default().fg(Color::DarkGray)));
|
||||||
|
|
||||||
|
// Exec
|
||||||
|
let exec = c.exec.iter().map(ToString::to_string).collect::<Vec<_>>().join("; ");
|
||||||
|
spans.push(Span::styled(exec, Style::default().fg(Color::Magenta)));
|
||||||
|
|
||||||
|
ListItem::new(Line::from(spans))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
List::new(items)
|
||||||
|
.block(Block::new().padding(Padding::new(0, 1, 1, 1)))
|
||||||
|
.bg(Color::Rgb(47, 51, 73))
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue