From bb2353c7b931f3f80394b39e771fb3a2b096ab17 Mon Sep 17 00:00:00 2001 From: Hanaasagi Date: Sat, 18 Nov 2023 01:40:20 +0800 Subject: [PATCH] feat: make `Input` and `Select` positions customizable (#361) --- yazi-config/preset/yazi.toml | 56 +++++++++ yazi-config/src/lib.rs | 5 + yazi-config/src/popup/input.rs | 73 ++++++++++++ yazi-config/src/popup/mod.rs | 13 +++ yazi-config/src/popup/offset.rs | 34 ++++++ yazi-config/src/popup/options.rs | 143 +++++++++++++++++++++++ yazi-config/src/popup/origin.rs | 24 ++++ yazi-config/src/popup/position.rs | 69 +++++++++++ yazi-config/src/popup/select.rs | 28 +++++ yazi-core/src/context.rs | 45 +++---- yazi-core/src/event.rs | 4 +- yazi-core/src/input/commands/escape.rs | 2 +- yazi-core/src/input/commands/move_.rs | 6 +- yazi-core/src/input/input.rs | 21 ++-- yazi-core/src/input/mod.rs | 2 - yazi-core/src/input/option.rs | 58 --------- yazi-core/src/input/snap.rs | 16 +-- yazi-core/src/input/snaps.rs | 44 +++---- yazi-core/src/lib.rs | 2 - yazi-core/src/manager/commands/create.rs | 8 +- yazi-core/src/manager/commands/open.rs | 10 +- yazi-core/src/manager/commands/quit.rs | 8 +- yazi-core/src/manager/commands/rename.rs | 11 +- yazi-core/src/position.rs | 25 ---- yazi-core/src/select/mod.rs | 4 - yazi-core/src/select/option.rs | 32 ----- yazi-core/src/select/select.rs | 8 +- yazi-core/src/tab/commands/cd.rs | 10 +- yazi-core/src/tab/commands/find.rs | 8 +- yazi-core/src/tab/commands/search.rs | 8 +- yazi-core/src/tab/commands/shell.rs | 10 +- yazi-core/src/tasks/tasks.rs | 13 +-- yazi-fm/src/completion/completion.rs | 19 ++- 33 files changed, 554 insertions(+), 265 deletions(-) create mode 100644 yazi-config/src/popup/input.rs create mode 100644 yazi-config/src/popup/mod.rs create mode 100644 yazi-config/src/popup/offset.rs create mode 100644 yazi-config/src/popup/options.rs create mode 100644 yazi-config/src/popup/origin.rs create mode 100644 yazi-config/src/popup/position.rs create mode 100644 yazi-config/src/popup/select.rs delete mode 100644 yazi-core/src/input/option.rs delete mode 100644 yazi-core/src/position.rs delete mode 100644 yazi-core/src/select/option.rs diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index c234909b..349b836b 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -73,5 +73,61 @@ bizarre_retry = 5 [plugins] preload = [] +[input] +# cd +cd_title = "Change directory:" +cd_origin = "top-center" +cd_offset = [ 0, 2, 50, 3 ] + +# create +create_title = "Create:" +create_origin = "top-center" +create_offset = [ 0, 2, 50, 3 ] + +# rename +rename_title = "Rename:" +rename_origin = "hovered" +rename_offset = [ 0, 1, 50, 3 ] + +# trash +trash_title = "Move {n} selected file{s} to trash? (y/N)" +trash_origin = "top-center" +trash_offset = [ 0, 2, 50, 3 ] + +# delete +delete_title = "Delete {n} selected file{s} permanently? (y/N)" +delete_origin = "top-center" +delete_offset = [ 0, 2, 50, 3 ] + +# find +find_title = [ "Find next:", "Find previous:" ] +find_origin = "top-center" +find_offset = [ 0, 2, 50, 3 ] + +# search +search_title = "Search:" +search_origin = "top-center" +search_offset = [ 0, 2, 50, 3 ] + +# shell +shell_title = [ "Shell:", "Shell (block):" ] +shell_origin = "top-center" +shell_offset = [ 0, 2, 50, 3 ] + +# overwrite +overwrite_title = "Overwrite an existing file? (y/N)" +overwrite_origin = "top-center" +overwrite_offset = [ 0, 2, 50, 3 ] + +# quit +quit_title = "{n} task{s} running, sure to quit? (y/N)" +quit_origin = "top-center" +quit_offset = [ 0, 2, 50, 3 ] + +[select] +open_title = "Open with:" +open_origin = "hovered" +open_offset = [ 0, 1, 50, 7 ] + [log] enabled = false diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 7baec519..0edb2e45 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -9,6 +9,7 @@ pub mod manager; pub mod open; mod pattern; pub mod plugins; +pub mod popup; mod preset; pub mod preview; mod tasks; @@ -32,6 +33,8 @@ pub static PLUGINS: RoCell = RoCell::new(); pub static PREVIEW: RoCell = RoCell::new(); pub static TASKS: RoCell = RoCell::new(); pub static THEME: RoCell = RoCell::new(); +pub static INPUT: RoCell = RoCell::new(); +pub static SELECT: RoCell = RoCell::new(); pub static BOOT: RoCell = RoCell::new(); @@ -48,6 +51,8 @@ pub fn init() { PREVIEW.with(Default::default); TASKS.with(Default::default); THEME.with(Default::default); + INPUT.with(Default::default); + SELECT.with(Default::default); BOOT.with(Default::default); } diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs new file mode 100644 index 00000000..6453b11f --- /dev/null +++ b/yazi-config/src/popup/input.rs @@ -0,0 +1,73 @@ +use serde::Deserialize; + +use super::{Offset, Origin}; +use crate::MERGED_YAZI; + +#[derive(Deserialize)] +pub struct Input { + // cd + pub cd_title: String, + pub cd_origin: Origin, + pub cd_offset: Offset, + + // create + pub create_title: String, + pub create_origin: Origin, + pub create_offset: Offset, + + // rename + pub rename_title: String, + pub rename_origin: Origin, + pub rename_offset: Offset, + + // trash + pub trash_title: String, + pub trash_origin: Origin, + pub trash_offset: Offset, + + // delete + pub delete_title: String, + pub delete_origin: Origin, + pub delete_offset: Offset, + + // find + pub find_title: [String; 2], + pub find_origin: Origin, + pub find_offset: Offset, + + // search + pub search_title: String, + pub search_origin: Origin, + pub search_offset: Offset, + + // shell + pub shell_title: [String; 2], + pub shell_origin: Origin, + pub shell_offset: Offset, + + // overwrite + pub overwrite_title: String, + pub overwrite_origin: Origin, + pub overwrite_offset: Offset, + + // quit + pub quit_title: String, + pub quit_origin: Origin, + pub quit_offset: Offset, +} + +impl Default for Input { + fn default() -> Self { + #[derive(Deserialize)] + struct Outer { + input: Input, + } + + toml::from_str::(&MERGED_YAZI).unwrap().input + } +} + +impl Input { + #[inline] + pub const fn border(&self) -> u16 { 2 } +} diff --git a/yazi-config/src/popup/mod.rs b/yazi-config/src/popup/mod.rs new file mode 100644 index 00000000..8308b911 --- /dev/null +++ b/yazi-config/src/popup/mod.rs @@ -0,0 +1,13 @@ +mod input; +mod offset; +mod options; +mod origin; +mod position; +mod select; + +pub use input::*; +pub use offset::*; +pub use options::*; +pub use origin::*; +pub use position::*; +pub use select::*; diff --git a/yazi-config/src/popup/offset.rs b/yazi-config/src/popup/offset.rs new file mode 100644 index 00000000..d8524321 --- /dev/null +++ b/yazi-config/src/popup/offset.rs @@ -0,0 +1,34 @@ +use anyhow::bail; +use serde::Deserialize; + +#[derive(Clone, Copy, Default, Deserialize)] +#[serde(try_from = "Vec")] +pub struct Offset { + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, +} + +impl TryFrom> for Offset { + type Error = anyhow::Error; + + fn try_from(values: Vec) -> Result { + if values.len() != 4 { + bail!("offset must have 4 values: {:?}", values); + } + if values[2] < 0 || values[3] < 0 { + bail!("offset width and height must be positive: {:?}", values); + } + if values[3] < 3 { + bail!("offset height must be at least 3: {:?}", values); + } + + Ok(Self { + x: values[0], + y: values[1], + width: values[2] as u16, + height: values[3] as u16, + }) + } +} diff --git a/yazi-config/src/popup/options.rs b/yazi-config/src/popup/options.rs new file mode 100644 index 00000000..472d45b5 --- /dev/null +++ b/yazi-config/src/popup/options.rs @@ -0,0 +1,143 @@ +use super::{Offset, Position}; +use crate::{INPUT, SELECT}; + +#[derive(Default)] +pub struct InputOpt { + pub title: String, + pub value: String, + pub position: Position, + pub realtime: bool, + pub completion: bool, + pub highlight: bool, +} + +#[derive(Default)] +pub struct SelectOpt { + pub title: String, + pub items: Vec, + pub position: Position, +} + +impl InputOpt { + #[inline] + pub fn cd() -> Self { + Self { + title: INPUT.cd_title.to_owned(), + position: Position::new(INPUT.cd_origin, INPUT.cd_offset), + completion: true, + ..Default::default() + } + } + + #[inline] + pub fn create() -> Self { + Self { + title: INPUT.create_title.to_owned(), + position: Position::new(INPUT.create_origin, INPUT.create_offset), + ..Default::default() + } + } + + #[inline] + pub fn rename() -> Self { + Self { + title: INPUT.rename_title.to_owned(), + position: Position::new(INPUT.rename_origin, INPUT.rename_offset), + ..Default::default() + } + } + + #[inline] + pub fn trash(n: usize) -> Self { + let title = INPUT.trash_title.replace("{n}", &n.to_string()); + Self { + title: title.replace("{s}", if n > 1 { "s" } else { "" }), + position: Position::new(INPUT.trash_origin, INPUT.trash_offset), + ..Default::default() + } + } + + #[inline] + pub fn delete(n: usize) -> Self { + let title = INPUT.delete_title.replace("{n}", &n.to_string()); + Self { + title: title.replace("{s}", if n > 1 { "s" } else { "" }), + position: Position::new(INPUT.delete_origin, INPUT.delete_offset), + ..Default::default() + } + } + + #[inline] + pub fn find(prev: bool) -> Self { + Self { + title: INPUT.find_title[prev as usize].to_owned(), + position: Position::new(INPUT.find_origin, INPUT.find_offset), + realtime: true, + ..Default::default() + } + } + + #[inline] + pub fn search() -> Self { + Self { + title: INPUT.search_title.to_owned(), + position: Position::new(INPUT.search_origin, INPUT.search_offset), + ..Default::default() + } + } + + #[inline] + pub fn shell(block: bool) -> Self { + Self { + title: INPUT.shell_title[block as usize].to_owned(), + position: Position::new(INPUT.shell_origin, INPUT.shell_offset), + highlight: true, + ..Default::default() + } + } + + #[inline] + pub fn overwrite() -> Self { + Self { + title: INPUT.overwrite_title.to_owned(), + position: Position::new(INPUT.overwrite_origin, INPUT.overwrite_offset), + ..Default::default() + } + } + + #[inline] + pub fn quit(n: usize) -> Self { + let title = INPUT.quit_title.replace("{n}", &n.to_string()); + Self { + title: title.replace("{s}", if n > 1 { "s" } else { "" }), + position: Position::new(INPUT.quit_origin, INPUT.quit_offset), + ..Default::default() + } + } + + #[inline] + pub fn with_value(mut self, value: impl Into) -> Self { + self.value = value.into(); + self + } +} + +impl SelectOpt { + #[inline] + fn max_height(len: usize) -> u16 { + SELECT.open_offset.height.min(SELECT.border().saturating_add(len as u16)) + } + + #[inline] + pub fn open(items: Vec) -> Self { + let max_height = Self::max_height(items.len()); + Self { + title: SELECT.open_title.to_owned(), + items, + position: Position::new(SELECT.open_origin, Offset { + height: max_height, + ..SELECT.open_offset + }), + } + } +} diff --git a/yazi-config/src/popup/origin.rs b/yazi-config/src/popup/origin.rs new file mode 100644 index 00000000..ac997ecf --- /dev/null +++ b/yazi-config/src/popup/origin.rs @@ -0,0 +1,24 @@ +use serde::Deserialize; + +#[derive(Clone, Copy, Default, Deserialize, PartialEq, Eq)] +pub enum Origin { + #[default] + #[serde(rename = "top-left")] + TopLeft, + #[serde(rename = "top-center")] + TopCenter, + #[serde(rename = "top-right")] + TopRight, + + #[serde(rename = "bottom-left")] + BottomLeft, + #[serde(rename = "bottom-center")] + BottomCenter, + #[serde(rename = "bottom-right")] + BottomRight, + + #[serde(rename = "center")] + Center, + #[serde(rename = "hovered")] + Hovered, +} diff --git a/yazi-config/src/popup/position.rs b/yazi-config/src/popup/position.rs new file mode 100644 index 00000000..4964e10b --- /dev/null +++ b/yazi-config/src/popup/position.rs @@ -0,0 +1,69 @@ +use crossterm::terminal::WindowSize; +use ratatui::layout::Rect; +use yazi_shared::Term; + +use super::{Offset, Origin}; + +#[derive(Clone, Copy, Default)] +pub struct Position { + pub origin: Origin, + pub offset: Offset, +} + +impl Position { + #[inline] + pub fn new(origin: Origin, offset: Offset) -> Self { Self { origin, offset } } + + pub fn rect(&self) -> Rect { + use Origin::*; + let Offset { x, y, width, height } = self.offset; + let WindowSize { columns, rows, .. } = Term::size(); + + let max_x = columns.saturating_sub(width); + let new_x = match self.origin { + TopLeft | BottomLeft => x.clamp(0, max_x as i16) as u16, + TopCenter | BottomCenter | Center => { + (columns / 2).saturating_sub(width / 2).saturating_add_signed(x).clamp(0, max_x) + } + TopRight | BottomRight => max_x.saturating_add_signed(x).clamp(0, max_x), + Hovered => unreachable!(), + }; + + let max_y = rows.saturating_sub(height); + let new_y = match self.origin { + TopLeft | TopCenter | TopRight => y.clamp(0, max_y as i16) as u16, + Center => (max_y / 2).saturating_sub(height / 2).saturating_add_signed(y).clamp(0, max_y), + BottomLeft | BottomCenter | BottomRight => max_y.saturating_add_signed(y).clamp(0, max_y), + Hovered => unreachable!(), + }; + + Rect { + x: new_x, + y: new_y, + width: width.min(columns.saturating_sub(new_x)), + height: height.min(rows.saturating_sub(new_y)), + } + } + + pub fn sticky(base: Rect, offset: Offset) -> Rect { + let Offset { x, y, width, height } = offset; + let WindowSize { columns, rows, .. } = Term::size(); + + let above = + base.y.saturating_add(base.height).saturating_add(height).saturating_add_signed(y) > rows; + + let new_x = base.x.saturating_add_signed(x).clamp(0, columns.saturating_sub(width)); + let new_y = if above { + base.y.saturating_sub(height.saturating_sub(y.unsigned_abs())) + } else { + base.y.saturating_add(base.height).saturating_add_signed(y) + }; + + Rect { + x: new_x, + y: new_y, + width: width.min(columns.saturating_sub(new_x)), + height: height.min(rows.saturating_sub(new_y)), + } + } +} diff --git a/yazi-config/src/popup/select.rs b/yazi-config/src/popup/select.rs new file mode 100644 index 00000000..05d24a19 --- /dev/null +++ b/yazi-config/src/popup/select.rs @@ -0,0 +1,28 @@ +use serde::Deserialize; + +use super::{Offset, Origin}; +use crate::MERGED_YAZI; + +#[derive(Deserialize)] +pub struct Select { + // open + pub open_title: String, + pub open_origin: Origin, + pub open_offset: Offset, +} + +impl Default for Select { + fn default() -> Self { + #[derive(Deserialize)] + struct Outer { + select: Select, + } + + toml::from_str::(&MERGED_YAZI).unwrap().select + } +} + +impl Select { + #[inline] + pub const fn border(&self) -> u16 { 2 } +} diff --git a/yazi-core/src/context.rs b/yazi-core/src/context.rs index 31546bb3..d44ee720 100644 --- a/yazi-core/src/context.rs +++ b/yazi-core/src/context.rs @@ -1,8 +1,7 @@ -use crossterm::terminal::WindowSize; use ratatui::prelude::Rect; -use yazi_shared::Term; +use yazi_config::popup::{Origin, Position}; -use crate::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position}; +use crate::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which}; pub struct Ctx { pub manager: Manager, @@ -27,36 +26,18 @@ impl Ctx { } } - pub fn area(&self, pos: &Position) -> Rect { - let WindowSize { columns, rows, .. } = Term::size(); + pub fn area(&self, position: &Position) -> Rect { + if position.origin != Origin::Hovered { + return position.rect(); + } - let (x, y) = match pos { - Position::Top(Rect { mut x, mut y, width, height }) => { - x = x.min(columns.saturating_sub(*width)); - y = y.min(rows.saturating_sub(*height)); - ((columns / 2).saturating_sub(width / 2) + x, y) - } - Position::Sticky(Rect { mut x, y, width, height }, r) => { - x = x.min(columns.saturating_sub(*width)); - if y + height + r.y + r.height > rows { - (x + r.x, r.y.saturating_sub(height.saturating_sub(*y))) - } else { - (x + r.x, y + r.y + r.height) - } - } - Position::Hovered(rect) => { - return self.area(&if let Some(r) = - self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url)) - { - Position::Sticky(*rect, r) - } else { - Position::Top(*rect) - }); - } - }; - - let (w, h) = pos.dimension(); - Rect { x, y, width: w.min(columns.saturating_sub(x)), height: h.min(rows.saturating_sub(y)) } + if let Some(r) = + self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url)) + { + Position::sticky(r, position.offset) + } else { + Position::new(Origin::TopCenter, position.offset).rect() + } } #[inline] diff --git a/yazi-core/src/event.rs b/yazi-core/src/event.rs index 82baeb99..5ab91cff 100644 --- a/yazi-core/src/event.rs +++ b/yazi-core/src/event.rs @@ -3,10 +3,10 @@ use std::{collections::BTreeMap, ffi::OsString}; use anyhow::Result; use crossterm::event::KeyEvent; use tokio::sync::{mpsc::{self, UnboundedSender}, oneshot}; -use yazi_config::{keymap::{Exec, KeymapLayer}, open::Opener}; +use yazi_config::{keymap::{Exec, KeymapLayer}, open::Opener, popup::{InputOpt, SelectOpt}}; use yazi_shared::{InputError, RoCell, Url}; -use super::{files::FilesOp, input::InputOpt, select::SelectOpt}; +use super::files::FilesOp; use crate::{preview::PreviewLock, tasks::TasksProgress}; static TX: RoCell> = RoCell::new(); diff --git a/yazi-core/src/input/commands/escape.rs b/yazi-core/src/input/commands/escape.rs index 4e260bc8..1e8767fb 100644 --- a/yazi-core/src/input/commands/escape.rs +++ b/yazi-core/src/input/commands/escape.rs @@ -30,7 +30,7 @@ impl Input { } } } - self.snaps.tag(); + self.snaps.tag(self.limit()); true } } diff --git a/yazi-core/src/input/commands/move_.rs b/yazi-core/src/input/commands/move_.rs index b58e2a48..4bccecd4 100644 --- a/yazi-core/src/input/commands/move_.rs +++ b/yazi-core/src/input/commands/move_.rs @@ -38,7 +38,7 @@ impl Input { false, ); - let snap = self.snap_mut(); + let (limit, snap) = (self.limit(), self.snap_mut()); if snap.offset > snap.cursor { snap.offset = snap.cursor; } else if snap.value.is_empty() { @@ -46,9 +46,9 @@ impl Input { } else { let delta = snap.mode.delta(); let s = snap.slice(snap.offset..snap.cursor + delta); - if s.width() >= /*TODO: hardcode*/ 50 - 2 { + if s.width() >= limit { let s = s.chars().rev().collect::(); - snap.offset = snap.cursor - InputSnap::find_window(&s, 0).end.saturating_sub(delta); + snap.offset = snap.cursor - InputSnap::find_window(&s, 0, limit).end.saturating_sub(delta); } } diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 2a019c79..0709f4e1 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -2,10 +2,11 @@ use std::ops::Range; use tokio::sync::mpsc::UnboundedSender; use unicode_width::UnicodeWidthStr; +use yazi_config::{popup::{InputOpt, Position}, INPUT}; use yazi_shared::InputError; -use super::{mode::InputMode, op::InputOp, InputOpt, InputSnap, InputSnaps}; -use crate::{external, Position}; +use super::{mode::InputMode, op::InputOp, InputSnap, InputSnaps}; +use crate::external; #[derive(Default)] pub struct Input { @@ -28,9 +29,7 @@ pub struct Input { impl Input { pub fn show(&mut self, opt: InputOpt, tx: UnboundedSender>) { self.close(false); - self.snaps.reset(opt.value); self.visible = true; - self.title = opt.title; self.position = opt.position; @@ -41,6 +40,14 @@ impl Input { // Shell self.highlight = opt.highlight; + + // Reset snaps + self.snaps.reset(opt.value, self.limit()); + } + + #[inline] + pub(super) fn limit(&self) -> usize { + self.position.offset.width.saturating_sub(INPUT.border()) as usize } pub fn type_str(&mut self, s: &str) -> bool { @@ -92,7 +99,7 @@ impl Input { return false; } if !matches!(old.op, InputOp::None | InputOp::Select(_)) { - self.snaps.tag().then(|| self.flush_value()); + self.snaps.tag(self.limit()).then(|| self.flush_value()); } true } @@ -115,7 +122,7 @@ impl Input { impl Input { #[inline] - pub fn value(&self) -> &str { self.snap().slice(self.snap().window()) } + pub fn value(&self) -> &str { self.snap().slice(self.snap().window(self.limit())) } #[inline] pub fn mode(&self) -> InputMode { self.snap().mode } @@ -133,7 +140,7 @@ impl Input { let (start, end) = if start < snap.cursor { (start, snap.cursor) } else { (snap.cursor + 1, start + 1) }; - let win = snap.window(); + let win = snap.window(self.limit()); let Range { start, end } = start.max(win.start)..end.min(win.end); let s = snap.slice(snap.offset..start).width() as u16; diff --git a/yazi-core/src/input/mod.rs b/yazi-core/src/input/mod.rs index 328d8295..bd00b9e5 100644 --- a/yazi-core/src/input/mod.rs +++ b/yazi-core/src/input/mod.rs @@ -2,7 +2,6 @@ mod commands; mod input; mod mode; mod op; -mod option; mod shell; mod snap; mod snaps; @@ -10,6 +9,5 @@ mod snaps; pub use input::*; pub use mode::*; use op::*; -pub use option::*; use snap::*; use snaps::*; diff --git a/yazi-core/src/input/option.rs b/yazi-core/src/input/option.rs deleted file mode 100644 index f8bb6e64..00000000 --- a/yazi-core/src/input/option.rs +++ /dev/null @@ -1,58 +0,0 @@ -use ratatui::prelude::Rect; - -use crate::Position; - -#[derive(Default)] -pub struct InputOpt { - pub title: String, - pub value: String, - pub position: Position, - pub realtime: bool, - pub completion: bool, - pub highlight: bool, -} - -impl InputOpt { - pub fn top(title: impl AsRef) -> Self { - Self { - title: title.as_ref().to_owned(), - position: Position::Top(/* TODO: hardcode */ Rect { x: 0, y: 2, width: 50, height: 3 }), - ..Default::default() - } - } - - pub fn hovered(title: impl AsRef) -> Self { - Self { - title: title.as_ref().to_owned(), - position: Position::Hovered( - // TODO: hardcode - Rect { x: 0, y: 1, width: 50, height: 3 }, - ), - ..Default::default() - } - } - - #[inline] - pub fn with_value(mut self, value: impl AsRef) -> Self { - self.value = value.as_ref().to_owned(); - self - } - - #[inline] - pub fn with_realtime(mut self) -> Self { - self.realtime = true; - self - } - - #[inline] - pub fn with_completion(mut self) -> Self { - self.completion = true; - self - } - - #[inline] - pub fn with_highlight(mut self) -> Self { - self.highlight = true; - self - } -} diff --git a/yazi-core/src/input/snap.rs b/yazi-core/src/input/snap.rs index 21c06f2d..59d405b9 100644 --- a/yazi-core/src/input/snap.rs +++ b/yazi-core/src/input/snap.rs @@ -16,7 +16,7 @@ pub(super) struct InputSnap { } impl InputSnap { - pub(super) fn new(value: String) -> Self { + pub(super) fn new(value: String, limit: usize) -> Self { let mut snap = Self { value, @@ -26,15 +26,15 @@ impl InputSnap { offset: usize::MAX, cursor: usize::MAX, }; - snap.reset(); + snap.reset(limit); snap } #[inline] - pub(super) fn reset(&mut self) { + pub(super) fn reset(&mut self, limit: usize) { self.cursor = self.cursor.min(self.value.chars().count().saturating_sub(self.mode.delta())); self.offset = - self.offset.min(self.cursor.saturating_sub(Self::find_window(&self.rev(), 0).end)); + self.offset.min(self.cursor.saturating_sub(Self::find_window(&self.rev(), 0, limit).end)); } } @@ -65,10 +65,12 @@ impl InputSnap { pub(super) fn rev(&self) -> String { self.value.chars().rev().collect::() } #[inline] - pub(super) fn window(&self) -> Range { Self::find_window(&self.value, self.offset) } + pub(super) fn window(&self, limit: usize) -> Range { + Self::find_window(&self.value, self.offset, limit) + } #[inline] - pub(super) fn find_window(s: &str, offset: usize) -> Range { + pub(super) fn find_window(s: &str, offset: usize, limit: usize) -> Range { let mut width = 0; let v = s .chars() @@ -76,7 +78,7 @@ impl InputSnap { .skip(offset) .map_while(|(i, c)| { width += c.width().unwrap_or(0); - if width < /*TODO: hardcode*/ 50 - 2 { Some(i) } else { None } + if width < limit { Some(i) } else { None } }) .collect::>(); diff --git a/yazi-core/src/input/snaps.rs b/yazi-core/src/input/snaps.rs index f1721c0b..14b8b444 100644 --- a/yazi-core/src/input/snaps.rs +++ b/yazi-core/src/input/snaps.rs @@ -11,13 +11,33 @@ pub(super) struct InputSnaps { impl InputSnaps { #[inline] - pub(super) fn reset(&mut self, value: String) { + pub(super) fn reset(&mut self, value: String, limit: usize) { self.idx = 0; self.versions.clear(); - self.versions.push(InputSnap::new(value)); + self.versions.push(InputSnap::new(value, limit)); self.current = self.versions[0].clone(); } + pub(super) fn tag(&mut self, limit: usize) -> bool { + // Sync *current* cursor position to the *last* version: + // Save offset/cursor/ect. of the *current* as the last version, + // while keeping the *last* value unchanged. + let value = mem::take(&mut self.versions[self.idx].value); + self.versions[self.idx] = self.current.clone(); + self.versions[self.idx].value = value; + self.versions[self.idx].reset(limit); + + // If the *current* value is the same as the *last* version + if self.versions[self.idx].value == self.current.value { + return false; + } + + self.versions.truncate(self.idx + 1); + self.versions.push(self.current().clone()); + self.idx += 1; + true + } + pub(super) fn undo(&mut self) -> bool { if self.idx == 0 { return false; @@ -37,26 +57,6 @@ impl InputSnaps { self.current = self.versions[self.idx].clone(); true } - - pub(super) fn tag(&mut self) -> bool { - // Sync *current* cursor position to the *last* version: - // Save offset/cursor/ect. of the *current* as the last version, - // while keeping the *last* value unchanged. - let value = mem::take(&mut self.versions[self.idx].value); - self.versions[self.idx] = self.current.clone(); - self.versions[self.idx].value = value; - self.versions[self.idx].reset(); - - // If the *current* value is the same as the *last* version - if self.versions[self.idx].value == self.current.value { - return false; - } - - self.versions.truncate(self.idx + 1); - self.versions.push(self.current().clone()); - self.idx += 1; - true - } } impl InputSnaps { diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 044a3d2c..04664cdf 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -16,7 +16,6 @@ pub mod help; mod highlighter; pub mod input; pub mod manager; -mod position; pub mod preview; pub mod select; mod step; @@ -28,7 +27,6 @@ pub use blocker::*; pub use context::*; pub use event::*; pub use highlighter::*; -pub use position::*; pub use step::*; pub fn init() { init_blocker(); } diff --git a/yazi-core/src/manager/commands/create.rs b/yazi-core/src/manager/commands/create.rs index e97af987..2a912acc 100644 --- a/yazi-core/src/manager/commands/create.rs +++ b/yazi-core/src/manager/commands/create.rs @@ -1,10 +1,10 @@ use std::path::{PathBuf, MAIN_SEPARATOR}; use tokio::fs; -use yazi_config::keymap::Exec; +use yazi_config::{keymap::Exec, popup::InputOpt}; use yazi_shared::Url; -use crate::{emit, files::{File, FilesOp}, input::InputOpt, manager::Manager}; +use crate::{emit, files::{File, FilesOp}, manager::Manager}; pub struct Opt { force: bool, @@ -19,14 +19,14 @@ impl Manager { let opt = opt.into() as Opt; let cwd = self.cwd().to_owned(); tokio::spawn(async move { - let mut result = emit!(Input(InputOpt::top("Create:"))); + let mut result = emit!(Input(InputOpt::create())); let Some(Ok(name)) = result.recv().await else { return Ok(()); }; let path = cwd.join(&name); if !opt.force && fs::symlink_metadata(&path).await.is_ok() { - match emit!(Input(InputOpt::top("Overwrite an existing file? (y/N)"))).recv().await { + match emit!(Input(InputOpt::overwrite())).recv().await { Some(Ok(c)) if c == "y" || c == "Y" => (), _ => return Ok(()), } diff --git a/yazi-core/src/manager/commands/open.rs b/yazi-core/src/manager/commands/open.rs index 50fca89f..d436c3f6 100644 --- a/yazi-core/src/manager/commands/open.rs +++ b/yazi-core/src/manager/commands/open.rs @@ -1,9 +1,9 @@ use std::ffi::OsString; -use yazi_config::{keymap::Exec, OPEN}; +use yazi_config::{keymap::Exec, popup::SelectOpt, OPEN}; use yazi_shared::MIME_DIR; -use crate::{emit, external, manager::Manager, select::SelectOpt}; +use crate::{emit, external, manager::Manager}; pub struct Opt { interactive: bool, @@ -20,11 +20,7 @@ impl Manager { return; } - let result = emit!(Select(SelectOpt::hovered( - "Open with:", - openers.iter().map(|o| o.desc.clone()).collect() - ))); - + let result = emit!(Select(SelectOpt::open(openers.iter().map(|o| o.desc.clone()).collect()))); if let Ok(choice) = result.await { emit!(Open(files, Some(openers[choice].clone()))); } diff --git a/yazi-core/src/manager/commands/quit.rs b/yazi-core/src/manager/commands/quit.rs index d1eb0fcd..e4f70fc0 100644 --- a/yazi-core/src/manager/commands/quit.rs +++ b/yazi-core/src/manager/commands/quit.rs @@ -1,6 +1,6 @@ -use yazi_config::keymap::Exec; +use yazi_config::{keymap::Exec, popup::InputOpt}; -use crate::{emit, input::InputOpt, manager::Manager, tasks::Tasks}; +use crate::{emit, manager::Manager, tasks::Tasks}; #[derive(Default)] pub struct Opt { @@ -24,9 +24,7 @@ impl Manager { } tokio::spawn(async move { - let mut result = - emit!(Input(InputOpt::top(format!("{tasks} tasks running, sure to quit? (y/N)")))); - + let mut result = emit!(Input(InputOpt::quit(tasks))); if let Some(Ok(choice)) = result.recv().await { if choice == "y" || choice == "Y" { emit!(Quit(opt.no_cwd_file)); diff --git a/yazi-core/src/manager/commands/rename.rs b/yazi-core/src/manager/commands/rename.rs index 35a4f7aa..bae6ca67 100644 --- a/yazi-core/src/manager/commands/rename.rs +++ b/yazi-core/src/manager/commands/rename.rs @@ -2,10 +2,10 @@ use std::{collections::BTreeSet, ffi::OsStr, io::{stdout, BufWriter, Write}, pat use anyhow::{anyhow, bail, Result}; use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; -use yazi_config::{keymap::Exec, OPEN, PREVIEW}; +use yazi_config::{keymap::Exec, popup::InputOpt, OPEN, PREVIEW}; use yazi_shared::{max_common_root, Defer, Term, Url}; -use crate::{emit, external::{self, ShellOpt}, files::{File, FilesOp}, input::InputOpt, manager::Manager, Event, BLOCKER}; +use crate::{emit, external::{self, ShellOpt}, files::{File, FilesOp}, manager::Manager, Event, BLOCKER}; pub struct Opt { force: bool, @@ -42,9 +42,8 @@ impl Manager { let opt = opt.into() as Opt; tokio::spawn(async move { - let mut result = emit!(Input( - InputOpt::hovered("Rename:").with_value(hovered.file_name().unwrap().to_string_lossy()) - )); + let mut result = + emit!(Input(InputOpt::rename().with_value(hovered.file_name().unwrap().to_string_lossy()))); let Some(Ok(name)) = result.recv().await else { return; @@ -56,7 +55,7 @@ impl Manager { return; } - let mut result = emit!(Input(InputOpt::hovered("Overwrite an existing file? (y/N)"))); + let mut result = emit!(Input(InputOpt::overwrite())); if let Some(Ok(choice)) = result.recv().await { if choice == "y" || choice == "Y" { Self::rename_and_hover(hovered, Url::from(new)).await.ok(); diff --git a/yazi-core/src/position.rs b/yazi-core/src/position.rs deleted file mode 100644 index 37bd0911..00000000 --- a/yazi-core/src/position.rs +++ /dev/null @@ -1,25 +0,0 @@ -use ratatui::prelude::Rect; - -pub enum Position { - Top(Rect), - Sticky(Rect, Rect), - Hovered(Rect), -} - -impl Default for Position { - fn default() -> Self { Self::Top(Rect::default()) } -} - -impl Position { - #[inline] - pub fn rect(&self) -> Rect { - match self { - Position::Top(rect) => *rect, - Position::Sticky(rect, _) => *rect, - Position::Hovered(rect) => *rect, - } - } - - #[inline] - pub fn dimension(&self) -> (u16, u16) { (self.rect().width, self.rect().height) } -} diff --git a/yazi-core/src/select/mod.rs b/yazi-core/src/select/mod.rs index 3b4cf878..f98d4db8 100644 --- a/yazi-core/src/select/mod.rs +++ b/yazi-core/src/select/mod.rs @@ -1,8 +1,4 @@ mod commands; -mod option; mod select; -pub use option::*; pub use select::*; - -pub const SELECT_PADDING: u16 = 2; diff --git a/yazi-core/src/select/option.rs b/yazi-core/src/select/option.rs deleted file mode 100644 index eaf41037..00000000 --- a/yazi-core/src/select/option.rs +++ /dev/null @@ -1,32 +0,0 @@ -use ratatui::prelude::Rect; - -use crate::Position; - -pub struct SelectOpt { - pub title: String, - pub items: Vec, - pub position: Position, -} - -impl SelectOpt { - pub fn top(title: &str, items: Vec) -> Self { - let height = 2 + items.len().min(/* TODO: hardcode */ 5) as u16; - Self { - title: title.to_owned(), - items, - position: Position::Top(/* TODO: hardcode */ Rect { x: 0, y: 2, width: 50, height }), - } - } - - pub fn hovered(title: &str, items: Vec) -> Self { - let height = 2 + items.len().min(/* TODO: hardcode */ 5) as u16; - Self { - title: title.to_owned(), - items, - position: Position::Hovered( - // TODO: hardcode - Rect { x: 0, y: 1, width: 50, height }, - ), - } - } -} diff --git a/yazi-core/src/select/select.rs b/yazi-core/src/select/select.rs index 38fb14a6..5946c42b 100644 --- a/yazi-core/src/select/select.rs +++ b/yazi-core/src/select/select.rs @@ -1,8 +1,6 @@ use anyhow::Result; use tokio::sync::oneshot::Sender; - -use super::SelectOpt; -use crate::Position; +use yazi_config::{popup::{Position, SelectOpt}, SELECT}; #[derive(Default)] pub struct Select { @@ -36,7 +34,9 @@ impl Select { } #[inline] - pub fn limit(&self) -> usize { self.items.len().min(5) } + pub(super) fn limit(&self) -> usize { + self.position.offset.height.saturating_sub(SELECT.border()) as usize + } } impl Select { diff --git a/yazi-core/src/tab/commands/cd.rs b/yazi-core/src/tab/commands/cd.rs index 74935103..457ea7f0 100644 --- a/yazi-core/src/tab/commands/cd.rs +++ b/yazi-core/src/tab/commands/cd.rs @@ -2,10 +2,10 @@ use std::{mem, time::Duration}; use tokio::{fs, pin}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; -use yazi_config::keymap::{Exec, KeymapLayer}; +use yazi_config::{keymap::{Exec, KeymapLayer}, popup::InputOpt}; use yazi_shared::{expand_path, Debounce, InputError, Url}; -use crate::{emit, input::InputOpt, tab::Tab}; +use crate::{emit, tab::Tab}; pub struct Opt { target: Url, @@ -65,11 +65,7 @@ impl Tab { let opt = opt.into() as Opt; tokio::spawn(async move { - let rx = emit!(Input( - InputOpt::top("Change directory:") - .with_value(opt.target.to_string_lossy()) - .with_completion() - )); + let rx = emit!(Input(InputOpt::cd().with_value(opt.target.to_string_lossy()))); let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); pin!(rx); diff --git a/yazi-core/src/tab/commands/find.rs b/yazi-core/src/tab/commands/find.rs index 6aa54e1f..53dd0ef4 100644 --- a/yazi-core/src/tab/commands/find.rs +++ b/yazi-core/src/tab/commands/find.rs @@ -2,10 +2,10 @@ use std::time::Duration; use tokio::pin; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; -use yazi_config::keymap::{Exec, KeymapLayer}; +use yazi_config::{keymap::{Exec, KeymapLayer}, popup::InputOpt}; use yazi_shared::{Debounce, InputError}; -use crate::{emit, input::InputOpt, tab::{Finder, FinderCase, Tab}}; +use crate::{emit, tab::{Finder, FinderCase, Tab}}; pub struct Opt<'a> { query: Option<&'a str>, @@ -39,9 +39,7 @@ impl Tab { pub fn find<'a>(&mut self, opt: impl Into>) -> bool { let opt = opt.into() as Opt; tokio::spawn(async move { - let rx = emit!(Input( - InputOpt::top(if opt.prev { "Find previous:" } else { "Find next:" }).with_realtime() - )); + let rx = emit!(Input(InputOpt::find(opt.prev))); let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); pin!(rx); diff --git a/yazi-core/src/tab/commands/search.rs b/yazi-core/src/tab/commands/search.rs index 93acade8..5e271900 100644 --- a/yazi-core/src/tab/commands/search.rs +++ b/yazi-core/src/tab/commands/search.rs @@ -3,9 +3,9 @@ use std::{mem, time::Duration}; use anyhow::bail; use tokio::pin; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; -use yazi_config::keymap::{Exec, KeymapLayer}; +use yazi_config::{keymap::{Exec, KeymapLayer}, popup::InputOpt}; -use crate::{emit, external, files::FilesOp, input::InputOpt, tab::Tab}; +use crate::{emit, external, files::FilesOp, tab::Tab}; pub struct Opt { pub type_: OptType, @@ -45,9 +45,7 @@ impl Tab { let hidden = self.conf.show_hidden; self.search = Some(tokio::spawn(async move { - let Some(Ok(subject)) = emit!(Input(InputOpt::top("Search:"))).recv().await else { - bail!("") - }; + let Some(Ok(subject)) = emit!(Input(InputOpt::search())).recv().await else { bail!("") }; cwd = cwd.into_search(subject.clone()); let rx = if opt.type_ == OptType::Rg { diff --git a/yazi-core/src/tab/commands/shell.rs b/yazi-core/src/tab/commands/shell.rs index 2fd1ba37..fb309e72 100644 --- a/yazi-core/src/tab/commands/shell.rs +++ b/yazi-core/src/tab/commands/shell.rs @@ -1,6 +1,6 @@ -use yazi_config::{keymap::Exec, open::Opener}; +use yazi_config::{keymap::Exec, open::Opener, popup::InputOpt}; -use crate::{emit, input::InputOpt, tab::Tab}; +use crate::{emit, tab::Tab}; pub struct Opt { cmd: String, @@ -29,11 +29,7 @@ impl Tab { let mut opt = opt.into() as Opt; tokio::spawn(async move { if !opt.confirm || opt.cmd.is_empty() { - let mut result = emit!(Input( - InputOpt::top(if opt.block { "Shell (block):" } else { "Shell:" }) - .with_value(opt.cmd) - .with_highlight() - )); + let mut result = emit!(Input(InputOpt::shell(opt.block).with_value(opt.cmd))); match result.recv().await { Some(Ok(e)) => opt.cmd = e, _ => return, diff --git a/yazi-core/src/tasks/tasks.rs b/yazi-core/src/tasks/tasks.rs index a7e90f25..765f69c0 100644 --- a/yazi-core/src/tasks/tasks.rs +++ b/yazi-core/src/tasks/tasks.rs @@ -2,11 +2,11 @@ use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, path::Path, syn use serde::Serialize; use tracing::debug; -use yazi_config::{manager::SortBy, open::Opener, OPEN}; +use yazi_config::{manager::SortBy, open::Opener, popup::InputOpt, OPEN}; use yazi_shared::{MimeKind, Term, Url}; use super::{running::Running, task::TaskSummary, Scheduler, TASKS_PADDING, TASKS_PERCENT}; -use crate::{emit, files::{File, Files}, input::InputOpt}; +use crate::{emit, files::{File, Files}}; pub struct Tasks { pub(super) scheduler: Arc, @@ -110,12 +110,11 @@ impl Tasks { let scheduler = self.scheduler.clone(); tokio::spawn(async move { - let s = if targets.len() > 1 { "s" } else { "" }; - let mut result = emit!(Input(InputOpt::hovered(if permanently { - format!("Delete {} selected file{s} permanently? (y/N)", targets.len()) + let mut result = emit!(Input(if permanently { + InputOpt::delete(targets.len()) } else { - format!("Move {} selected file{s} to trash? (y/N)", targets.len()) - }))); + InputOpt::trash(targets.len()) + })); if let Some(Ok(choice)) = result.recv().await { if choice != "y" && choice != "Y" { diff --git a/yazi-fm/src/completion/completion.rs b/yazi-fm/src/completion/completion.rs index 5243159f..f748ef77 100644 --- a/yazi-fm/src/completion/completion.rs +++ b/yazi-fm/src/completion/completion.rs @@ -1,8 +1,8 @@ use std::path::MAIN_SEPARATOR; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}}; -use yazi_config::THEME; -use yazi_core::{Ctx, Position}; +use yazi_config::{popup::{Offset, Position}, THEME}; +use yazi_core::Ctx; pub(crate) struct Completion<'a> { cx: &'a Ctx, @@ -39,15 +39,12 @@ impl<'a> Widget for Completion<'a> { .collect::>(); let input_area = self.cx.area(&self.cx.input.position); - let mut area = self.cx.area(&Position::Sticky( - Rect { - x: 1, - y: 0, - width: input_area.width.saturating_sub(2), - height: items.len() as u16 + 2, - }, - input_area, - )); + let mut area = Position::sticky(input_area, Offset { + x: 1, + y: 0, + width: input_area.width.saturating_sub(2), + height: items.len() as u16 + 2, + }); if area.y > input_area.y { area.y = area.y.saturating_sub(1);