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] [tasks]
keymap = [ keymap = [
{ on = [ "<C-q>" ], exec = "close" },
{ on = [ "<Esc>" ], exec = "close" }, { on = [ "<Esc>" ], exec = "close" },
{ on = [ "w" ], exec = "close" }, { on = [ "w" ], exec = "close" },
@ -76,6 +77,20 @@ keymap = [
{ on = [ "x" ], exec = "cancel" }, { 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] [input]
keymap = [ keymap = [
@ -87,7 +102,7 @@ keymap = [
# Mode # Mode
{ on = [ "i" ], exec = "insert" }, { on = [ "i" ], exec = "insert" },
{ on = [ "a" ], exec = "insert --append" }, { on = [ "a" ], exec = "insert --append" },
{ on = [ "v" ],exec = "visual" }, { on = [ "v" ], exec = "visual" },
{ on = [ "h" ], exec = "move -1" }, { on = [ "h" ], exec = "move -1" },
{ on = [ "l" ], exec = "move 1" }, { on = [ "l" ], exec = "move 1" },

View file

@ -14,6 +14,7 @@ pub struct Single {
pub struct Keymap { pub struct Keymap {
pub manager: Vec<Single>, pub manager: Vec<Single>,
pub tasks: Vec<Single>, pub tasks: Vec<Single>,
pub select: Vec<Single>,
pub input: Vec<Single>, pub input: Vec<Single>,
} }
@ -26,6 +27,7 @@ impl<'de> Deserialize<'de> for Keymap {
struct Shadow { struct Shadow {
manager: Inner, manager: Inner,
tasks: Inner, tasks: Inner,
select: Inner,
input: Inner, input: Inner,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -37,6 +39,7 @@ impl<'de> Deserialize<'de> for Keymap {
Ok(Self { Ok(Self {
manager: shadow.manager.keymap, manager: shadow.manager.keymap,
tasks: shadow.tasks.keymap, tasks: shadow.tasks.keymap,
select: shadow.select.keymap,
input: shadow.input.keymap, input: shadow.input.keymap,
}) })
} }

View file

@ -22,10 +22,10 @@ struct OpenRule {
impl Open { impl Open {
pub fn new() -> Self { toml::from_str(&MERGED_YAZI).unwrap() } 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| { self.rules.iter().find_map(|rule| {
if rule.name.as_ref().map_or(false, |e| e.match_path(path, Some(false))) 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)) || rule.mime.as_ref().map_or(false, |m| m.matches(&mime))
{ {
self.openers.get(&rule.use_).map(|v| v.iter().collect()) self.openers.get(&rule.use_).map(|v| v.iter().collect())
} else { } 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 grouped = targets.into_iter().filter_map(|(p, m)| self.openers(p, m)).collect::<Vec<_>>();
let flat = grouped.iter().flatten().cloned().collect::<BTreeSet<_>>(); let flat = grouped.iter().flatten().cloned().collect::<BTreeSet<_>>();
flat.into_iter().filter(|o| grouped.iter().all(|g| g.contains(o))).collect() flat.into_iter().filter(|o| grouped.iter().all(|g| g.contains(o))).collect()

View file

@ -10,10 +10,10 @@ pub struct Pattern {
} }
impl 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 { 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) 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 crossterm::event::KeyEvent;
use tokio::sync::{mpsc::Sender, oneshot}; 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; static mut TX: Option<Sender<Event>> = None;
@ -25,10 +26,11 @@ pub enum Event {
Preview(PathBuf, PreviewData), Preview(PathBuf, PreviewData),
// Input // Input
Select(SelectOpt, oneshot::Sender<Result<usize>>),
Input(InputOpt, oneshot::Sender<Result<String>>), Input(InputOpt, oneshot::Sender<Result<String>>),
// Tasks // Tasks
Open(Vec<(PathBuf, String)>), Open(Vec<(PathBuf, String)>, Option<Opener>),
Progress(u8, u32), Progress(u8, u32),
} }
@ -87,13 +89,17 @@ macro_rules! emit {
$crate::core::Event::Preview($path, $data).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)) => {{ (Input($opt:expr)) => {{
let (tx, rx) = tokio::sync::oneshot::channel(); let (tx, rx) = tokio::sync::oneshot::channel();
$crate::core::Event::Input($opt, tx).wait(rx) $crate::core::Event::Input($opt, tx).wait(rx)
}}; }};
(Open($files:expr)) => { (Open($targets:expr, $opener:expr)) => {
$crate::core::Event::Open($files).emit(); $crate::core::Event::Open($targets, $opener).emit();
}; };
(Progress($percent:expr, $tasks:expr)) => { (Progress($percent:expr, $tasks:expr)) => {
$crate::core::Event::Progress($percent, $tasks).emit(); $crate::core::Event::Progress($percent, $tasks).emit();

View file

@ -3,8 +3,9 @@ use ratatui::layout::Rect;
use tokio::sync::oneshot::Sender; use tokio::sync::oneshot::Sender;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::misc::{tty_size, CharKind}; use crate::{core::Position, misc::CharKind};
#[derive(Default)]
pub struct Input { pub struct Input {
title: String, title: String,
value: String, value: String,
@ -24,13 +25,7 @@ pub struct Input {
pub struct InputOpt { pub struct InputOpt {
pub title: String, pub title: String,
pub value: String, pub value: String,
pub position: InputPos, pub position: Position,
}
pub enum InputPos {
Top,
Hovered,
Coords(u16, u16),
} }
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@ -50,14 +45,15 @@ pub enum InputOp {
impl Input { impl Input {
pub fn show(&mut self, opt: InputOpt, tx: Sender<Result<String>>) { pub fn show(&mut self, opt: InputOpt, tx: Sender<Result<String>>) {
self.close(false);
self.title = opt.title; self.title = opt.title;
self.value = opt.value; self.value = opt.value;
self.position = match opt.position { self.position = match opt.position {
InputPos::Coords(x, y) => (x, y), Position::Coords(x, y) => (x, y),
_ => unimplemented!(), _ => unimplemented!(),
}; };
self.mode = InputMode::Insert;
self.cursor = self.count(); self.cursor = self.count();
self.offset = self.value.width().saturating_sub(50); self.offset = self.value.width().saturating_sub(50);
self.callback = Some(tx); self.callback = Some(tx);
@ -65,10 +61,12 @@ impl Input {
} }
pub fn close(&mut self, submit: bool) -> bool { pub fn close(&mut self, submit: bool) -> bool {
self.visible = false;
if let Some(cb) = self.callback.take() { if let Some(cb) = self.callback.take() {
let _ = cb.send(if submit { Ok(self.value.clone()) } else { Err(anyhow!("canceled")) }); let _ = cb.send(if submit { Ok(self.value.clone()) } else { Err(anyhow!("canceled")) });
} }
self.mode = InputMode::Insert;
self.visible = false;
true true
} }
@ -284,11 +282,6 @@ impl Input {
(area.x + width + 1, area.y + 1) (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] #[inline]
fn count(&self) -> usize { self.value.chars().count() } 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 }) .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] #[inline]
pub fn has_selected(&self) -> bool { self.files.iter().any(|(_, item)| item.is_selected) } pub fn has_selected(&self) -> bool { self.files.iter().any(|(_, item)| item.is_selected) }
pub fn selected(&self) -> Option<Vec<PathBuf>> { pub fn selected(&self) -> Option<Vec<&File>> {
let v = self let v = self.files.iter().filter(|(_, f)| f.is_selected).map(|(_, f)| f).collect::<Vec<_>>();
.files
.iter()
.filter(|(_, item)| item.is_selected)
.map(|(path, _)| path.clone())
.collect::<Vec<_>>();
if v.is_empty() { None } else { Some(v) } 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 tokio::fs;
use super::{PreviewData, Tab, Tabs, Watcher}; 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 { pub struct Manager {
tabs: Tabs, tabs: Tabs,
@ -71,9 +71,10 @@ impl Manager {
} }
pub fn yank(&mut self, cut: bool) -> bool { 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.0 = cut;
self.yanked.1.clear(); self.yanked.1.clear();
self.yanked.1.extend(self.selected()); self.yanked.1.extend(selected);
false false
} }
@ -91,7 +92,7 @@ impl Manager {
let result = emit!(Input(InputOpt { let result = emit!(Input(InputOpt {
title: format!("There are {} tasks running, sure to quit? (y/N)", tasks), title: format!("There are {} tasks running, sure to quit? (y/N)", tasks),
value: "".to_string(), value: "".to_string(),
position: InputPos::Top, position: Position::Top,
})) }))
.await; .await;
@ -112,23 +113,21 @@ impl Manager {
} }
pub fn open(&mut self, select: bool) -> bool { pub fn open(&mut self, select: bool) -> bool {
let mut selected = self let mut files = self
.selected() .selected()
.into_iter() .into_iter()
.map(|p| { .filter(|f| f.meta.is_file())
let mime = self.mimetype.get(&p).cloned(); .map(|f| (f.path(), self.mimetype.get(&f.path).cloned()))
(p, mime)
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if selected.is_empty() { if files.is_empty() {
return false; return false;
} }
tokio::spawn(async move { 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 { if let Ok(mut mimes) = external::file(&todo).await {
selected = selected files = files
.into_iter() .into_iter()
.map(|(p, m)| { .map(|(p, m)| {
let mime = m.or_else(|| mimes.remove(&p)); let mime = m.or_else(|| mimes.remove(&p));
@ -136,7 +135,27 @@ impl Manager {
}) })
.collect::<Vec<_>>(); .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 false
} }
@ -147,7 +166,7 @@ impl Manager {
let result = emit!(Input(InputOpt { let result = emit!(Input(InputOpt {
title: "Create:".to_string(), title: "Create:".to_string(),
value: "".to_string(), value: "".to_string(),
position: InputPos::Top, position: Position::Top,
})) }))
.await; .await;
@ -179,7 +198,7 @@ impl Manager {
let result = emit!(Input(InputOpt { let result = emit!(Input(InputOpt {
title: "Rename:".to_string(), title: "Rename:".to_string(),
value: hovered.file_name().unwrap().to_string_lossy().to_string(), value: hovered.file_name().unwrap().to_string_lossy().to_string(),
position: InputPos::Hovered, position: Position::Hovered,
})) }))
.await; .await;
@ -193,8 +212,8 @@ impl Manager {
fn bulk_rename(&self) -> bool { false } fn bulk_rename(&self) -> bool { false }
pub fn selected(&self) -> Vec<PathBuf> { pub fn selected(&self) -> Vec<&File> {
self.current().selected().or_else(|| self.hovered().map(|h| vec![h.path()])).unwrap_or_default() self.current().selected().or_else(|| self.hovered().map(|h| vec![h])).unwrap_or_default()
} }
pub fn update_read(&mut self, op: FilesOp) -> bool { pub fn update_read(&mut self, op: FilesOp) -> bool {

View file

@ -4,7 +4,7 @@ use anyhow::{Error, Result};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use super::{Folder, Mode, Preview}; 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 struct Tab {
pub(super) current: Folder, pub(super) current: Folder,
@ -186,7 +186,7 @@ impl Tab {
let subject = emit!(Input(InputOpt { let subject = emit!(Input(InputOpt {
title: "Search:".to_string(), title: "Search:".to_string(),
value: "".to_string(), value: "".to_string(),
position: InputPos::Top, position: Position::Top,
})) }))
.await?; .await?;

View file

@ -5,7 +5,10 @@ pub mod external;
pub mod files; pub mod files;
pub mod input; pub mod input;
pub mod manager; pub mod manager;
pub mod position;
pub mod select;
pub mod tasks; pub mod tasks;
pub use blocker::*; pub use blocker::*;
pub use event::*; 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 anyhow::Result;
use tokio::{process::Command, select, sync::{mpsc, oneshot}}; use tokio::{process::Command, select, sync::{mpsc, oneshot}};
@ -23,7 +23,7 @@ pub(super) enum ProcessOp {
pub(super) struct ProcessOpOpen { pub(super) struct ProcessOpOpen {
pub id: usize, pub id: usize,
pub cmd: String, pub cmd: String,
pub args: Vec<String>, pub args: Vec<OsString>,
pub block: bool, pub block: bool,
pub cancel: oneshot::Sender<()>, 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 async_channel::{Receiver, Sender};
use futures::{future::BoxFuture, FutureExt}; use futures::{future::BoxFuture, FutureExt};
@ -334,24 +334,25 @@ impl Scheduler {
}); });
} }
pub(super) fn process_open(&self, opener: &Opener, args: &[String]) { pub(super) fn process_open(&self, opener: &Opener, args: &[impl AsRef<OsStr>]) {
let args = opener let args: Vec<OsString> = opener
.args .args
.iter() .iter()
.map_while(|a| { .map_while(|a| {
if !a.starts_with('$') { if !a.starts_with('$') {
return Some(vec![a.clone()]); return Some(vec![a.into()]);
} }
if a == "$*" { 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() .flatten()
.collect::<Vec<_>>(); .collect();
let mut running = self.running.write(); 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(); let (cancel_tx, mut cancel_rx) = oneshot::channel();
running.hooks.insert(id, { 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 tracing::trace;
use super::{Scheduler, TASKS_PADDING, TASKS_PERCENT}; 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)] #[derive(Clone, Debug)]
pub struct Task { pub struct Task {
@ -95,28 +95,30 @@ impl Tasks {
id.map(|id| self.scheduler.cancel(id)).unwrap_or(false) 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(); let mut openers = BTreeMap::new();
for target in targets { for (path, mime) in targets {
if let Some(opener) = OPEN.openers(&target.0, &target.1).and_then(|o| o.first().cloned()) { if let Some(opener) = OPEN.openers(path, mime).and_then(|o| o.first().cloned()) {
openers openers.entry(opener).or_insert_with(|| vec![]).push(path.as_ref());
.entry(opener)
.or_insert_with(|| vec![])
.push(target.0.to_string_lossy().into_owned());
} }
} }
for (opener, args) in openers { for (opener, args) in openers {
if opener.spread { self.file_open_with(opener, &args);
self.scheduler.process_open(&opener, &args);
continue;
}
for target in args {
self.scheduler.process_open(&opener, &[target]);
}
} }
false 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 { pub fn file_cut(&self, src: &HashSet<PathBuf>, dest: PathBuf, force: bool) -> bool {
for p in src { for p in src {
let to = dest.join(p.file_name().unwrap()); let to = dest.join(p.file_name().unwrap());
@ -153,7 +155,7 @@ impl Tasks {
let result = emit!(Input(InputOpt { let result = emit!(Input(InputOpt {
title: "Are you sure delete these files? (y/N)".to_string(), title: "Are you sure delete these files? (y/N)".to_string(),
value: "".to_string(), value: "".to_string(),
position: InputPos::Hovered, position: Position::Hovered,
})) }))
.await; .await;

View file

@ -3,7 +3,7 @@ use crossterm::event::KeyEvent;
use tokio::sync::oneshot::{self}; use tokio::sync::oneshot::{self};
use super::{root::Root, Ctx, Executor, Logs, Signals, Term}; 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 { pub struct App {
cx: Ctx, cx: Ctx,
@ -73,6 +73,7 @@ impl App {
async fn dispatch_module(&mut self, event: Event) { async fn dispatch_module(&mut self, event: Event) {
let manager = &mut self.cx.manager; let manager = &mut self.cx.manager;
let tasks = &mut self.cx.tasks;
match event { match event {
Event::Cd(path) => { Event::Cd(path) => {
manager.active_mut().cd(path).await; manager.active_mut().cd(path).await;
@ -93,11 +94,11 @@ impl App {
Event::Pages(page) => { Event::Pages(page) => {
if manager.current().page == page { if manager.current().page == page {
let targets = self.cx.manager.current().paginate().into_iter().map(|(_, f)| f).collect(); 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) => { Event::Mimetype(mimes) => {
if manager.update_mimetype(mimes, &self.cx.tasks) { if manager.update_mimetype(mimes, tasks) {
emit!(Render); emit!(Render);
} }
} }
@ -111,26 +112,26 @@ impl App {
emit!(Render); 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) => { Event::Input(mut opt, tx) => {
opt.position = match opt.position { opt.position = self.cx.position(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,
};
self.cx.input.show(opt, tx); self.cx.input.show(opt, tx);
emit!(Render); emit!(Render);
} }
Event::Open(files) => { Event::Open(targets, opener) => {
self.cx.tasks.file_open(files); 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) => { Event::Progress(percent, left) => {
self.cx.tasks.progress = (percent, left); tasks.progress = (percent, left);
emit!(Render); 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 struct Ctx {
pub cursor: Option<(u16, u16)>, pub cursor: Option<(u16, u16)>,
pub manager: Manager, pub manager: Manager,
pub select: Select,
pub input: Input, pub input: Input,
pub tasks: Tasks, pub tasks: Tasks,
} }
@ -14,8 +15,22 @@ impl Ctx {
cursor: None, cursor: None,
manager: Manager::new(), manager: Manager::new(),
select: Select::default(),
input: Input::default(), input: Input::default(),
tasks: Tasks::start(), 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 { impl Executor {
pub fn handle(cx: &mut Ctx, key: Key) -> bool { pub fn handle(cx: &mut Ctx, key: Key) -> bool {
let layer = if cx.input.visible { let layer = if cx.input.visible {
3
} else if cx.select.visible {
2 2
} else if cx.tasks.visible { } else if cx.tasks.visible {
1 1
@ -17,7 +19,7 @@ impl Executor {
let mut render = false; let mut render = false;
let mut matched = 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 { for Single { on, exec } in keymap {
if on.len() < 1 || on[0] != key { if on.len() < 1 || on[0] != key {
@ -31,12 +33,14 @@ impl Executor {
} else if layer == 1 { } else if layer == 1 {
render = Self::tasks(cx, e) || render; render = Self::tasks(cx, e) || render;
} else if layer == 2 { } else if layer == 2 {
render = Self::select(cx, e) || render;
} else if layer == 3 {
render = Self::input(cx, Some(e), key.code) || render; 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 = Self::input(cx, None, key.code);
} }
render render
@ -84,7 +88,8 @@ impl Executor {
} }
} }
"remove" => { "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(), "create" => cx.manager.create(),
"rename" => cx.manager.rename(), "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 { fn input(cx: &mut Ctx, exec: Option<&Exec>, code: KeyCode) -> bool {
let exec = if let Some(e) = exec { let exec = if let Some(e) = exec {
e e

View file

@ -21,8 +21,8 @@ impl<'a> Widget for Input<'a> {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(Style::default().fg(Color::Blue))
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Blue))
.title({ .title({
let mut line = Line::from(input.title()); let mut line = Line::from(input.title());
line.patch_style(Style::default().fg(Color::White)); line.patch_style(Style::default().fg(Color::White));

View file

@ -40,9 +40,9 @@ impl<'a> Folder<'a> {
impl<'a> Widget for Folder<'a> { impl<'a> Widget for Folder<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { 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() .iter()
.enumerate() .enumerate()
.map(|(i, (k, v))| { .map(|(i, (k, v))| {

View file

@ -16,7 +16,7 @@ impl<'a> Preview<'a> {
impl<'a> Widget for Preview<'a> { impl<'a> Widget for Preview<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { 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(); stdout().write(Kitty::image_hide()).ok();
return; return;
} }

View file

@ -6,6 +6,7 @@ mod input;
mod logs; mod logs;
mod manager; mod manager;
mod root; mod root;
mod select;
mod signals; mod signals;
mod status; mod status;
mod tasks; mod tasks;
@ -16,5 +17,6 @@ pub use context::*;
pub use dispatcher::*; pub use dispatcher::*;
pub use input::*; pub use input::*;
pub use logs::*; pub use logs::*;
pub use select::*;
pub use signals::*; pub use signals::*;
pub use term::*; pub use term::*;

View file

@ -1,6 +1,6 @@
use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget}; 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> { pub struct Root<'a> {
cx: &'a mut Ctx, cx: &'a mut Ctx,
@ -25,6 +25,10 @@ impl<'a> Widget for Root<'a> {
tasks::Layout::new(self.cx).render(area, buf); 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 { if self.cx.input.visible {
Input::new(self.cx).render(area, buf); Input::new(self.cx).render(area, buf);
self.cx.cursor = Some(self.cx.input.cursor()); 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);
}
}