feat: custom input box position

Close: #351
This commit is contained in:
Hanaasagi 2023-11-12 19:53:13 +09:00 committed by sxyazi
parent ab7acfec5c
commit 564af8827e
No known key found for this signature in database
19 changed files with 339 additions and 68 deletions

View file

@ -16,6 +16,25 @@ cache_dir = ""
ueberzug_scale = 1
ueberzug_offset = [ 0, 0, 0, 0 ]
[input]
cd_position = "top"
cd_offset = [0, 2, 50, 3]
search_position = "top"
search_offset = [0, 2, 50, 3]
find_position = "top"
find_offset = [0, 2, 50, 3]
shell_position = "top"
shell_offset = [0, 2, 50, 3]
create_position = "top"
create_offset = [0, 2, 50, 3]
rename_position = "hovered"
rename_offset = [0, 1, 50, 3]
[opener]
edit = [
{ exec = '$EDITOR "$@"', block = true, for = "unix" },

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,7 @@ 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 INPUTBOX: RoCell<popup::Input> = RoCell::new();
pub static BOOT: RoCell<boot::Boot> = RoCell::new();
@ -48,6 +50,7 @@ pub fn init() {
PREVIEW.with(Default::default);
TASKS.with(Default::default);
THEME.with(Default::default);
INPUTBOX.with(Default::default);
BOOT.with(Default::default);
}

View file

@ -0,0 +1,38 @@
use serde::Deserialize;
use super::position::{Offset, Position};
use crate::MERGED_YAZI;
#[derive(Debug, Deserialize)]
pub struct Input {
// cd
pub cd_position: Position,
pub cd_offset: Offset,
// search
pub search_position: Position,
pub search_offset: Offset,
// find
pub find_position: Position,
pub find_offset: Offset,
// shell
pub shell_position: Position,
pub shell_offset: Offset,
// create
pub create_position: Position,
pub create_offset: Offset,
// rename
pub rename_position: Position,
pub rename_offset: Offset,
}
impl Default for Input {
fn default() -> Self {
#[derive(Deserialize)]
struct Outer {
input: Input,
}
// TODO:
toml::from_str::<Outer>(&MERGED_YAZI).unwrap().input
}
}

View file

@ -0,0 +1,7 @@
mod input;
mod position;
mod select;
pub use input::*;
pub use position::*;
pub use select::*;

View file

@ -0,0 +1,51 @@
use anyhow::bail;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub enum Position {
#[serde(rename = "top-left")]
TopLeft,
#[serde(rename = "top-right")]
TopRight,
#[serde(rename = "top")]
Top,
#[serde(rename = "center")]
Center,
#[serde(rename = "bottom")]
Bottom,
#[serde(rename = "bottom-left")]
BottomLeft,
#[serde(rename = "bottom-right")]
BottomRight,
#[serde(rename = "hovered")]
Hovered,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[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!("invalid offset: {:?}", values);
}
if values[2] < 0 || values[3] < 0 {
bail!("invalid offset: {:?}", values);
}
Ok(Self {
x: values[0],
y: values[1],
width: values[2] as u16,
height: values[3] as u16,
})
}
}

View file

@ -0,0 +1 @@
// TODO:

View file

