diff --git a/yazi-config/preset/keymap.toml b/yazi-config/preset/keymap.toml index 393ad20ed..1e5744c08 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -151,6 +151,9 @@ keymap = [ # Tasks { on = "w", run = "tasks_show", desc = "Show task manager" }, + # Mount manager + { on = "M", run = "mount_show", desc = "Show mount manager" }, + # Help { on = "~", run = "help", desc = "Open help" }, { on = "", run = "help", desc = "Open help" }, @@ -178,6 +181,28 @@ keymap = [ { on = "", run = "help", desc = "Open help" }, ] +[mount] + +keymap = [ + { on = "", run = "close", desc = "Close mount manager" }, + { on = "", run = "close", desc = "Close mount manager" }, + { on = "", run = "close", desc = "Close mount manager" }, + { on = "M", run = "close", desc = "Close mount manager" }, + + { on = "k", run = "arrow -1", desc = "Move cursor up" }, + { on = "j", run = "arrow 1", desc = "Move cursor down" }, + + { on = "", run = "arrow -1", desc = "Move cursor up" }, + { on = "", run = "arrow 1", desc = "Move cursor down" }, + + { on = "", run = "mountpoint_cd", desc = "Change directory to selected mountpoint" }, + { on = "l", run = "mountpoint_cd", desc = "Change directory to selected mountpoint" }, + + # Help + { on = "~", run = "help", desc = "Open help" }, + { on = "", run = "help", desc = "Open help" }, +] + [spot] keymap = [ diff --git a/yazi-config/src/keymap/keymap.rs b/yazi-config/src/keymap/keymap.rs index a39dc6866..5760622d5 100644 --- a/yazi-config/src/keymap/keymap.rs +++ b/yazi-config/src/keymap/keymap.rs @@ -18,6 +18,7 @@ pub struct Keymap { pub confirm: Vec, pub help: Vec, pub completion: Vec, + pub mount: Vec, } impl Keymap { @@ -34,6 +35,7 @@ impl Keymap { Layer::Help => &self.help, Layer::Completion => &self.completion, Layer::Which => unreachable!(), + Layer::Mount => &self.mount, } } } @@ -61,6 +63,7 @@ impl<'de> Deserialize<'de> for Keymap { confirm: Inner, help: Inner, completion: Inner, + mount: Inner, } #[derive(Deserialize)] struct Inner { @@ -104,6 +107,8 @@ impl<'de> Deserialize<'de> for Keymap { help: mix(shadow.help.prepend_keymap, shadow.help.keymap, shadow.help.append_keymap), #[rustfmt::skip] completion: mix(shadow.completion.prepend_keymap, shadow.completion.keymap, shadow.completion.append_keymap), + #[rustfmt::skip] + mount: mix(shadow.mount.prepend_keymap, shadow.mount.keymap, shadow.mount.append_keymap), }) } } diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 6c2d14ebe..5dd0d09f4 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -6,7 +6,7 @@ clippy::unit_arg )] -yazi_macro::mod_pub!(completion confirm help input manager notify pick spot tab tasks which); +yazi_macro::mod_pub!(completion confirm help input manager notify pick spot tab tasks which mount); pub fn init() { manager::WATCHED.with(<_>::default); diff --git a/yazi-core/src/mount/commands/arrow.rs b/yazi-core/src/mount/commands/arrow.rs new file mode 100644 index 000000000..90f429404 --- /dev/null +++ b/yazi-core/src/mount/commands/arrow.rs @@ -0,0 +1,37 @@ +use yazi_macro::render; +use yazi_shared::event::{CmdCow, Data}; + +use crate::mount::Mount; + +struct Opt { + step: isize, +} + +impl From for Opt { + fn from(c: CmdCow) -> Self { + Self { step: c.first().and_then(Data::as_isize).unwrap_or(0) } + } +} + +impl From for Opt { + fn from(step: isize) -> Self { + Self { step } + } +} + +impl Mount { + #[yazi_codegen::command] + pub fn arrow(&mut self, opt: Opt) { + self.update(); + let old = self.cursor; + if opt.step > 0 { + self.cursor += 1; + } else { + self.cursor = self.cursor.saturating_sub(1); + } + + let max = Self::limit().min(self.points.len()); + self.cursor = self.cursor.min(max.saturating_sub(1)); + render!(self.cursor != old); + } +} diff --git a/yazi-core/src/mount/commands/mod.rs b/yazi-core/src/mount/commands/mod.rs new file mode 100644 index 000000000..da3ef2d98 --- /dev/null +++ b/yazi-core/src/mount/commands/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(arrow toggle mountpoint_cd); diff --git a/yazi-core/src/mount/commands/mountpoint_cd.rs b/yazi-core/src/mount/commands/mountpoint_cd.rs new file mode 100644 index 000000000..8d8c08162 --- /dev/null +++ b/yazi-core/src/mount/commands/mountpoint_cd.rs @@ -0,0 +1,14 @@ +use yazi_macro::emit; +use yazi_proxy::options::ProcessExecOpt; +use yazi_shared::{Layer, event::Cmd, fs::Url}; + +use crate::mount::Mount; + +impl Mount { + pub fn mountpoint_cd(&mut self, _opt: impl TryInto) { + if let Some(target) = self.points.get(self.cursor) { + let url: Url = target.path.clone().into(); + emit!(Call(Cmd::args("cd", &[url]), Layer::Manager)); + } + } +} diff --git a/yazi-core/src/mount/commands/toggle.rs b/yazi-core/src/mount/commands/toggle.rs new file mode 100644 index 000000000..f7946f7fc --- /dev/null +++ b/yazi-core/src/mount/commands/toggle.rs @@ -0,0 +1,26 @@ +use yazi_macro::render; +use yazi_shared::event::CmdCow; + +use crate::mount::Mount; + +struct Opt; + +impl From for Opt { + fn from(_: CmdCow) -> Self { Self } +} +impl From<()> for Opt { + fn from(_: ()) -> Self { Self } +} + +impl Mount { + #[yazi_codegen::command] + pub fn toggle(&mut self, _: Opt) { + self.visible = !self.visible; + + if self.visible { + self.arrow(0); + } + + render!(); + } +} diff --git a/yazi-core/src/mount/mod.rs b/yazi-core/src/mount/mod.rs new file mode 100644 index 000000000..908225ae6 --- /dev/null +++ b/yazi-core/src/mount/mod.rs @@ -0,0 +1,7 @@ +yazi_macro::mod_pub!(commands); + +yazi_macro::mod_flat!(mount); + +pub const MOUNT_BORDER: u16 = 2; +pub const MOUNT_PADDING: u16 = 2; +pub const MOUNT_PERCENT: u16 = 80; diff --git a/yazi-core/src/mount/mount.rs b/yazi-core/src/mount/mount.rs new file mode 100644 index 000000000..355e2c694 --- /dev/null +++ b/yazi-core/src/mount/mount.rs @@ -0,0 +1,53 @@ +use std::{io::BufRead, path::PathBuf, sync::Arc, time::Duration}; + +use parking_lot::Mutex; +use yazi_adapter::Dimension; +use yazi_scheduler::{Ongoing, TaskSummary}; + +use super::{MOUNT_BORDER, MOUNT_PADDING, MOUNT_PERCENT}; + +#[derive(Debug)] +pub struct MountPoint { + pub dev: String, + pub path: PathBuf, + pub fs: String, + pub opts: String, +} + +#[derive(Default)] +pub struct Mount { + pub visible: bool, + pub cursor: usize, + + pub points: Vec, +} + +impl Mount { + pub fn update(&mut self) { + let points = + std::io::BufReader::new(std::fs::File::open(PathBuf::from("/proc/mounts")).unwrap()) + .lines() + .map_while(Result::ok) + .filter_map(|l| { + let mut parts = l.trim_end_matches(" 0 0").split(' '); + Some(MountPoint { + dev: parts.next()?.into(), + path: parts.next()?.into(), + fs: parts.next()?.into(), + opts: parts.next()?.into(), + }) + }) + .filter(|p| !p.path.starts_with("/sys")) + .filter(|p| !p.path.starts_with("/tmp")) + .filter(|p| !p.path.starts_with("/run")) + .filter(|p| !p.path.starts_with("/dev")) + .filter(|p| !p.path.starts_with("/proc")); + self.points = points.collect(); + } + + #[inline] + pub fn limit() -> usize { + (Dimension::available().rows * MOUNT_PERCENT / 100).saturating_sub(MOUNT_BORDER + MOUNT_PADDING) + as usize + } +} diff --git a/yazi-fm/src/context.rs b/yazi-fm/src/context.rs index 8e4c1b4db..fca7c589a 100644 --- a/yazi-fm/src/context.rs +++ b/yazi-fm/src/context.rs @@ -1,5 +1,5 @@ use ratatui::layout::Rect; -use yazi_core::{completion::Completion, confirm::Confirm, help::Help, input::Input, manager::Manager, notify::Notify, pick::Pick, tab::Tab, tasks::Tasks, which::Which}; +use yazi_core::{completion::Completion, confirm::Confirm, help::Help, input::Input, manager::Manager, notify::Notify, pick::Pick, tab::Tab, tasks::Tasks, which::Which, mount::Mount}; use yazi_fs::Folder; pub struct Ctx { @@ -12,20 +12,22 @@ pub struct Ctx { pub completion: Completion, pub which: Which, pub notify: Notify, + pub mount: Mount, } impl Ctx { pub fn make() -> Self { Self { - manager: Manager::make(), - tasks: Tasks::serve(), - pick: Default::default(), - input: Default::default(), - confirm: Default::default(), - help: Default::default(), + manager: Manager::make(), + tasks: Tasks::serve(), + pick: Default::default(), + input: Default::default(), + confirm: Default::default(), + help: Default::default(), completion: Default::default(), - which: Default::default(), - notify: Default::default(), + which: Default::default(), + notify: Default::default(), + mount: Default::default(), } } diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index efe7619e5..dfd9e2d52 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -24,6 +24,7 @@ impl<'a> Executor<'a> { Layer::Help => self.help(cmd), Layer::Completion => self.completion(cmd), Layer::Which => self.which(cmd), + Layer::Mount => self.mount(cmd), } } @@ -145,6 +146,8 @@ impl<'a> Executor<'a> { match cmd.name.as_str() { // Tasks "tasks_show" => self.app.cx.tasks.toggle(()), + // Mount + "mount_show" => self.app.cx.mount.toggle(()), // Help "help" => self.app.cx.help.toggle(Layer::Manager), // Plugin @@ -355,4 +358,31 @@ impl<'a> Executor<'a> { on!(show); on!(callback); } + + fn mount(&mut self, cmd: CmdCow) { + macro_rules! on { + ($name:ident) => { + if cmd.name == stringify!($name) { + return self.app.cx.mount.$name(cmd); + } + }; + ($name:ident, $alias:literal) => { + if cmd.name == $alias { + return self.app.cx.mount.$name(cmd); + } + }; + } + + on!(toggle, "close"); + on!(arrow); + on!(mountpoint_cd); + + match cmd.name.as_str() { + // Help + "help" => self.app.cx.help.toggle(Layer::Mount), + // Plugin + "plugin" => self.app.plugin(cmd), + _ => {} + } + } } diff --git a/yazi-fm/src/root.rs b/yazi-fm/src/root.rs index 844cc4833..120a2c512 100644 --- a/yazi-fm/src/root.rs +++ b/yazi-fm/src/root.rs @@ -39,6 +39,10 @@ impl Widget for Root<'_> { tasks::Tasks::new(self.cx).render(area, buf); } + if self.cx.mount.visible { + tasks::Mount::new(self.cx).render(area, buf); + } + if self.cx.active().spot.visible() { spot::Spot::new(self.cx).render(area, buf); } diff --git a/yazi-fm/src/router.rs b/yazi-fm/src/router.rs index 28f94fc56..d45b616a6 100644 --- a/yazi-fm/src/router.rs +++ b/yazi-fm/src/router.rs @@ -40,6 +40,8 @@ impl<'a> Router<'a> { self.matches(Layer::Spot, key) } else if cx.tasks.visible { self.matches(Layer::Tasks, key) + } else if cx.mount.visible { + self.matches(Layer::Mount, key) } else { self.matches(Layer::Manager, key) } diff --git a/yazi-fm/src/tasks/mod.rs b/yazi-fm/src/tasks/mod.rs index 52d57d379..d3ca1d1f7 100644 --- a/yazi-fm/src/tasks/mod.rs +++ b/yazi-fm/src/tasks/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(progress tasks); +yazi_macro::mod_flat!(progress tasks mount); diff --git a/yazi-fm/src/tasks/mount.rs b/yazi-fm/src/tasks/mount.rs new file mode 100644 index 000000000..9aa7d87f8 --- /dev/null +++ b/yazi-fm/src/tasks/mount.rs @@ -0,0 +1,67 @@ +use ratatui::{ + buffer::Buffer, + layout::{self, Alignment, Constraint, Rect}, + text::Line, + widgets::{Block, BorderType, List, ListItem, Padding, Widget}, +}; +use yazi_config::THEME; +use yazi_core::tasks::TASKS_PERCENT; + +use crate::Ctx; + +pub(crate) struct Mount<'a> { + cx: &'a Ctx, +} + +impl<'a> Mount<'a> { + pub(crate) fn new(cx: &'a Ctx) -> Self { + Self { cx } + } + + pub(super) fn area(area: Rect) -> Rect { + let chunk = layout::Layout::vertical([ + Constraint::Percentage((100 - TASKS_PERCENT) / 2), + Constraint::Percentage(TASKS_PERCENT), + Constraint::Percentage((100 - TASKS_PERCENT) / 2), + ]) + .split(area)[1]; + + layout::Layout::horizontal([ + Constraint::Percentage((100 - TASKS_PERCENT) / 2), + Constraint::Percentage(TASKS_PERCENT), + Constraint::Percentage((100 - TASKS_PERCENT) / 2), + ]) + .split(chunk)[1] + } +} + +impl Widget for Mount<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + let area = Self::area(area); + + yazi_plugin::elements::Clear::default().render(area, buf); + let block = Block::bordered() + .title(Line::styled("Mount", THEME.tasks.title)) + .title_alignment(Alignment::Center) + .padding(Padding::symmetric(1, 1)) + .border_type(BorderType::Rounded) + .border_style(THEME.tasks.border); + block.clone().render(area, buf); + + let mnt = &self.cx.mount; + let items = mnt + .points + .iter() + .enumerate() + .map(|(i, p)| { + let mut item = ListItem::new(format!("{} {}", p.dev, p.path.to_string_lossy())); + if i == mnt.cursor { + item = item.style(THEME.tasks.hovered); + } + item + }) + .collect::>(); + + List::new(items).render(block.inner(area), buf); + } +} diff --git a/yazi-shared/src/layer.rs b/yazi-shared/src/layer.rs index 06b0689a3..87cd5a8a0 100644 --- a/yazi-shared/src/layer.rs +++ b/yazi-shared/src/layer.rs @@ -15,6 +15,7 @@ pub enum Layer { Help, Completion, Which, + Mount, } impl Display for Layer { @@ -30,6 +31,7 @@ impl Display for Layer { Self::Help => "help", Self::Completion => "completion", Self::Which => "which", + Self::Mount => "mount", }) } } @@ -49,6 +51,7 @@ impl FromStr for Layer { "help" => Self::Help, "completion" => Self::Completion, "which" => Self::Which, + "mount" => Self::Mount, _ => bail!("invalid layer: {s}"), }) }