mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41: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]
|
[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" },
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
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 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<()>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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))| {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::*;
|
||||||
|
|
|
||||||
|
|
@ -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
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