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]
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

View file

@ -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<plugins::Plugins> = RoCell::new();
pub static PREVIEW: RoCell<preview::Preview> = RoCell::new();
pub static TASKS: RoCell<tasks::Tasks> = 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();
@ -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);
}

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 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]

View file

@ -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<UnboundedSender<Event>> = RoCell::new();

View file

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

View file

@ -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::<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 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<Result<String, InputError>>) {
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;

View file

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

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 {
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::<String>() }
#[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]
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 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::<Vec<_>>();

View file

@ -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 {

View file

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

View file

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

View file

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

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)]
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));

View file

@ -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();

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 option;
mod select;
pub use option::*;
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 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 {

View file

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

View file

@ -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<Opt<'a>>) -> 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);

View file

@ -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 {

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 {
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,

View file

@ -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<Scheduler>,
@ -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" {

View file

@ -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::<Vec<_>>();
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);