@ -2,7 +2,7 @@ use crossterm::terminal::WindowSize;
use ratatui::prelude::Rect;
use yazi_shared::Term;
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, Position, RectShim};
pub struct Ctx {
pub manager: Manager,
@ -30,29 +30,114 @@ impl Ctx {
pub fn area(&self, pos: &Position) -> Rect {
let WindowSize { columns, rows, .. } = Term::size();
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)
let (x, y) = match *pos {
Position::TopLeft(RectShim { x_offset, y_offset, width, height }) => {
let right_max = columns.saturating_sub(width);
let bottom_max = rows.saturating_sub(height);
let base_x = 0_u16;
let base_y = 0_u16;
let x = base_x.saturating_add_signed(x_offset).max(0).min(right_max);
let y = base_y.saturating_add_signed(y_offset).max(0).min(bottom_max);
(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::TopRight(RectShim { x_offset, y_offset, width, height }) => {
let right_max = columns.saturating_sub(width);
let bottom_max = rows.saturating_sub(height);
let base_x = right_max;
let base_y = 0_u16;
let x = base_x.saturating_add_signed(x_offset).max(0).min(right_max);
let y = base_y.saturating_add_signed(y_offset).max(0).min(bottom_max);
(x, y)
}
Position::Hovered(rect) => {
Position::Top(RectShim { x_offset, y_offset, width, height }) => {
let right_max = columns.saturating_sub(width);
let bottom_max = rows.saturating_sub(height);
let base_x = (columns / 2).saturating_sub(width / 2);
let base_y = 0_u16;
let x = base_x.saturating_add_signed(x_offset).max(0).min(right_max);
let y = base_y.saturating_add_signed(y_offset).max(0).min(bottom_max);
(x, y)
}
Position::Center(RectShim { x_offset, y_offset, width, height }) => {
let right_max = columns.saturating_sub(width);
let bottom_max = rows.saturating_sub(height);
let base_x = (columns / 2).saturating_sub(width / 2);
let base_y = (bottom_max / 2).saturating_sub(height / 2);
let x = base_x.saturating_add_signed(x_offset).max(0).min(right_max);
let y = base_y.saturating_add_signed(y_offset).max(0).min(bottom_max);
(x, y)
}
Position::Bottom(RectShim { x_offset, y_offset, width, height }) => {
let right_max = columns.saturating_sub(width);
let bottom_max = rows.saturating_sub(height);
let base_x = (columns / 2).saturating_sub(width / 2);
let base_y = bottom_max;
let x = base_x.saturating_add_signed(x_offset).max(0).min(right_max);
let y = base_y.saturating_add_signed(y_offset).max(0).min(bottom_max);
(x, y)
}
Position::BottomLeft(RectShim { x_offset, y_offset, width, height }) => {
let right_max = columns.saturating_sub(width);
let bottom_max = rows.saturating_sub(height);
let base_x = 0_u16;
let base_y = bottom_max;
let x = base_x.saturating_add_signed(x_offset).max(0).min(right_max);
let y = base_y.saturating_add_signed(y_offset).max(0).min(bottom_max);
(x, y)
}
Position::BottomRight(RectShim { x_offset, y_offset, width, height }) => {
let right_max = columns.saturating_sub(width);
let bottom_max = rows.saturating_sub(height);
let base_x = right_max;
let base_y = bottom_max;
let x = base_x.saturating_add_signed(x_offset).max(0).min(right_max);
let y = base_y.saturating_add_signed(y_offset).max(0).min(bottom_max);
(x, y)
}
Position::Hovered(rect_shim) => {
return self.area(&if let Some(r) =
self.manager.hovered().and_then(|h| self.manager.current().rect_current(&h.url))
{
Position::Sticky(*rect, r)
Position::Sticky(rect_shim, r)
} else {
Position::Top(*rect)
Position::Top(rect_shim)
});
}
Position::Sticky(RectShim { x_offset, y_offset, width, height }, r) => {
// TODO:
unimplemented!("Position::Sticky is not implemented");
// let mut x = columns.saturating_sub(width);
// x = x.saturating_add_signed(x_offset);
// let mut y = r.y;
// y = y.saturating_add_signed(y_offset);
// if y + height + r.height > rows {
// (x + r.x, r.y.saturating_sub(height.saturating_sub(y)))
// } else {
// (x + r.x, y + r.height)
// }
}
};
let (w, h) = pos.dimension();

View file

@ -1,6 +1,6 @@
use ratatui::prelude::Rect;
use yazi_config::popup::{Offset as CfgOffset, Position as CfgPosition};
use crate::Position;
use crate::{Position, RectShim};
#[derive(Default)]
pub struct InputOpt {
@ -12,23 +12,48 @@ pub struct InputOpt {
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()
macro_rules! gen_method {
($func_name:ident, $position:ident) => {
pub fn $func_name(title: impl AsRef<str>, rect: RectShim) -> InputOpt {
InputOpt {
title: title.as_ref().to_owned(),
position: Position::$position(rect),
..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()
impl InputOpt {
gen_method!(top_left, TopLeft);
gen_method!(top_right, TopRight);
gen_method!(top, Top);
gen_method!(center, Center);
gen_method!(bottom, Bottom);
gen_method!(bottom_left, BottomLeft);
gen_method!(bottom_right, BottomRight);
gen_method!(hovered, Hovered);
pub fn from_cfg(title: impl AsRef<str>, pos: &CfgPosition, rect: &CfgOffset) -> Self {
let rect =
RectShim { x_offset: rect.x, y_offset: rect.y, width: rect.width, height: rect.height };
match pos {
CfgPosition::TopLeft => Self::top_left(title, rect),
CfgPosition::TopRight => Self::top_right(title, rect),
CfgPosition::Top => Self::top(title, rect),
CfgPosition::Center => Self::center(title, rect),
CfgPosition::Bottom => Self::bottom(title, rect),
CfgPosition::BottomLeft => Self::bottom_left(title, rect),
CfgPosition::BottomRight => Self::bottom_right(title, rect),
CfgPosition::Hovered => Self::hovered(title, rect),
}
}

View file

@ -19,14 +19,17 @@ 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::top("Create:", Default::default())));
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::top("Overwrite an existing file? (y/N)", Default::default())))
.recv()
.await
{
Some(Ok(c)) if c == "y" || c == "Y" => (),
_ => return Ok(()),
}

View file

@ -24,8 +24,10 @@ 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::top(
format!("{tasks} tasks running, sure to quit? (y/N)",),
Default::default()
)));
if let Some(Ok(choice)) = result.recv().await {
if choice == "y" || choice == "Y" {

View file

@ -2,7 +2,7 @@ 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, INPUTBOX, 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};
@ -43,7 +43,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())
InputOpt::from_cfg("Rename:", &INPUTBOX.rename_position, &INPUTBOX.rename_offset)
.with_value(hovered.file_name().unwrap().to_string_lossy())
));
let Some(Ok(name)) = result.recv().await else {
@ -56,7 +57,11 @@ impl Manager {
return;
}
let mut result = emit!(Input(InputOpt::hovered("Overwrite an existing file? (y/N)")));
let mut result = emit!(Input(InputOpt::from_cfg(
"Overwrite an existing file? (y/N)",
&INPUTBOX.rename_position,
&INPUTBOX.rename_offset
)));
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,22 +1,47 @@
use ratatui::prelude::Rect;
#[derive(Debug, Clone, Copy)]
pub struct RectShim {
pub x_offset: i16,
pub y_offset: i16,
pub width: u16,
pub height: u16,
}
impl Default for RectShim {
fn default() -> Self { Self { x_offset: 0, y_offset: 2, width: 50, height: 3 } }
}
#[derive(Debug, Clone)]
pub enum Position {
Top(Rect),
Sticky(Rect, Rect),
Hovered(Rect),
TopLeft(RectShim),
TopRight(RectShim),
Top(RectShim),
Center(RectShim),
Bottom(RectShim),
BottomLeft(RectShim),
BottomRight(RectShim),
Hovered(RectShim),
Sticky(RectShim, Rect),
}
impl Default for Position {
fn default() -> Self { Self::Top(Rect::default()) }
fn default() -> Self { Self::Top(RectShim::default()) }
}
impl Position {
#[inline]
pub fn rect(&self) -> Rect {
pub fn rect(&self) -> &RectShim {
match self {
Position::Top(rect) => *rect,
Position::Sticky(rect, _) => *rect,
Position::Hovered(rect) => *rect,
Position::TopLeft(rect) => rect,
Position::TopRight(rect) => rect,
Position::Top(rect) => rect,
Position::Center(rect) => rect,
Position::Bottom(rect) => rect,
Position::BottomLeft(rect) => rect,
Position::BottomRight(rect) => rect,
Position::Hovered(rect) => rect,
Position::Sticky(rect, _) => rect,
}
}

View file

@ -1,6 +1,6 @@
use ratatui::prelude::Rect;
use crate::Position;
use crate::{Position, RectShim};
pub struct SelectOpt {
pub title: String,
@ -14,7 +14,10 @@ impl SelectOpt {
Self {
title: title.to_owned(),
items,
position: Position::Top(/* TODO: hardcode */ Rect { x: 0, y: 2, width: 50, height }),
position: Position::Top(
// TODO:
RectShim { x_offset: 0, y_offset: 2, width: 50, height },
),
}
}
@ -24,8 +27,8 @@ impl SelectOpt {
title: title.to_owned(),
items,
position: Position::Hovered(
// TODO: hardcode
Rect { x: 0, y: 1, width: 50, height },
// TODO:
RectShim { x_offset: 0, y_offset: 1, width: 50, height },
),
}
}

View file

@ -2,7 +2,7 @@ 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}, INPUTBOX};
use yazi_shared::{expand_path, Debounce, InputError, Url};
use crate::{emit, input::InputOpt, tab::Tab};
@ -66,7 +66,7 @@ impl Tab {
tokio::spawn(async move {
let rx = emit!(Input(
InputOpt::top("Change directory:")
InputOpt::from_cfg("Change directory:", &INPUTBOX.cd_position, &INPUTBOX.cd_offset)
.with_value(opt.target.to_string_lossy())
.with_completion()
));

View file

@ -2,7 +2,7 @@ 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}, INPUTBOX};
use yazi_shared::{Debounce, InputError};
use crate::{emit, input::InputOpt, tab::{Finder, FinderCase, Tab}};
@ -39,8 +39,9 @@ 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 title = if opt.prev { "Find previous:" } else { "Find next:" };
let rx = emit!(Input(
InputOpt::top(if opt.prev { "Find previous:" } else { "Find next:" }).with_realtime()
InputOpt::from_cfg(title, &INPUTBOX.find_position, &INPUTBOX.find_offset).with_realtime()
));
let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50));

View file

@ -3,7 +3,7 @@ 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}, INPUTBOX};
use crate::{emit, external, files::FilesOp, input::InputOpt, tab::Tab};
@ -45,7 +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 {
let Some(Ok(subject)) = emit!(Input(InputOpt::from_cfg("Search:", &INPUTBOX.cd_position, &INPUTBOX.cd_offset))).recv().await else {
bail!("")
};

View file

@ -1,4 +1,4 @@
use yazi_config::{keymap::Exec, open::Opener};
use yazi_config::{keymap::Exec, open::Opener, INPUTBOX};
use crate::{emit, input::InputOpt, tab::Tab};
@ -29,8 +29,9 @@ impl Tab {
let mut opt = opt.into() as Opt;
tokio::spawn(async move {
if !opt.confirm || opt.cmd.is_empty() {
let title = if opt.block { "Shell (block):" } else { "Shell:" };
let mut result = emit!(Input(
InputOpt::top(if opt.block { "Shell (block):" } else { "Shell:" })
InputOpt::from_cfg(title, &INPUTBOX.shell_position, &INPUTBOX.shell_offset)
.with_value(opt.cmd)
.with_highlight()
));

View file

@ -111,11 +111,12 @@ 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 {
let prompt = if permanently {
format!("Delete {} selected file{s} permanently? (y/N)", targets.len())
} else {
format!("Move {} selected file{s} to trash? (y/N)", targets.len())
})));
};
let mut result = emit!(Input(InputOpt::hovered(prompt, Default::default())));
if let Some(Ok(choice)) = result.recv().await {
if choice != "y" && choice != "Y" {

View file

@ -2,7 +2,7 @@ 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_core::{Ctx, Position, RectShim};
pub(crate) struct Completion<'a> {
cx: &'a Ctx,
@ -40,11 +40,12 @@ impl<'a> Widget for Completion<'a> {
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,
// TODO:
RectShim {
x_offset: 1,
y_offset: 0,
width: input_area.width.saturating_sub(2),
height: items.len() as u16 + 2,
},
input_area,
));