From e52547de104ccdca17e9e37864ad7adb556e6022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Mon, 7 Aug 2023 13:31:24 +0800 Subject: [PATCH] feat: fix and enhance unset mode (#27) --- Cargo.lock | 75 ++++++++++++++++++++----------------- app/src/executor.rs | 2 +- app/src/manager/folder.rs | 7 +++- app/src/manager/layout.rs | 4 +- config/docs/keymap.md | 8 ++-- config/src/keymap/key.rs | 2 +- core/src/files/file.rs | 6 +-- core/src/manager/folder.rs | 7 +--- core/src/manager/manager.rs | 29 +++++++++++--- core/src/manager/mode.rs | 45 ++++++++++++++++------ core/src/manager/tab.rs | 27 +++++++------ 11 files changed, 128 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9343c69..d7d4c172 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,7 +75,7 @@ dependencies = [ "anyhow", "config", "core", - "crossterm", + "crossterm 0.27.0", "futures", "libc", "ratatui", @@ -238,7 +238,7 @@ name = "config" version = "0.1.0" dependencies = [ "anyhow", - "crossterm", + "crossterm 0.27.0", "futures", "glob", "once_cell", @@ -257,7 +257,7 @@ dependencies = [ "anyhow", "async-channel", "config", - "crossterm", + "crossterm 0.27.0", "futures", "indexmap 2.0.0", "notify", @@ -341,6 +341,22 @@ checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" dependencies = [ "bitflags 1.3.2", "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.3.3", + "crossterm_winapi", "futures-core", "libc", "mio", @@ -436,13 +452,13 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "windows-sys 0.48.0", ] @@ -778,9 +794,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ "kqueue-sys", "libc", @@ -788,9 +804,9 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", @@ -1062,7 +1078,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall", "smallvec", "windows-targets 0.48.1", ] @@ -1081,18 +1097,18 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", @@ -1101,9 +1117,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" [[package]] name = "pin-utils" @@ -1188,7 +1204,7 @@ checksum = "8285baa38bdc9f879d92c0e37cb562ef38aa3aeefca22b3200186bc39242d3d5" dependencies = [ "bitflags 2.3.3", "cassowary", - "crossterm", + "crossterm 0.26.1", "indoc", "paste", "unicode-segmentation", @@ -1217,15 +1233,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.3.5" @@ -1276,18 +1283,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.181" +version = "1.0.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d3e73c93c3240c0bda063c239298e633114c69a888c3e37ca8bb33f343e9890" +checksum = "bdb30a74471f5b7a1fa299f40b4bf1be93af61116df95465b2b5fc419331e430" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.181" +version = "1.0.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be02f6cb0cd3a5ec20bbcfbcbd749f57daddb1a0882dc2e46a6c236c90b977ed" +checksum = "6f4c2c6ea4bc09b5c419012eafcdb0fcef1d9119d626c8f3a0708a5b92d38a70" dependencies = [ "proc-macro2", "quote", @@ -1328,7 +1335,7 @@ name = "shared" version = "0.1.0" dependencies = [ "anyhow", - "crossterm", + "crossterm 0.27.0", "libc", "parking_lot", "ratatui", @@ -2005,9 +2012,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46aab759304e4d7b2075a9aecba26228bb073ee8c50db796b2c72c676b5d807" +checksum = "acaaa1190073b2b101e15083c38ee8ec891b5e05cbee516521e94ec008f61e64" dependencies = [ "memchr", ] diff --git a/app/src/executor.rs b/app/src/executor.rs index f61293d5..5bbc8b48 100644 --- a/app/src/executor.rs +++ b/app/src/executor.rs @@ -76,11 +76,11 @@ impl Executor { let state = exec.named.get("state").cloned().unwrap_or("none".to_string()); cx.manager.active_mut().select(optional_bool(&state)) } - "visual_mode" => cx.manager.active_mut().visual_mode(exec.named.contains_key("unset")), "select_all" => { let state = exec.named.get("state").cloned().unwrap_or("none".to_string()); cx.manager.active_mut().select_all(optional_bool(&state)) } + "visual_mode" => cx.manager.active_mut().visual_mode(exec.named.contains_key("unset")), // Operation "open" => cx.manager.open(exec.named.contains_key("interactive")), diff --git a/app/src/manager/folder.rs b/app/src/manager/folder.rs index bd88cfbd..ddc20bb6 100644 --- a/app/src/manager/folder.rs +++ b/app/src/manager/folder.rs @@ -15,7 +15,7 @@ pub(super) struct Folder<'a> { impl<'a> Folder<'a> { pub(super) fn new(cx: &'a Ctx, folder: &'a core::manager::Folder) -> Self { - Self { cx, folder, is_preview: false, is_selection: true } + Self { cx, folder, is_preview: false, is_selection: false } } #[inline] @@ -45,6 +45,7 @@ impl<'a> Folder<'a> { impl<'a> Widget for Folder<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let window = self.folder.window(); + let mode = self.cx.manager.active().mode(); let items = window .iter() @@ -57,7 +58,9 @@ impl<'a> Widget for Folder<'a> { .map(|x| x.display.as_ref()) .unwrap_or(""); - if v.is_selected { + if (!self.is_selection && v.is_selected) + || (self.is_selection && mode.pending(i, v.is_selected)) + { buf.set_style( Rect { x: area.x.saturating_sub(1), y: i as u16 + 1, width: 1, height: 1 }, if self.is_selection { diff --git a/app/src/manager/layout.rs b/app/src/manager/layout.rs index 073f5aeb..57e174ec 100644 --- a/app/src/manager/layout.rs +++ b/app/src/manager/layout.rs @@ -1,4 +1,4 @@ -use core::manager::{Mode, ALL_RATIO, CURRENT_RATIO, PARENT_RATIO, PREVIEW_RATIO}; +use core::manager::{ALL_RATIO, CURRENT_RATIO, PARENT_RATIO, PREVIEW_RATIO}; use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::{Block, Borders, Padding, Widget}}; @@ -38,7 +38,7 @@ impl<'a> Widget for Layout<'a> { // Current Folder::new(self.cx, manager.current()) - .with_selection(matches!(manager.active().mode(), Mode::Select(_))) + .with_selection(manager.active().mode().is_visual()) .render(chunks[1], buf); // Preview diff --git a/config/docs/keymap.md b/config/docs/keymap.md index 042ac98e..adf585fe 100644 --- a/config/docs/keymap.md +++ b/config/docs/keymap.md @@ -28,16 +28,16 @@ - `--state=false`: Deselect the current file. - `--state=none`: Default, toggle the selection state of the current file. -- visual_mode: Enter visual mode (selection mode). - - - `--unset`: Enter visual mode (unset mode). - - select_all - `--state=true`: Select all files. - `--state=false`: Deselect all files. - `--state=none`: Default, toggle the selection state of all files. +- visual_mode: Enter visual mode (selection mode). + + - `--unset`: Enter visual mode (unset mode). + ### Operation - open: Open the selected files. diff --git a/config/src/keymap/key.rs b/config/src/keymap/key.rs index ad22fec2..a5d71d07 100644 --- a/config/src/keymap/key.rs +++ b/config/src/keymap/key.rs @@ -47,7 +47,7 @@ impl TryFrom for Key { } let mut key = Self::default(); - if !s.starts_with("<") || !s.ends_with(">") { + if !s.starts_with('<') || !s.ends_with('>') { let c = s.chars().next().unwrap(); key.code = KeyCode::Char(c); key.shift = c.is_ascii_uppercase(); diff --git a/core/src/files/file.rs b/core/src/files/file.rs index 4a7ff8f8..515993c7 100644 --- a/core/src/files/file.rs +++ b/core/src/files/file.rs @@ -1,4 +1,4 @@ -use std::{fs::Metadata, path::{Path, PathBuf}}; +use std::{borrow::Cow, fs::Metadata, path::{Path, PathBuf}}; use anyhow::Result; use tokio::fs; @@ -43,7 +43,5 @@ impl File { } #[inline] - pub fn name(&self) -> Option { - self.path.file_name().map(|s| s.to_string_lossy().to_string()) - } + pub fn name(&self) -> Option> { self.path.file_name().map(|s| s.to_string_lossy()) } } diff --git a/core/src/manager/folder.rs b/core/src/manager/folder.rs index 5f66f94e..177da4fd 100644 --- a/core/src/manager/folder.rs +++ b/core/src/manager/folder.rs @@ -180,12 +180,7 @@ impl Folder { } #[inline] - pub fn has_selected(&self) -> bool { self.files.iter().any(|(_, item)| item.is_selected) } - - pub fn selected(&self) -> Option> { - let v = self.files.iter().filter(|(_, f)| f.is_selected).map(|(_, f)| f).collect::>(); - if v.is_empty() { None } else { Some(v) } - } + pub fn has_selected(&self) -> bool { self.files.iter().any(|(_, f)| f.is_selected) } pub fn rect_current(&self, path: &Path) -> Option { let pos = self.position(path)? - self.offset; diff --git a/core/src/manager/manager.rs b/core/src/manager/manager.rs index 08e0610e..2ac229de 100644 --- a/core/src/manager/manager.rs +++ b/core/src/manager/manager.rs @@ -98,8 +98,9 @@ impl Manager { } tokio::spawn(async move { - let result = - emit!(Input(InputOpt::top(format!("There are {tasks} tasks running, sure to quit? (y/N)")))); + let result = emit!(Input(InputOpt::top(format!( + "There are {tasks} tasks running, sure to quit? (y/N)" + )))); if let Ok(choice) = result.await { if choice.to_lowercase() == "y" { @@ -199,7 +200,7 @@ impl Manager { } pub fn rename(&self) -> bool { - if self.current().has_selected() { + if self.in_selecting() { return self.bulk_rename(); } @@ -360,8 +361,26 @@ impl Manager { #[inline] pub fn hovered(&self) -> Option<&File> { self.tabs.active().current.hovered.as_ref() } - #[inline] pub fn selected(&self) -> Vec<&File> { - self.current().selected().or_else(|| self.hovered().map(|h| vec![h])).unwrap_or_default() + let mode = &self.active().mode; + let files = &self.current().files; + + let selected: Vec<_> = if !mode.is_visual() { + files.iter().filter(|(_, f)| f.is_selected).map(|(_, f)| f).collect() + } else { + files + .iter() + .enumerate() + .filter(|(i, (_, f))| mode.pending(*i, f.is_selected)) + .map(|(_, (_, f))| f) + .collect() + }; + + if selected.is_empty() { self.hovered().map(|h| vec![h]).unwrap_or_default() } else { selected } + } + + #[inline] + pub fn in_selecting(&self) -> bool { + self.active().mode.is_visual() || self.current().has_selected() } } diff --git a/core/src/manager/mode.rs b/core/src/manager/mode.rs index eded7dae..a5145306 100644 --- a/core/src/manager/mode.rs +++ b/core/src/manager/mode.rs @@ -1,13 +1,13 @@ -use std::fmt::Display; +use std::{collections::BTreeSet, fmt::Display}; use config::theme::{self, ColorGroup}; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub enum Mode { #[default] Normal, - Select(usize), - Unset(usize), + Select(usize, BTreeSet), + Unset(usize, BTreeSet), } impl Mode { @@ -15,27 +15,50 @@ impl Mode { pub fn color<'a>(&self, group: &'a ColorGroup) -> &'a theme::Color { match *self { Mode::Normal => &group.normal, - Mode::Select(_) => &group.select, - Mode::Unset(_) => &group.unset, + Mode::Select(..) => &group.select, + Mode::Unset(..) => &group.unset, } } #[inline] - pub fn start(&self) -> Option { + pub fn visual(&self) -> Option<(usize, &BTreeSet)> { match self { Mode::Normal => None, - Mode::Select(n) => Some(*n), - Mode::Unset(n) => Some(*n), + Mode::Select(start, indices) => Some((*start, indices)), + Mode::Unset(start, indices) => Some((*start, indices)), } } + + #[inline] + pub fn visual_mut(&mut self) -> Option<(usize, &mut BTreeSet)> { + match self { + Mode::Normal => None, + Mode::Select(start, indices) => Some((*start, indices)), + Mode::Unset(start, indices) => Some((*start, indices)), + } + } + + #[inline] + pub fn pending(&self, idx: usize, state: bool) -> bool { + match self { + Mode::Normal => state, + Mode::Select(_, indices) => state || indices.contains(&idx), + Mode::Unset(_, indices) => state && !indices.contains(&idx), + } + } +} + +impl Mode { + #[inline] + pub fn is_visual(&self) -> bool { matches!(self, Mode::Select(..) | Mode::Unset(..)) } } impl Display for Mode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { Mode::Normal => write!(f, "NORMAL"), - Mode::Select(_) => write!(f, "SELECT"), - Mode::Unset(_) => write!(f, "UN-SET"), + Mode::Select(..) => write!(f, "SELECT"), + Mode::Unset(..) => write!(f, "UN-SET"), } } } diff --git a/core/src/manager/tab.rs b/core/src/manager/tab.rs index c858cfd4..c7e1135e 100644 --- a/core/src/manager/tab.rs +++ b/core/src/manager/tab.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, mem, path::{Path, PathBuf}}; +use std::{collections::{BTreeMap, BTreeSet}, mem, path::{Path, PathBuf}}; use anyhow::{Error, Result}; use shared::Defer; @@ -33,7 +33,12 @@ impl Tab { } pub fn escape(&mut self) -> bool { - if matches!(self.mode, Mode::Select(_) | Mode::Unset(_)) { + if let Some((_, indices)) = self.mode.visual() { + let b = matches!(self.mode, Mode::Select(..)); + for idx in indices.iter() { + self.current.select(Some(*idx), Some(b)); + } + self.mode = Mode::Normal; return true; } @@ -46,7 +51,6 @@ impl Tab { } pub fn arrow(&mut self, step: isize) -> bool { - let before = self.current.cursor(); let ok = if step > 0 { self.current.next(step as usize) } else { @@ -57,15 +61,12 @@ impl Tab { } // Visual selection - if let Some(start) = self.mode.start() { + if let Some((start, items)) = self.mode.visual_mut() { let after = self.current.cursor(); - if (after > before && before < start) || (after < before && before > start) { - for i in before.min(start)..=start.max(before) { - self.current.select(Some(i), Some(false)); - } - } + + items.clear(); for i in start.min(after)..=after.max(start) { - self.current.select(Some(i), Some(true)); + items.insert(i); } } @@ -251,11 +252,9 @@ impl Tab { let idx = self.current.cursor(); if unset { - self.mode = Mode::Unset(idx); - self.current.select(Some(idx), Some(false)); + self.mode = Mode::Unset(idx, BTreeSet::from([idx])); } else { - self.mode = Mode::Select(idx); - self.current.select(Some(idx), Some(true)); + self.mode = Mode::Select(idx, BTreeSet::from([idx])); }; true }