feat: open_with command, and select interface

This commit is contained in:
sxyazi 2023-07-18 22:23:27 +08:00
parent 521c7271b6
commit 97f5f9491d
No known key found for this signature in database
25 changed files with 345 additions and 125 deletions

View file

@ -67,6 +67,7 @@ keymap = [
[tasks]
keymap = [
{ on = [ "<C-q>" ], exec = "close" },
{ on = [ "<Esc>" ], exec = "close" },
{ on = [ "w" ], exec = "close" },
@ -76,6 +77,20 @@ keymap = [
{ on = [ "x" ], exec = "cancel" },
]
[select]
keymap = [
{ on = [ "<C-q>" ], exec = "close" },
{ on = [ "<Esc>" ], exec = "close" },
{ on = [ "<Enter>" ], 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" },

View file

@ -14,6 +14,7 @@ pub struct Single {
pub struct Keymap {
pub manager: Vec<Single>,
pub tasks: Vec<Single>,
pub select: Vec<Single>,
pub input: Vec<Single>,
}
@ -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,
})
}

View file

@ -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<Vec<&Opener>> {
pub fn openers(&self, path: impl AsRef<Path>, mime: impl AsRef<str>) -> Option<Vec<&Opener>> {
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<Path>, impl AsRef<str>)],
) -> Vec<&Opener> {
let grouped = targets.into_iter().filter_map(|(p, m)| self.openers(p, m)).collect::<Vec<_>>();
let flat = grouped.iter().flatten().cloned().collect::<BTreeSet<_>>();
flat.into_iter().filter(|o| grouped.iter().all(|g| g.contains(o))).collect()

View file

@ -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<str>) -> bool { self.inner.matches(str.as_ref()) }
pub fn match_path(&self, path: &Path, is_folder: Option<bool>) -> bool {
is_folder.map_or(true, |f| f == self.is_folder) && self.inner.matches_path(path)
pub fn match_path(&self, path: impl AsRef<Path>, is_folder: Option<bool>) -> bool {
is_folder.map_or(true, |f| f == self.is_folder) && self.inner.matches_path(path.as_ref())
}
}

View file

@ -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<Sender<Event>> = None;
@ -25,10 +26,11 @@ pub enum Event {
Preview(PathBuf, PreviewData),
// Input
Select(SelectOpt, oneshot::Sender<Result<usize>>),
Input(InputOpt, oneshot::Sender<Result<String>>),
// Tasks
Open(Vec<(PathBuf, String)>),
Open(Vec<(PathBuf, String)>, Option<Opener>),
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();

View file

@ -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<Result<String>>) {
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,
}
}
}

View file

@ -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<Vec<PathBuf>> {
let v = self
.files
.iter()
.filter(|(_, item)| item.is_selected)
.map(|(path, _)| path.clone())
.collect::<Vec<_>>();
pub fn selected(&self) -> Option<Vec<&File>> {
let v = self.files.iter().filter(|(_, f)| f.is_selected).map(|(_, f)| f).collect::<Vec<_>>();
if v.is_empty() { None } else { Some(v) }
}

View file

@ -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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
let todo = files.iter().filter(|(_, m)| m.is_none()).map(|(p, _)| p).collect::<Vec<_>>();
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::<Vec<_>>();
}
emit!(Open(selected.into_iter().filter_map(|(p, m)| m.map(|m| (p, m))).collect::<Vec<_>>()));
let files = files.into_iter().filter_map(|(p, m)| m.map(|m| (p, m))).collect::<Vec<_>>();
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<PathBuf> {
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 {

View file

@ -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?;

View file

@ -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::*;

5
src/core/position.rs Normal file
View file

@ -0,0 +1,5 @@
pub enum Position {
Top,
Hovered,
Coords(u16, u16),
}

5
src/core/select/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod select;
pub use select::*;
pub const SELECT_PADDING: u16 = 2;

108
src/core/select/select.rs Normal file
View file

@ -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<String>,
position: (u16, u16),
offset: usize,
cursor: usize,
callback: Option<Sender<Result<usize>>>,
pub visible: bool,
}
pub struct SelectOpt {
pub title: String,
pub items: Vec<String>,
pub position: Position,
}
impl Select {
pub fn show(&mut self, opt: SelectOpt, tx: Sender<Result<usize>>) {
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,
}
}
}

View file

@ -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<String>,
pub args: Vec<OsString>,
pub block: bool,
pub cancel: oneshot::Sender<()>,
}

View file

@ -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<OsStr>]) {
let args: Vec<OsString> = 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::<Vec<_>>();
.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, {

View file

@ -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<Path>, impl AsRef<str>)]) -> 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<OsStr>]) -> 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<PathBuf>, 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;

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, 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::<Vec<_>>());
} else {
tasks.file_open(&targets);
}
}
Event::Progress(percent, left) => {
self.cx.tasks.progress = (percent, left);
tasks.progress = (percent, left);
emit!(Render);
}

View file

@ -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,
}
}
}

View file

@ -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

View file

@ -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));

View file

@ -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))| {

View file

@ -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;
}

View file

@ -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::*;

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};
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());

42
src/ui/select.rs Normal file
View file

@ -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::<Vec<_>>();
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);
}
}