feat: which-key and multi-key support (#4)

This commit is contained in:
三咲雅 · Misaki Masa 2023-07-24 07:26:16 +08:00 committed by GitHub
parent ca3c7c90b5
commit 1f8c170359
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 378 additions and 105 deletions

24
Cargo.lock generated
View file

@ -382,9 +382,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "either"
version = "1.8.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "equivalent"
@ -665,15 +665,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "http"
version = "0.2.9"
@ -1334,9 +1325,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.31"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
dependencies = [
"proc-macro2",
]
@ -2358,12 +2349,9 @@ dependencies = [
[[package]]
name = "xdg"
version = "2.5.0"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "688597db5a750e9cad4511cb94729a078e274308099a0382b5b8203bbc767fee"
dependencies = [
"home",
]
checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
[[package]]
name = "yaml-rust"

View file

@ -2,7 +2,7 @@ use std::{collections::BTreeMap, fmt};
use serde::{de::{self, Visitor}, Deserializer};
#[derive(Debug, Default)]
#[derive(Clone, Debug, Default)]
pub struct Exec {
pub cmd: 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 {
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Exec>, D::Error>
where

View file

@ -102,3 +102,62 @@ impl TryFrom<String> for 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 + ">"
}
}

View file

@ -3,8 +3,8 @@ use serde::{Deserialize, Deserializer};
use super::{Exec, Key};
use crate::config::MERGED_KEYMAP;
#[derive(Debug, Deserialize)]
pub struct Single {
#[derive(Clone, Debug, Deserialize)]
pub struct Control {
pub on: Vec<Key>,
#[serde(deserialize_with = "Exec::deserialize")]
pub exec: Vec<Exec>,
@ -12,10 +12,19 @@ pub struct Single {
#[derive(Debug)]
pub struct Keymap {
pub manager: Vec<Single>,
pub tasks: Vec<Single>,
pub select: Vec<Single>,
pub input: Vec<Single>,
pub manager: Vec<Control>,
pub tasks: Vec<Control>,
pub select: Vec<Control>,
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 {
@ -32,7 +41,7 @@ impl<'de> Deserialize<'de> for Keymap {
}
#[derive(Deserialize)]
struct Inner {
keymap: Vec<Single>,
keymap: Vec<Control>,
}
let shadow = Shadow::deserialize(deserializer)?;
@ -47,4 +56,15 @@ impl<'de> Deserialize<'de> for Keymap {
impl Keymap {
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!(),
}
}
}

View file

@ -64,7 +64,8 @@ pub struct Theme {
impl Theme {
pub fn new() -> Self {
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
}
}

View file

@ -5,16 +5,17 @@ use crossterm::event::KeyEvent;
use tokio::sync::{mpsc::UnboundedSender, oneshot};
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;
pub enum Event {
Quit,
Stop(bool, Option<oneshot::Sender<()>>),
Key(KeyEvent),
Render(String),
Resize(u16, u16),
Stop(bool, Option<oneshot::Sender<()>>),
Ctrl(Control, KeymapLayer),
// Manager
Cd(PathBuf),
@ -57,10 +58,6 @@ impl Event {
#[macro_export]
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)) => {
$crate::core::Event::Key($key).emit();
};
@ -70,6 +67,13 @@ macro_rules! emit {
(Resize($cols:expr, $rows:expr)) => {
$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)) => {
$crate::core::Event::Cd($op).emit();

View file

@ -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;
@ -25,6 +25,8 @@ impl Manager {
}
pub fn refresh(&mut self) {
env::set_current_dir(&self.current().cwd).ok();
self.watcher.trigger(&self.current().cwd);
if let Some(p) = self.parent() {
self.watcher.trigger(&p.cwd);

View file

@ -8,6 +8,7 @@ pub mod manager;
pub mod position;
pub mod select;
pub mod tasks;
pub mod which;
pub use blocker::*;
pub use event::*;

4
src/core/which/mod.rs Normal file
View file

@ -0,0 +1,4 @@
mod which;
pub use which::*;

52
src/core/which/which.rs Normal file
View 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;
}
}

View file

@ -2,7 +2,8 @@ use std::{env, path::{Path, PathBuf}};
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 let Ok(home) = env::var("HOME") {
let mut expanded = PathBuf::new();
@ -11,7 +12,7 @@ pub fn absolute_path(p: &Path) -> PathBuf {
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 {

View file

@ -3,7 +3,7 @@ use crossterm::event::KeyEvent;
use tokio::sync::oneshot::{self};
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 {
cx: Ctx,
@ -22,30 +22,17 @@ impl App {
while let Some(event) = app.signals.rx.recv().await {
match event {
Event::Quit => break,
Event::Stop(state, tx) => app.dispatch_stop(state, tx),
Event::Key(key) => app.dispatch_key(key),
Event::Render(_) => app.dispatch_render(),
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,
}
}
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) {
let key = Key::from(key);
if Executor::handle(&mut self.cx, key) {
@ -71,12 +58,33 @@ impl App {
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) {
let manager = &mut self.cx.manager;
let tasks = &mut self.cx.tasks;
match event {
Event::Cd(path) => {
manager.active_mut().cd(path).await;
manager.active_mut().cd(absolute_path(path).await).await;
}
Event::Refresh => {
manager.refresh();

View file

@ -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 cursor: Option<(u16, u16)>,
pub manager: Manager,
pub which: Which,
pub select: Select,
pub input: Input,
pub tasks: Tasks,
@ -15,12 +16,28 @@ impl Ctx {
cursor: None,
manager: Manager::new(),
select: Select::default(),
input: Input::default(),
which: Default::default(),
select: Default::default(),
input: Default::default(),
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 {
match pos {
Position::Top => Position::Coords((tty_size().ws_col / 2).saturating_sub(25), 2),

View file

@ -1,52 +1,49 @@
use std::path::PathBuf;
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;
impl Executor {
pub fn handle(cx: &mut Ctx, key: Key) -> bool {
let layer = if cx.input.visible {
3
} else if cx.select.visible {
2
} else if cx.tasks.visible {
1
} else {
0
};
let layer = cx.layer();
if layer == KeymapLayer::Which {
return cx.which.press(key);
}
let mut render = false;
let mut matched = false;
let keymap = [&KEYMAP.manager, &KEYMAP.tasks, &KEYMAP.select, &KEYMAP.input][layer];
if layer == KeymapLayer::Input && cx.input.mode() == InputMode::Insert {
if let Some(c) = key.plain() {
return cx.input.type_(c);
}
}
for Single { on, exec } in keymap {
if on.len() < 1 || on[0] != key {
for Control { on, exec } in KEYMAP.get(layer) {
if on.is_empty() || on[0] != key {
continue;
}
matched = true;
for e in exec {
if layer == 0 {
render = Self::manager(cx, e) || render;
} 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;
}
}
}
return if on.len() > 1 {
cx.which.show(&key, layer)
} else {
Self::dispatch(cx, exec, layer)
};
}
false
}
if layer == 3 && !matched {
render = Self::input(cx, None, &key);
#[inline]
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
}
@ -65,6 +62,11 @@ impl Executor {
"enter" => cx.manager.active_mut().enter(),
"back" => cx.manager.active_mut().back(),
"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
"select" => {
@ -170,18 +172,7 @@ impl Executor {
}
}
fn input(cx: &mut Ctx, exec: Option<&Exec>, key: &Key) -> 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;
};
fn input(cx: &mut Ctx, exec: &Exec) -> bool {
match exec.cmd.as_str() {
"close" => return cx.input.close(exec.named.contains_key("submit")),
"escape" => return cx.input.escape(),

View file

@ -10,7 +10,7 @@ impl Logs {
.context("failed to get XDG base directories")?
.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 filter = EnvFilter::from_default_env();

View file

@ -11,6 +11,7 @@ mod signals;
mod status;
mod tasks;
mod term;
mod which;
pub use app::*;
pub use context::*;

View file

@ -1,6 +1,6 @@
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> {
cx: &'a mut Ctx,
@ -35,5 +35,9 @@ impl<'a> Widget for Root<'a> {
} else {
self.cx.cursor = None;
}
if self.cx.which.visible {
Which::new(self.cx).render(area, buf);
}
}
}

47
src/ui/which/layout.rs Normal file
View 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
View file

@ -0,0 +1,5 @@
mod layout;
mod side;
pub use layout::*;
pub use side::*;

49
src/ui/which/side.rs Normal file
View 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);
}
}