mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: open_with command, and select interface
This commit is contained in:
parent
521c7271b6
commit
97f5f9491d
25 changed files with 345 additions and 125 deletions
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
5
src/core/position.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub enum Position {
|
||||
Top,
|
||||
Hovered,
|
||||
Coords(u16, u16),
|
||||
}
|
||||
5
src/core/select/mod.rs
Normal file
5
src/core/select/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod select;
|
||||
|
||||
pub use select::*;
|
||||
|
||||
pub const SELECT_PADDING: u16 = 2;
|
||||
108
src/core/select/select.rs
Normal file
108
src/core/select/select.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<()>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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))| {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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
42
src/ui/select.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue