feat: make Input and Select positions customizable (#361)

This commit is contained in:
Hanaasagi 2023-11-18 01:40:20 +08:00 committed by GitHub
parent 6a64b162be
commit bb2353c7b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 554 additions and 265 deletions

View file

@ -73,5 +73,61 @@ bizarre_retry = 5
[plugins] [plugins]
preload = [] 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] [log]
enabled = false enabled = false

View file

@ -9,6 +9,7 @@ pub mod manager;
pub mod open; pub mod open;
mod pattern; mod pattern;
pub mod plugins; pub mod plugins;
pub mod popup;
mod preset; mod preset;
pub mod preview; pub mod preview;
mod tasks; mod tasks;
@ -32,6 +33,8 @@ pub static PLUGINS: RoCell<plugins::Plugins> = RoCell::new();
pub static PREVIEW: RoCell<preview::Preview> = RoCell::new(); pub static PREVIEW: RoCell<preview::Preview> = RoCell::new();
pub static TASKS: RoCell<tasks::Tasks> = RoCell::new(); pub static TASKS: RoCell<tasks::Tasks> = RoCell::new();
pub static THEME: RoCell<theme::Theme> = RoCell::new(); pub static THEME: RoCell<theme::Theme> = RoCell::new();
pub static INPUT: RoCell<popup::Input> = RoCell::new();
pub static SELECT: RoCell<popup::Select> = RoCell::new();
pub static BOOT: RoCell<boot::Boot> = RoCell::new(); pub static BOOT: RoCell<boot::Boot> = RoCell::new();
@ -48,6 +51,8 @@ pub fn init() {
PREVIEW.with(Default::default); PREVIEW.with(Default::default);
TASKS.with(Default::default); TASKS.with(Default::default);
THEME.with(Default::default); THEME.with(Default::default);
INPUT.with(Default::default);
SELECT.with(Default::default);
BOOT.with(Default::default); BOOT.with(Default::default);
} }

View file

@ -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::<Outer>(&MERGED_YAZI).unwrap().input
}
}
impl Input {
#[inline]
pub const fn border(&self) -> u16 { 2 }
}

View file

@ -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::*;

View file

@ -0,0 +1,34 @@
use anyhow::bail;
use serde::Deserialize;
#[derive(Clone, Copy, Default, Deserialize)]
#[serde(try_from = "Vec<i16>")]
pub struct Offset {
pub x: i16,
pub y: i16,
pub width: u16,
pub height: u16,
}
impl TryFrom<Vec<i16>> for Offset {
type Error = anyhow::Error;
fn try_from(values: Vec<i16>) -> Result<Self, Self::Error> {
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,
})
}
}

View file

@ -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<String>,
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<String>) -> 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<String>) -> 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
}),
}
}
}

View file

@ -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,
}

View file

@ -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)),
}
}
}

View file

@ -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::<Outer>(&MERGED_YAZI).unwrap().select
}
}
impl Select {
#[inline]
pub const fn border(&self) -> u16 { 2 }
}

View file

@ -1,8 +1,7 @@
use crossterm::terminal::WindowSize;
use ratatui::prelude::Rect; 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 struct Ctx {
pub manager: Manager, pub manager: Manager,
@ -27,36 +26,18 @@ impl Ctx {
} }
} }
pub fn area(&self, pos: &Position) -> Rect { pub fn area(&self, position: &Position) -> Rect {
let WindowSize { columns, rows, .. } = Term::size(); if position.origin != Origin::Hovered {
return position.rect();
}
let (x, y) = match pos { if let Some(r) =
Position::Top(Rect { mut x, mut y, width, height }) => { self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url))
x = x.min(columns.saturating_sub(*width)); {
y = y.min(rows.saturating_sub(*height)); Position::sticky(r, position.offset)
((columns / 2).saturating_sub(width / 2) + x, y) } else {
} Position::new(Origin::TopCenter, position.offset).rect()
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)) }
} }
#[inline] #[inline]

View file

@ -3,10 +3,10 @@ use std::{collections::BTreeMap, ffi::OsString};
use anyhow::Result; use anyhow::Result;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use tokio::sync::{mpsc::{self, UnboundedSender}, oneshot}; 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 yazi_shared::{InputError, RoCell, Url};
use super::{files::FilesOp, input::InputOpt, select::SelectOpt}; use super::files::FilesOp;
use crate::{preview::PreviewLock, tasks::TasksProgress}; use crate::{preview::PreviewLock, tasks::TasksProgress};
static TX: RoCell<UnboundedSender<Event>> = RoCell::new(); static TX: RoCell<UnboundedSender<Event>> = RoCell::new();

