From 97f5f9491dac2b1402ebb30a9c2c8c08e9754eb2 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Tue, 18 Jul 2023 22:23:27 +0800 Subject: [PATCH] feat: `open_with` command, and `select` interface --- config/keymap.toml | 17 +++++- src/config/keymap/keymap.rs | 3 + src/config/open/open.rs | 11 ++-- src/config/pattern.rs | 6 +- src/core/event.rs | 14 +++-- src/core/input/input.rs | 44 +++------------ src/core/manager/folder.rs | 10 +--- src/core/manager/manager.rs | 51 +++++++++++------ src/core/manager/tab.rs | 4 +- src/core/mod.rs | 3 + src/core/position.rs | 5 ++ src/core/select/mod.rs | 5 ++ src/core/select/select.rs | 108 ++++++++++++++++++++++++++++++++++++ src/core/tasks/process.rs | 4 +- src/core/tasks/scheduler.rs | 17 +++--- src/core/tasks/tasks.rs | 36 ++++++------ src/ui/app.rs | 33 +++++------ src/ui/context.rs | 17 +++++- src/ui/dispatcher.rs | 24 +++++++- src/ui/input.rs | 2 +- src/ui/manager/folder.rs | 4 +- src/ui/manager/preview.rs | 2 +- src/ui/mod.rs | 2 + src/ui/root.rs | 6 +- src/ui/select.rs | 42 ++++++++++++++ 25 files changed, 345 insertions(+), 125 deletions(-) create mode 100644 src/core/position.rs create mode 100644 src/core/select/mod.rs create mode 100644 src/core/select/select.rs create mode 100644 src/ui/select.rs diff --git a/config/keymap.toml b/config/keymap.toml index ae976740..a96eeb17 100644 --- a/config/keymap.toml +++ b/config/keymap.toml @@ -67,6 +67,7 @@ keymap = [ [tasks] keymap = [ + { on = [ "" ], exec = "close" }, { on = [ "" ], exec = "close" }, { on = [ "w" ], exec = "close" }, @@ -76,6 +77,20 @@ keymap = [ { on = [ "x" ], exec = "cancel" }, ] +[select] + +keymap = [ + { on = [ "" ], exec = "close" }, + { on = [ "" ], exec = "close" }, + { on = [ "" ], exec = "close --submit" }, + + { on = [ "j" ], exec = "arrow 1" }, + { on = [ "k" ], exec = "arrow -1" }, + + { on = [ "J" ], exec = "arrow 5" }, + { on = [ "K" ], exec = "arrow -5" }, +] + [input] keymap = [ @@ -87,7 +102,7 @@ keymap = [ # Mode { on = [ "i" ], exec = "insert" }, { on = [ "a" ], exec = "insert --append" }, - { on = [ "v" ],exec = "visual" }, + { on = [ "v" ], exec = "visual" }, { on = [ "h" ], exec = "move -1" }, { on = [ "l" ], exec = "move 1" }, diff --git a/src/config/keymap/keymap.rs b/src/config/keymap/keymap.rs index b32002ff..9d8a4757 100644 --- a/src/config/keymap/keymap.rs +++ b/src/config/keymap/keymap.rs @@ -14,6 +14,7 @@ pub struct Single { pub struct Keymap { pub manager: Vec, pub tasks: Vec, + pub select: Vec, pub input: Vec, } @@ -26,6 +27,7 @@ impl<'de> Deserialize<'de> for Keymap { struct Shadow { manager: Inner, tasks: Inner, + select: Inner, input: Inner, } #[derive(Deserialize)] @@ -37,6 +39,7 @@ impl<'de> Deserialize<'de> for Keymap { Ok(Self { manager: shadow.manager.keymap, tasks: shadow.tasks.keymap, + select: shadow.select.keymap, input: shadow.input.keymap, }) } diff --git a/src/config/open/open.rs b/src/config/open/open.rs index b97c97e5..1bb107db 100644 --- a/src/config/open/open.rs +++ b/src/config/open/open.rs @@ -22,10 +22,10 @@ struct OpenRule { impl Open { pub fn new() -> Self { toml::from_str(&MERGED_YAZI).unwrap() } - pub fn openers(&self, path: &Path, mime: &str) -> Option> { + pub fn openers(&self, path: impl AsRef, mime: impl AsRef) -> Option> { self.rules.iter().find_map(|rule| { - if rule.name.as_ref().map_or(false, |e| e.match_path(path, Some(false))) - || rule.mime.as_ref().map_or(false, |m| m.matches(mime)) + if rule.name.as_ref().map_or(false, |e| e.match_path(&path, Some(false))) + || rule.mime.as_ref().map_or(false, |m| m.matches(&mime)) { self.openers.get(&rule.use_).map(|v| v.iter().collect()) } else { @@ -34,7 +34,10 @@ impl Open { }) } - pub fn common_openers(&self, targets: Vec<(&Path, &str)>) -> Vec<&Opener> { + pub fn common_openers<'a>( + &self, + targets: &[(impl AsRef, impl AsRef)], + ) -> Vec<&Opener> { let grouped = targets.into_iter().filter_map(|(p, m)| self.openers(p, m)).collect::>(); let flat = grouped.iter().flatten().cloned().collect::>(); flat.into_iter().filter(|o| grouped.iter().all(|g| g.contains(o))).collect() diff --git a/src/config/pattern.rs b/src/config/pattern.rs index 49d2871a..b2875ba3 100644 --- a/src/config/pattern.rs +++ b/src/config/pattern.rs @@ -10,10 +10,10 @@ pub struct Pattern { } impl Pattern { - pub fn matches(&self, str: &str) -> bool { self.inner.matches(str) } + pub fn matches(&self, str: impl AsRef) -> bool { self.inner.matches(str.as_ref()) } - pub fn match_path(&self, path: &Path, is_folder: Option) -> bool { - is_folder.map_or(true, |f| f == self.is_folder) && self.inner.matches_path(path) + pub fn match_path(&self, path: impl AsRef, is_folder: Option) -> bool { + is_folder.map_or(true, |f| f == self.is_folder) && self.inner.matches_path(path.as_ref()) } } diff --git a/src/core/event.rs b/src/core/event.rs index 58693cc4..a629c03a 100644 --- a/src/core/event.rs +++ b/src/core/event.rs @@ -4,7 +4,8 @@ use anyhow::Result; use crossterm::event::KeyEvent; use tokio::sync::{mpsc::Sender, oneshot}; -use super::{files::FilesOp, input::InputOpt, manager::PreviewData}; +use super::{files::FilesOp, input::InputOpt, manager::PreviewData, select::SelectOpt}; +use crate::config::open::Opener; static mut TX: Option> = None; @@ -25,10 +26,11 @@ pub enum Event { Preview(PathBuf, PreviewData), // Input + Select(SelectOpt, oneshot::Sender>), Input(InputOpt, oneshot::Sender>), // Tasks - Open(Vec<(PathBuf, String)>), + Open(Vec<(PathBuf, String)>, Option), Progress(u8, u32), } @@ -87,13 +89,17 @@ macro_rules! emit { $crate::core::Event::Preview($path, $data).emit(); }; + (Select($opt:expr)) => {{ + let (tx, rx) = tokio::sync::oneshot::channel(); + $crate::core::Event::Select($opt, tx).wait(rx) + }}; (Input($opt:expr)) => {{ let (tx, rx) = tokio::sync::oneshot::channel(); $crate::core::Event::Input($opt, tx).wait(rx) }}; - (Open($files:expr)) => { - $crate::core::Event::Open($files).emit(); + (Open($targets:expr, $opener:expr)) => { + $crate::core::Event::Open($targets, $opener).emit(); }; (Progress($percent:expr, $tasks:expr)) => { $crate::core::Event::Progress($percent, $tasks).emit(); diff --git a/src/core/input/input.rs b/src/core/input/input.rs index 8ca2f43d..90ff1c09 100644 --- a/src/core/input/input.rs +++ b/src/core/input/input.rs @@ -3,8 +3,9 @@ use ratatui::layout::Rect; use tokio::sync::oneshot::Sender; use unicode_width::UnicodeWidthStr; -use crate::misc::{tty_size, CharKind}; +use crate::{core::Position, misc::CharKind}; +#[derive(Default)] pub struct Input { title: String, value: String, @@ -24,13 +25,7 @@ pub struct Input { pub struct InputOpt { pub title: String, pub value: String, - pub position: InputPos, -} - -pub enum InputPos { - Top, - Hovered, - Coords(u16, u16), + pub position: Position, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -50,14 +45,15 @@ pub enum InputOp { impl Input { pub fn show(&mut self, opt: InputOpt, tx: Sender>) { + self.close(false); + self.title = opt.title; self.value = opt.value; self.position = match opt.position { - InputPos::Coords(x, y) => (x, y), + Position::Coords(x, y) => (x, y), _ => unimplemented!(), }; - self.mode = InputMode::Insert; self.cursor = self.count(); self.offset = self.value.width().saturating_sub(50); self.callback = Some(tx); @@ -65,10 +61,12 @@ impl Input { } pub fn close(&mut self, submit: bool) -> bool { - self.visible = false; if let Some(cb) = self.callback.take() { let _ = cb.send(if submit { Ok(self.value.clone()) } else { Err(anyhow!("canceled")) }); } + + self.mode = InputMode::Insert; + self.visible = false; true } @@ -284,11 +282,6 @@ impl Input { (area.x + width + 1, area.y + 1) } - #[inline] - pub fn top_position() -> InputPos { - InputPos::Coords((tty_size().ws_col / 2).saturating_sub(25), 2) - } - #[inline] fn count(&self) -> usize { self.value.chars().count() } @@ -302,22 +295,3 @@ impl Input { .or_else(|| if n == self.count() { Some(self.value.len()) } else { None }) } } - -impl Default for Input { - fn default() -> Self { - Self { - title: "".to_string(), - value: "".to_string(), - position: Default::default(), - - op: Default::default(), - mode: Default::default(), - cursor: 0, - offset: 0, - range: None, - - visible: false, - callback: None, - } - } -} diff --git a/src/core/manager/folder.rs b/src/core/manager/folder.rs index 83465f7f..25995ae9 100644 --- a/src/core/manager/folder.rs +++ b/src/core/manager/folder.rs @@ -183,14 +183,8 @@ 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(|(_, item)| item.is_selected) - .map(|(path, _)| path.clone()) - .collect::>(); - + 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) } } diff --git a/src/core/manager/manager.rs b/src/core/manager/manager.rs index 49d95b7f..8a2d2522 100644 --- a/src/core/manager/manager.rs +++ b/src/core/manager/manager.rs @@ -3,7 +3,7 @@ use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, mem, path::PathBu use tokio::fs; use super::{PreviewData, Tab, Tabs, Watcher}; -use crate::{core::{external, files::{File, FilesOp}, input::{InputOpt, InputPos}, manager::Folder, tasks::Tasks}, emit}; +use crate::{config::OPEN, core::{external, files::{File, FilesOp}, input::InputOpt, manager::Folder, select::SelectOpt, tasks::Tasks, Position}, emit}; pub struct Manager { tabs: Tabs, @@ -71,9 +71,10 @@ impl Manager { } pub fn yank(&mut self, cut: bool) -> bool { + let selected = self.selected().into_iter().map(|f| f.path()).collect::>(); self.yanked.0 = cut; self.yanked.1.clear(); - self.yanked.1.extend(self.selected()); + self.yanked.1.extend(selected); false } @@ -91,7 +92,7 @@ impl Manager { let result = emit!(Input(InputOpt { title: format!("There are {} tasks running, sure to quit? (y/N)", tasks), value: "".to_string(), - position: InputPos::Top, + position: Position::Top, })) .await; @@ -112,23 +113,21 @@ impl Manager { } pub fn open(&mut self, select: bool) -> bool { - let mut selected = self + let mut files = self .selected() .into_iter() - .map(|p| { - let mime = self.mimetype.get(&p).cloned(); - (p, mime) - }) + .filter(|f| f.meta.is_file()) + .map(|f| (f.path(), self.mimetype.get(&f.path).cloned())) .collect::>(); - if selected.is_empty() { + if files.is_empty() { return false; } tokio::spawn(async move { - let todo = selected.iter().filter(|(_, m)| m.is_none()).map(|(p, _)| p).collect::>(); + let todo = files.iter().filter(|(_, m)| m.is_none()).map(|(p, _)| p).collect::>(); if let Ok(mut mimes) = external::file(&todo).await { - selected = selected + files = files .into_iter() .map(|(p, m)| { let mime = m.or_else(|| mimes.remove(&p)); @@ -136,7 +135,27 @@ impl Manager { }) .collect::>(); } - emit!(Open(selected.into_iter().filter_map(|(p, m)| m.map(|m| (p, m))).collect::>())); + + let files = files.into_iter().filter_map(|(p, m)| m.map(|m| (p, m))).collect::>(); + if !select { + emit!(Open(files, None)); + return; + } + + let openers = OPEN.common_openers(&files); + if openers.is_empty() { + return; + } + + let result = emit!(Select(SelectOpt { + title: "Open with:".to_string(), + items: openers.iter().map(|o| o.cmd.clone()).collect(), + position: Position::Hovered, + })) + .await; + if let Ok(choice) = result { + emit!(Open(files, Some(openers[choice].clone()))); + } }); false } @@ -147,7 +166,7 @@ impl Manager { let result = emit!(Input(InputOpt { title: "Create:".to_string(), value: "".to_string(), - position: InputPos::Top, + position: Position::Top, })) .await; @@ -179,7 +198,7 @@ impl Manager { let result = emit!(Input(InputOpt { title: "Rename:".to_string(), value: hovered.file_name().unwrap().to_string_lossy().to_string(), - position: InputPos::Hovered, + position: Position::Hovered, })) .await; @@ -193,8 +212,8 @@ impl Manager { fn bulk_rename(&self) -> bool { false } - pub fn selected(&self) -> Vec { - self.current().selected().or_else(|| self.hovered().map(|h| vec![h.path()])).unwrap_or_default() + pub fn selected(&self) -> Vec<&File> { + self.current().selected().or_else(|| self.hovered().map(|h| vec![h])).unwrap_or_default() } pub fn update_read(&mut self, op: FilesOp) -> bool { diff --git a/src/core/manager/tab.rs b/src/core/manager/tab.rs index ec8d6d78..963b85ec 100644 --- a/src/core/manager/tab.rs +++ b/src/core/manager/tab.rs @@ -4,7 +4,7 @@ use anyhow::{Error, Result}; use tokio::task::JoinHandle; use super::{Folder, Mode, Preview}; -use crate::{core::{external::{self, FzfOpt, ZoxideOpt}, files::{File, Files, FilesOp}, input::{InputOpt, InputPos}, Event, BLOCKER}, emit, misc::Defer}; +use crate::{core::{external::{self, FzfOpt, ZoxideOpt}, files::{File, Files, FilesOp}, input::InputOpt, Event, Position, BLOCKER}, emit, misc::Defer}; pub struct Tab { pub(super) current: Folder, @@ -186,7 +186,7 @@ impl Tab { let subject = emit!(Input(InputOpt { title: "Search:".to_string(), value: "".to_string(), - position: InputPos::Top, + position: Position::Top, })) .await?; diff --git a/src/core/mod.rs b/src/core/mod.rs index 970de191..1417ecd2 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -5,7 +5,10 @@ pub mod external; pub mod files; pub mod input; pub mod manager; +pub mod position; +pub mod select; pub mod tasks; pub use blocker::*; pub use event::*; +pub use position::*; diff --git a/src/core/position.rs b/src/core/position.rs new file mode 100644 index 00000000..813597be --- /dev/null +++ b/src/core/position.rs @@ -0,0 +1,5 @@ +pub enum Position { + Top, + Hovered, + Coords(u16, u16), +} diff --git a/src/core/select/mod.rs b/src/core/select/mod.rs new file mode 100644 index 00000000..93af1071 --- /dev/null +++ b/src/core/select/mod.rs @@ -0,0 +1,5 @@ +mod select; + +pub use select::*; + +pub const SELECT_PADDING: u16 = 2; diff --git a/src/core/select/select.rs b/src/core/select/select.rs new file mode 100644 index 00000000..e0c57dd2 --- /dev/null +++ b/src/core/select/select.rs @@ -0,0 +1,108 @@ +use anyhow::{anyhow, Result}; +use ratatui::prelude::Rect; +use tokio::sync::oneshot::Sender; + +use super::SELECT_PADDING; +use crate::{core::Position, misc::tty_size}; + +#[derive(Default)] +pub struct Select { + title: String, + items: Vec, + position: (u16, u16), + + offset: usize, + cursor: usize, + callback: Option>>, + + pub visible: bool, +} + +pub struct SelectOpt { + pub title: String, + pub items: Vec, + pub position: Position, +} + +impl Select { + pub fn show(&mut self, opt: SelectOpt, tx: Sender>) { + self.close(false); + + self.title = opt.title; + self.items = opt.items; + self.position = match opt.position { + Position::Coords(x, y) => (x, y), + _ => unimplemented!(), + }; + self.callback = Some(tx); + self.visible = true; + } + + pub fn close(&mut self, submit: bool) -> bool { + if let Some(cb) = self.callback.take() { + let _ = cb.send(if submit { Ok(self.cursor) } else { Err(anyhow!("canceled")) }); + } + + self.cursor = 0; + self.offset = 0; + self.visible = false; + true + } + + pub fn next(&mut self, step: usize) -> bool { + let len = self.items.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 + } + + pub 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 + } + + #[inline] + pub fn window(&self) -> &[String] { + let end = (self.offset + self.limit()).min(self.items.len()); + &self.items[self.offset..end] + } + + #[inline] + pub fn limit(&self) -> usize { + self.items.len().min(tty_size().ws_row.saturating_sub(SELECT_PADDING).min(5) as usize) + } +} + +impl Select { + #[inline] + pub fn title(&self) -> String { self.title.clone() } + + #[inline] + pub fn rel_cursor(&self) -> usize { self.cursor - self.offset } + + #[inline] + pub fn area(&self) -> Rect { + Rect { + x: self.position.0, + y: self.position.1 + 2, + width: 50, + height: self.limit() as u16 + SELECT_PADDING, + } + } +} diff --git a/src/core/tasks/process.rs b/src/core/tasks/process.rs index 3f4d3efa..5e3cfb1e 100644 --- a/src/core/tasks/process.rs +++ b/src/core/tasks/process.rs @@ -1,4 +1,4 @@ -use std::process::Stdio; +use std::{ffi::OsString, process::Stdio}; use anyhow::Result; use tokio::{process::Command, select, sync::{mpsc, oneshot}}; @@ -23,7 +23,7 @@ pub(super) enum ProcessOp { pub(super) struct ProcessOpOpen { pub id: usize, pub cmd: String, - pub args: Vec, + pub args: Vec, pub block: bool, pub cancel: oneshot::Sender<()>, } diff --git a/src/core/tasks/scheduler.rs b/src/core/tasks/scheduler.rs index 371ec282..d0ce5655 100644 --- a/src/core/tasks/scheduler.rs +++ b/src/core/tasks/scheduler.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, path::PathBuf, sync::Arc, time::Duration}; +use std::{collections::BTreeMap, ffi::{OsStr, OsString}, path::PathBuf, sync::Arc, time::Duration}; use async_channel::{Receiver, Sender}; use futures::{future::BoxFuture, FutureExt}; @@ -334,24 +334,25 @@ impl Scheduler { }); } - pub(super) fn process_open(&self, opener: &Opener, args: &[String]) { - let args = opener + pub(super) fn process_open(&self, opener: &Opener, args: &[impl AsRef]) { + let args: Vec = opener .args .iter() .map_while(|a| { if !a.starts_with('$') { - return Some(vec![a.clone()]); + return Some(vec![a.into()]); } if a == "$*" { - return Some(args.to_vec()); + return Some(args.iter().map(Into::into).collect()); } - a[1..].parse().ok().and_then(|n: usize| args.get(n)).map(|a| vec![a.clone()]) + a[1..].parse().ok().and_then(|n: usize| args.get(n)).map(|a| vec![a.into()]) }) .flatten() - .collect::>(); + .collect(); let mut running = self.running.write(); - let id = running.add(format!("Exec `{} {}`", opener.cmd, args.join(" "))); + let name = format!("Exec `{} {}`", opener.cmd, args.join(" ".as_ref()).to_string_lossy()); + let id = running.add(name); let (cancel_tx, mut cancel_rx) = oneshot::channel(); running.hooks.insert(id, { diff --git a/src/core/tasks/tasks.rs b/src/core/tasks/tasks.rs index 6c5d2c79..6adef091 100644 --- a/src/core/tasks/tasks.rs +++ b/src/core/tasks/tasks.rs @@ -1,9 +1,9 @@ -use std::{collections::{BTreeMap, HashMap, HashSet}, path::PathBuf, sync::Arc}; +use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, path::{Path, PathBuf}, sync::Arc}; use tracing::trace; use super::{Scheduler, TASKS_PADDING, TASKS_PERCENT}; -use crate::{config::OPEN, core::{files::File, input::{InputOpt, InputPos}}, emit, misc::{tty_size, MimeKind}}; +use crate::{config::{open::Opener, OPEN}, core::{files::File, input::InputOpt, Position}, emit, misc::{tty_size, MimeKind}}; #[derive(Clone, Debug)] pub struct Task { @@ -95,28 +95,30 @@ impl Tasks { id.map(|id| self.scheduler.cancel(id)).unwrap_or(false) } - pub fn file_open(&self, targets: Vec<(PathBuf, String)>) -> bool { + pub fn file_open(&self, targets: &[(impl AsRef, impl AsRef)]) -> bool { let mut openers = BTreeMap::new(); - for target in targets { - if let Some(opener) = OPEN.openers(&target.0, &target.1).and_then(|o| o.first().cloned()) { - openers - .entry(opener) - .or_insert_with(|| vec![]) - .push(target.0.to_string_lossy().into_owned()); + for (path, mime) in targets { + if let Some(opener) = OPEN.openers(path, mime).and_then(|o| o.first().cloned()) { + openers.entry(opener).or_insert_with(|| vec![]).push(path.as_ref()); } } for (opener, args) in openers { - if opener.spread { - self.scheduler.process_open(&opener, &args); - continue; - } - for target in args { - self.scheduler.process_open(&opener, &[target]); - } + self.file_open_with(opener, &args); } false } + pub fn file_open_with(&self, opener: &Opener, args: &[impl AsRef]) -> bool { + if opener.spread { + self.scheduler.process_open(&opener, args); + return false; + } + for target in args { + self.scheduler.process_open(&opener, &[target]); + } + return false; + } + pub fn file_cut(&self, src: &HashSet, dest: PathBuf, force: bool) -> bool { for p in src { let to = dest.join(p.file_name().unwrap()); @@ -153,7 +155,7 @@ impl Tasks { let result = emit!(Input(InputOpt { title: "Are you sure delete these files? (y/N)".to_string(), value: "".to_string(), - position: InputPos::Hovered, + position: Position::Hovered, })) .await; diff --git a/src/ui/app.rs b/src/ui/app.rs index 33505f7c..01bfe077 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -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, input::{Input, InputPos}, Event}, emit}; +use crate::{config::keymap::Key, core::{files::FilesOp, Event}, emit}; pub struct App { cx: Ctx, @@ -73,6 +73,7 @@ impl App { 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; @@ -93,11 +94,11 @@ impl App { Event::Pages(page) => { if manager.current().page == page { let targets = self.cx.manager.current().paginate().into_iter().map(|(_, f)| f).collect(); - self.cx.tasks.precache_mime(targets, &self.cx.manager.mimetype); + tasks.precache_mime(targets, &self.cx.manager.mimetype); } } Event::Mimetype(mimes) => { - if manager.update_mimetype(mimes, &self.cx.tasks) { + if manager.update_mimetype(mimes, tasks) { emit!(Render); } } @@ -111,26 +112,26 @@ impl App { emit!(Render); } + Event::Select(mut opt, tx) => { + opt.position = self.cx.position(opt.position); + self.cx.select.show(opt, tx); + emit!(Render); + } Event::Input(mut opt, tx) => { - opt.position = match opt.position { - InputPos::Top => Input::top_position(), - InputPos::Hovered => manager - .hovered() - .and_then(|h| manager.current().rect_current(&h.path)) - .map(|r| InputPos::Coords(r.x, r.y)) - .unwrap_or_else(|| Input::top_position()), - p @ InputPos::Coords(..) => p, - }; - + opt.position = self.cx.position(opt.position); self.cx.input.show(opt, tx); emit!(Render); } - Event::Open(files) => { - self.cx.tasks.file_open(files); + Event::Open(targets, opener) => { + if let Some(opener) = opener { + tasks.file_open_with(&opener, &targets.iter().map(|(f, _)| f).collect::>()); + } else { + tasks.file_open(&targets); + } } Event::Progress(percent, left) => { - self.cx.tasks.progress = (percent, left); + tasks.progress = (percent, left); emit!(Render); } diff --git a/src/ui/context.rs b/src/ui/context.rs index e2bee6c9..1e44f24c 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -1,9 +1,10 @@ -use crate::core::{input::Input, manager::Manager, tasks::Tasks}; +use crate::{core::{input::Input, manager::Manager, select::Select, tasks::Tasks, Position}, misc::tty_size}; pub struct Ctx { pub cursor: Option<(u16, u16)>, pub manager: Manager, + pub select: Select, pub input: Input, pub tasks: Tasks, } @@ -14,8 +15,22 @@ impl Ctx { cursor: None, manager: Manager::new(), + select: Select::default(), input: Input::default(), tasks: Tasks::start(), } } + + pub fn position(&self, pos: Position) -> Position { + match pos { + Position::Top => Position::Coords((tty_size().ws_col / 2).saturating_sub(25), 2), + Position::Hovered => self + .manager + .hovered() + .and_then(|h| self.manager.current().rect_current(&h.path)) + .map(|r| Position::Coords(r.x, r.y)) + .unwrap_or_else(|| self.position(Position::Top)), + p @ Position::Coords(..) => p, + } + } } diff --git a/src/ui/dispatcher.rs b/src/ui/dispatcher.rs index 2b79a77d..af38da4a 100644 --- a/src/ui/dispatcher.rs +++ b/src/ui/dispatcher.rs @@ -8,6 +8,8 @@ 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 @@ -17,7 +19,7 @@ impl Executor { let mut render = false; let mut matched = false; - let keymap = [&KEYMAP.manager, &KEYMAP.tasks, &KEYMAP.input][layer]; + let keymap = [&KEYMAP.manager, &KEYMAP.tasks, &KEYMAP.select, &KEYMAP.input][layer]; for Single { on, exec } in keymap { if on.len() < 1 || on[0] != key { @@ -31,12 +33,14 @@ impl Executor { } else if layer == 1 { render = Self::tasks(cx, e) || render; } else if layer == 2 { + render = Self::select(cx, e) || render; + } else if layer == 3 { render = Self::input(cx, Some(e), key.code) || render; } } } - if layer == 2 && !matched { + if layer == 3 && !matched { render = Self::input(cx, None, key.code); } render @@ -84,7 +88,8 @@ impl Executor { } } "remove" => { - cx.tasks.file_remove(cx.manager.selected(), exec.named.contains_key("permanently")) + let targets = cx.manager.selected().into_iter().map(|p| p.path()).collect(); + cx.tasks.file_remove(targets, exec.named.contains_key("permanently")) } "create" => cx.manager.create(), "rename" => cx.manager.rename(), @@ -148,6 +153,19 @@ impl Executor { } } + fn select(cx: &mut Ctx, exec: &Exec) -> bool { + match exec.cmd.as_str() { + "close" => cx.select.close(exec.named.contains_key("submit")), + + "arrow" => { + let step: isize = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); + if step > 0 { cx.select.next(step as usize) } else { cx.select.prev(step.abs() as usize) } + } + + _ => false, + } + } + fn input(cx: &mut Ctx, exec: Option<&Exec>, code: KeyCode) -> bool { let exec = if let Some(e) = exec { e diff --git a/src/ui/input.rs b/src/ui/input.rs index 8b872e78..ceb3c4df 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -21,8 +21,8 @@ impl<'a> Widget for Input<'a> { .block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Blue)) .border_type(BorderType::Rounded) + .border_style(Style::default().fg(Color::Blue)) .title({ let mut line = Line::from(input.title()); line.patch_style(Style::default().fg(Color::White)); diff --git a/src/ui/manager/folder.rs b/src/ui/manager/folder.rs index 74a07ea2..d0ef0123 100644 --- a/src/ui/manager/folder.rs +++ b/src/ui/manager/folder.rs @@ -40,9 +40,9 @@ impl<'a> Folder<'a> { impl<'a> Widget for Folder<'a> { fn render(self, area: Rect, buf: &mut Buffer) { - let page = self.folder.window(); + let window = self.folder.window(); - let items = page + let items = window .iter() .enumerate() .map(|(i, (k, v))| { diff --git a/src/ui/manager/preview.rs b/src/ui/manager/preview.rs index 135918fc..f20eddd1 100644 --- a/src/ui/manager/preview.rs +++ b/src/ui/manager/preview.rs @@ -16,7 +16,7 @@ impl<'a> Preview<'a> { impl<'a> Widget for Preview<'a> { fn render(self, area: Rect, buf: &mut Buffer) { - if self.cx.input.visible || self.cx.tasks.visible { + if self.cx.input.visible || self.cx.select.visible || self.cx.tasks.visible { stdout().write(Kitty::image_hide()).ok(); return; } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9b00c0c1..bfa8ead8 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,6 +6,7 @@ mod input; mod logs; mod manager; mod root; +mod select; mod signals; mod status; mod tasks; @@ -16,5 +17,6 @@ pub use context::*; pub use dispatcher::*; pub use input::*; pub use logs::*; +pub use select::*; pub use signals::*; pub use term::*; diff --git a/src/ui/root.rs b/src/ui/root.rs index 76821fbc..3aa7b2b4 100644 --- a/src/ui/root.rs +++ b/src/ui/root.rs @@ -1,6 +1,6 @@ use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget}; -use super::{header, manager, status, tasks, Ctx, Input}; +use super::{header, manager, status, tasks, Ctx, Input, Select}; pub struct Root<'a> { cx: &'a mut Ctx, @@ -25,6 +25,10 @@ impl<'a> Widget for Root<'a> { tasks::Layout::new(self.cx).render(area, buf); } + if self.cx.select.visible { + Select::new(self.cx).render(area, buf); + } + if self.cx.input.visible { Input::new(self.cx).render(area, buf); self.cx.cursor = Some(self.cx.input.cursor()); diff --git a/src/ui/select.rs b/src/ui/select.rs new file mode 100644 index 00000000..4a328101 --- /dev/null +++ b/src/ui/select.rs @@ -0,0 +1,42 @@ +use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Style}, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}}; + +use super::Ctx; + +pub struct Select<'a> { + cx: &'a Ctx, +} + +impl<'a> Select<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Select<'a> { + fn render(self, _: Rect, buf: &mut Buffer) { + let select = &self.cx.select; + let area = select.area(); + + let items = select + .window() + .iter() + .enumerate() + .map(|(i, v)| { + if i != select.rel_cursor() { + return ListItem::new(format!(" {}", v)); + } + + ListItem::new(format!(" {}", v)).style(Style::default().fg(Color::Magenta)) + }) + .collect::>(); + + Clear.render(area, buf); + List::new(items) + .block( + Block::default() + .title(select.title()) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(Color::Blue)), + ) + .render(area, buf); + } +}