View file

@ -30,7 +30,7 @@ impl Input {
} }
} }
} }
self.snaps.tag(); self.snaps.tag(self.limit());
true true
} }
} }

View file

@ -38,7 +38,7 @@ impl Input {
false, false,
); );
let snap = self.snap_mut(); let (limit, snap) = (self.limit(), self.snap_mut());
if snap.offset > snap.cursor { if snap.offset > snap.cursor {
snap.offset = snap.cursor; snap.offset = snap.cursor;
} else if snap.value.is_empty() { } else if snap.value.is_empty() {
@ -46,9 +46,9 @@ impl Input {
} else { } else {
let delta = snap.mode.delta(); let delta = snap.mode.delta();
let s = snap.slice(snap.offset..snap.cursor + 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::<String>(); let s = s.chars().rev().collect::<String>();
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);
} }
} }

View file

@ -2,10 +2,11 @@ use std::ops::Range;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use yazi_config::{popup::{InputOpt, Position}, INPUT};
use yazi_shared::InputError; use yazi_shared::InputError;
use super::{mode::InputMode, op::InputOp, InputOpt, InputSnap, InputSnaps}; use super::{mode::InputMode, op::InputOp, InputSnap, InputSnaps};
use crate::{external, Position}; use crate::external;
#[derive(Default)] #[derive(Default)]
pub struct Input { pub struct Input {
@ -28,9 +29,7 @@ pub struct Input {
impl Input { impl Input {
pub fn show(&mut self, opt: InputOpt, tx: UnboundedSender<Result<String, InputError>>) { pub fn show(&mut self, opt: InputOpt, tx: UnboundedSender<Result<String, InputError>>) {
self.close(false); self.close(false);
self.snaps.reset(opt.value);
self.visible = true; self.visible = true;
self.title = opt.title; self.title = opt.title;
self.position = opt.position; self.position = opt.position;
@ -41,6 +40,14 @@ impl Input {
// Shell // Shell
self.highlight = opt.highlight; 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 { pub fn type_str(&mut self, s: &str) -> bool {
@ -92,7 +99,7 @@ impl Input {
return false; return false;
} }
if !matches!(old.op, InputOp::None | InputOp::Select(_)) { 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 true
} }
@ -115,7 +122,7 @@ impl Input {
impl Input { impl Input {
#[inline] #[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] #[inline]
pub fn mode(&self) -> InputMode { self.snap().mode } pub fn mode(&self) -> InputMode { self.snap().mode }
@ -133,7 +140,7 @@ impl Input {
let (start, end) = let (start, end) =
if start < snap.cursor { (start, snap.cursor) } else { (snap.cursor + 1, start + 1) }; 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 Range { start, end } = start.max(win.start)..end.min(win.end);
let s = snap.slice(snap.offset..start).width() as u16; let s = snap.slice(snap.offset..start).width() as u16;

View file

@ -2,7 +2,6 @@ mod commands;
mod input; mod input;
mod mode; mod mode;
mod op; mod op;
mod option;
mod shell; mod shell;
mod snap; mod snap;
mod snaps; mod snaps;
@ -10,6 +9,5 @@ mod snaps;
pub use input::*; pub use input::*;
pub use mode::*; pub use mode::*;
use op::*; use op::*;
pub use option::*;
use snap::*; use snap::*;
use snaps::*; use snaps::*;

View file

@ -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<str>) -> 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<str>) -> 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<str>) -> 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
}
}

View file

@ -16,7 +16,7 @@ pub(super) struct InputSnap {
} }
impl InputSnap { impl InputSnap {
pub(super) fn new(value: String) -> Self { pub(super) fn new(value: String, limit: usize) -> Self {
let mut snap = Self { let mut snap = Self {
value, value,
@ -26,15 +26,15 @@ impl InputSnap {
offset: usize::MAX, offset: usize::MAX,
cursor: usize::MAX, cursor: usize::MAX,
}; };
snap.reset(); snap.reset(limit);
snap snap
} }
#[inline] #[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.cursor = self.cursor.min(self.value.chars().count().saturating_sub(self.mode.delta()));
self.offset = 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::<String>() } pub(super) fn rev(&self) -> String { self.value.chars().rev().collect::<String>() }
#[inline] #[inline]
pub(super) fn window(&self) -> Range<usize> { Self::find_window(&self.value, self.offset) } pub(super) fn window(&self, limit: usize) -> Range<usize> {
Self::find_window(&self.value, self.offset, limit)
}
#[inline] #[inline]
pub(super) fn find_window(s: &str, offset: usize) -> Range<usize> { pub(super) fn find_window(s: &str, offset: usize, limit: usize) -> Range<usize> {
let mut width = 0; let mut width = 0;
let v = s let v = s
.chars() .chars()
@ -76,7 +78,7 @@ impl InputSnap {
.skip(offset) .skip(offset)
.map_while(|(i, c)| { .map_while(|(i, c)| {
width += c.width().unwrap_or(0); width += c.width().unwrap_or(0);
if width < /*TODO: hardcode*/ 50 - 2 { Some(i) } else { None } if width < limit { Some(i) } else { None }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -11,13 +11,33 @@ pub(super) struct InputSnaps {
impl InputSnaps { impl InputSnaps {
#[inline] #[inline]
pub(super) fn reset(&mut self, value: String) { pub(super) fn reset(&mut self, value: String, limit: usize) {
self.idx = 0; self.idx = 0;
self.versions.clear(); self.versions.clear();
self.versions.push(InputSnap::new(value)); self.versions.push(InputSnap::new(value, limit));
self.current = self.versions[0].clone(); 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 { pub(super) fn undo(&mut self) -> bool {
if self.idx == 0 { if self.idx == 0 {
return false; return false;
@ -37,26 +57,6 @@ impl InputSnaps {
self.current = self.versions[self.idx].clone(); self.current = self.versions[self.idx].clone();
true 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 { impl InputSnaps {

View file

@ -16,7 +16,6 @@ pub mod help;
mod highlighter; mod highlighter;
pub mod input; pub mod input;
pub mod manager; pub mod manager;
mod position;
pub mod preview; pub mod preview;
pub mod select; pub mod select;
mod step; mod step;
@ -28,7 +27,6 @@ pub use blocker::*;
pub use context::*; pub use context::*;
pub use event::*; pub use event::*;
pub use highlighter::*; pub use highlighter::*;
pub use position::*;
pub use step::*; pub use step::*;
pub fn init() { init_blocker(); } pub fn init() { init_blocker(); }

View file

@ -1,10 +1,10 @@
use std::path::{PathBuf, MAIN_SEPARATOR}; use std::path::{PathBuf, MAIN_SEPARATOR};
use tokio::fs; use tokio::fs;
use yazi_config::keymap::Exec; use yazi_config::{keymap::Exec, popup::InputOpt};
use yazi_shared::Url; 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 { pub struct Opt {
force: bool, force: bool,
@ -19,14 +19,14 @@ impl Manager {
let opt = opt.into() as Opt; let opt = opt.into() as Opt;
let cwd = self.cwd().to_owned(); let cwd = self.cwd().to_owned();
tokio::spawn(async move { 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 { let Some(Ok(name)) = result.recv().await else {
return Ok(()); return Ok(());
}; };
let path = cwd.join(&name); let path = cwd.join(&name);
if !opt.force && fs::symlink_metadata(&path).await.is_ok() { 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" => (), Some(Ok(c)) if c == "y" || c == "Y" => (),
_ => return Ok(()), _ => return Ok(()),
} }

View file

@ -1,9 +1,9 @@
use std::ffi::OsString; use std::ffi::OsString;
use yazi_config::{keymap::Exec, OPEN}; use yazi_config::{keymap::Exec, popup::SelectOpt, OPEN};
use yazi_shared::MIME_DIR; use yazi_shared::MIME_DIR;
use crate::{emit, external, manager::Manager, select::SelectOpt}; use crate::{emit, external, manager::Manager};
pub struct Opt { pub struct Opt {
interactive: bool, interactive: bool,
@ -20,11 +20,7 @@ impl Manager {
return; return;
} }
let result = emit!(Select(SelectOpt::hovered( let result = emit!(Select(SelectOpt::open(openers.iter().map(|o| o.desc.clone()).collect())));
"Open with:",
openers.iter().map(|o| o.desc.clone()).collect()
)));
if let Ok(choice) = result.await { if let Ok(choice) = result.await {
emit!(Open(files, Some(openers[choice].clone()))); emit!(Open(files, Some(openers[choice].clone())));
} }

View file

@ -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)] #[derive(Default)]
pub struct Opt { pub struct Opt {
@ -24,9 +24,7 @@ impl Manager {
} }
tokio::spawn(async move { tokio::spawn(async move {
let mut result = let mut result = emit!(Input(InputOpt::quit(tasks)));
emit!(Input(InputOpt::top(format!("{tasks} tasks running, sure to quit? (y/N)"))));
if let Some(Ok(choice)) = result.recv().await { if let Some(Ok(choice)) = result.recv().await {
if choice == "y" || choice == "Y" { if choice == "y" || choice == "Y" {
emit!(Quit(opt.no_cwd_file)); emit!(Quit(opt.no_cwd_file));

View file

@ -2,10 +2,10 @@ use std::{collections::BTreeSet, ffi::OsStr, io::{stdout, BufWriter, Write}, pat
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; 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 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 { pub struct Opt {
force: bool, force: bool,
@ -42,9 +42,8 @@ impl Manager {
let opt = opt.into() as Opt; let opt = opt.into() as Opt;
tokio::spawn(async move { tokio::spawn(async move {
let mut result = emit!(Input( let mut result =
InputOpt::hovered("Rename:").with_value(hovered.file_name().unwrap().to_string_lossy()) emit!(Input(InputOpt::rename().with_value(hovered.file_name().unwrap().to_string_lossy())));
));
let Some(Ok(name)) = result.recv().await else { let Some(Ok(name)) = result.recv().await else {
return; return;
@ -56,7 +55,7 @@ impl Manager {
return; 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 let Some(Ok(choice)) = result.recv().await {
if choice == "y" || choice == "Y" { if choice == "y" || choice == "Y" {
Self::rename_and_hover(hovered, Url::from(new)).await.ok(); Self::rename_and_hover(hovered, Url::from(new)).await.ok();

View file

@ -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) }
}

View file

@ -1,8 +1,4 @@
mod commands; mod commands;
mod option;
mod select; mod select;
pub use option::*;
pub use select::*; pub use select::*;
pub const SELECT_PADDING: u16 = 2;

View file

@ -1,32 +0,0 @@
use ratatui::prelude::Rect;
use crate::Position;
pub struct SelectOpt {
pub title: String,
pub items: Vec<String>,
pub position: Position,
}
impl SelectOpt {
pub fn top(title: &str, items: Vec<String>) -> 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<String>) -> 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 },
),
}
}
}

View file

@ -1,8 +1,6 @@
use anyhow::Result; use anyhow::Result;
use tokio::sync::oneshot::Sender; use tokio::sync::oneshot::Sender;
use yazi_config::{popup::{Position, SelectOpt}, SELECT};
use super::SelectOpt;
use crate::Position;
#[derive(Default)] #[derive(Default)]
pub struct Select { pub struct Select {
@ -36,7 +34,9 @@ impl Select {
} }
#[inline] #[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 { impl Select {

View file

@ -2,10 +2,10 @@ use std::{mem, time::Duration};
use tokio::{fs, pin}; use tokio::{fs, pin};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; 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 yazi_shared::{expand_path, Debounce, InputError, Url};
use crate::{emit, input::InputOpt, tab::Tab}; use crate::{emit, tab::Tab};
pub struct Opt { pub struct Opt {
target: Url, target: Url,
@ -65,11 +65,7 @@ impl Tab {
let opt = opt.into() as Opt; let opt = opt.into() as Opt;
tokio::spawn(async move { tokio::spawn(async move {
let rx = emit!(Input( let rx = emit!(Input(InputOpt::cd().with_value(opt.target.to_string_lossy())));
InputOpt::top("Change directory:")
.with_value(opt.target.to_string_lossy())
.with_completion()
));
let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50));
pin!(rx); pin!(rx);

View file

@ -2,10 +2,10 @@ use std::time::Duration;
use tokio::pin; use tokio::pin;
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; 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 yazi_shared::{Debounce, InputError};
use crate::{emit, input::InputOpt, tab::{Finder, FinderCase, Tab}}; use crate::{emit, tab::{Finder, FinderCase, Tab}};
pub struct Opt<'a> { pub struct Opt<'a> {
query: Option<&'a str>, query: Option<&'a str>,
@ -39,9 +39,7 @@ impl Tab {
pub fn find<'a>(&mut self, opt: impl Into<Opt<'a>>) -> bool { pub fn find<'a>(&mut self, opt: impl Into<Opt<'a>>) -> bool {
let opt = opt.into() as Opt; let opt = opt.into() as Opt;
tokio::spawn(async move { tokio::spawn(async move {
let rx = emit!(Input( let rx = emit!(Input(InputOpt::find(opt.prev)));
InputOpt::top(if opt.prev { "Find previous:" } else { "Find next:" }).with_realtime()
));
let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50));
pin!(rx); pin!(rx);

View file

@ -3,9 +3,9 @@ use std::{mem, time::Duration};
use anyhow::bail; use anyhow::bail;
use tokio::pin; use tokio::pin;
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; 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 struct Opt {
pub type_: OptType, pub type_: OptType,
@ -45,9 +45,7 @@ impl Tab {
let hidden = self.conf.show_hidden; let hidden = self.conf.show_hidden;
self.search = Some(tokio::spawn(async move { self.search = Some(tokio::spawn(async move {
let Some(Ok(subject)) = emit!(Input(InputOpt::top("Search:"))).recv().await else { let Some(Ok(subject)) = emit!(Input(InputOpt::search())).recv().await else { bail!("") };
bail!("")
};
cwd = cwd.into_search(subject.clone()); cwd = cwd.into_search(subject.clone());
let rx = if opt.type_ == OptType::Rg { let rx = if opt.type_ == OptType::Rg {

View file

@ -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 { pub struct Opt {
cmd: String, cmd: String,
@ -29,11 +29,7 @@ impl Tab {
let mut opt = opt.into() as Opt; let mut opt = opt.into() as Opt;
tokio::spawn(async move { tokio::spawn(async move {
if !opt.confirm || opt.cmd.is_empty() { if !opt.confirm || opt.cmd.is_empty() {
let mut result = emit!(Input( let mut result = emit!(Input(InputOpt::shell(opt.block).with_value(opt.cmd)));
InputOpt::top(if opt.block { "Shell (block):" } else { "Shell:" })
.with_value(opt.cmd)
.with_highlight()
));
match result.recv().await { match result.recv().await {
Some(Ok(e)) => opt.cmd = e, Some(Ok(e)) => opt.cmd = e,
_ => return, _ => return,

View file

@ -2,11 +2,11 @@ use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, path::Path, syn
use serde::Serialize; use serde::Serialize;
use tracing::debug; 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 yazi_shared::{MimeKind, Term, Url};
use super::{running::Running, task::TaskSummary, Scheduler, TASKS_PADDING, TASKS_PERCENT}; 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 struct Tasks {
pub(super) scheduler: Arc<Scheduler>, pub(super) scheduler: Arc<Scheduler>,
@ -110,12 +110,11 @@ impl Tasks {
let scheduler = self.scheduler.clone(); let scheduler = self.scheduler.clone();
tokio::spawn(async move { tokio::spawn(async move {
let s = if targets.len() > 1 { "s" } else { "" }; let mut result = emit!(Input(if permanently {
let mut result = emit!(Input(InputOpt::hovered(if permanently { InputOpt::delete(targets.len())
format!("Delete {} selected file{s} permanently? (y/N)", targets.len())
} else { } 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 let Some(Ok(choice)) = result.recv().await {
if choice != "y" && choice != "Y" { if choice != "y" && choice != "Y" {

View file

@ -1,8 +1,8 @@
use std::path::MAIN_SEPARATOR; use std::path::MAIN_SEPARATOR;
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}}; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}};
use yazi_config::THEME; use yazi_config::{popup::{Offset, Position}, THEME};
use yazi_core::{Ctx, Position}; use yazi_core::Ctx;
pub(crate) struct Completion<'a> { pub(crate) struct Completion<'a> {
cx: &'a Ctx, cx: &'a Ctx,
@ -39,15 +39,12 @@ impl<'a> Widget for Completion<'a> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let input_area = self.cx.area(&self.cx.input.position); let input_area = self.cx.area(&self.cx.input.position);
let mut area = self.cx.area(&Position::Sticky( let mut area = Position::sticky(input_area, Offset {
Rect { x: 1,
x: 1, y: 0,
y: 0, width: input_area.width.saturating_sub(2),
width: input_area.width.saturating_sub(2), height: items.len() as u16 + 2,
height: items.len() as u16 + 2, });
},
input_area,
));
if area.y > input_area.y { if area.y > input_area.y {
area.y = area.y.saturating_sub(1); area.y = area.y.saturating_sub(1);