Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion yazi-actor/src/input/close.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ impl Actor for Close {

if let Some(tx) = input.tx.take() {
let value = input.snap().value.clone();
_ = tx.send(if form.submit { InputEvent::Submit(value) } else { InputEvent::Cancel(value) });
if form.submit {
if !input.obscure {
input.history().push(value.clone());
}
_ = tx.send(InputEvent::Submit(value));
} else {
_ = tx.send(InputEvent::Cancel(value));
}
}

act!(cmp:close, cx)?;
Expand Down
17 changes: 17 additions & 0 deletions yazi-actor/src/input/history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use anyhow::Result;
use yazi_shared::data::Data;
use yazi_widgets::input::parser::HistoryOpt;

use crate::{Actor, Ctx};

pub struct History;

impl Actor for History {
type Form = HistoryOpt;

const NAME: &str = "history";

fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
cx.input.navigate_history(form)
}
}
2 changes: 1 addition & 1 deletion yazi-actor/src/input/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
yazi_macro::mod_flat!(close complete escape show);
yazi_macro::mod_flat!(close complete escape history show);
8 changes: 8 additions & 0 deletions yazi-config/preset/keymap-default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ keymap = [
{ on = "p", run = "paste", desc = "Paste copied characters after the cursor" },
{ on = "P", run = "paste --before", desc = "Paste copied characters before the cursor" },

# History
{ on = "<Up>", run = "history -1", desc = "Navigate to the previous history entry" },
{ on = "<Down>", run = "history +1", desc = "Navigate to the next history entry" },
{ on = "<C-p>", run = "history -1", desc = "Navigate to the previous history entry" },
{ on = "<C-n>", run = "history +1", desc = "Navigate to the next history entry" },
{ on = "k", run = "history -1", desc = "Navigate to the previous history entry" },
{ on = "j", run = "history +1", desc = "Navigate to the next history entry" },

# Undo/Redo/Casefy
{ on = "u", run = [ "undo", "casefy lower" ], desc = "Undo, or lowercase if in visual mode" },
{ on = "U", run = "casefy upper", desc = "Uppercase" },
Expand Down
10 changes: 9 additions & 1 deletion yazi-config/src/popup/options.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use ratatui::{text::{Line, Text}, widgets::{Paragraph, Wrap}};
use yazi_shared::{scheme::Encode as EncodeScheme, strand::ToStrand, url::{Url, UrlBuf}};
use yazi_shared::{SStr, scheme::Encode as EncodeScheme, strand::ToStrand, url::{Url, UrlBuf}};

use super::{Offset, Position};
use crate::{YAZI, popup::Origin};

#[derive(Clone, Debug, Default)]
pub struct InputCfg {
pub id: SStr,
pub title: String,
pub value: String,
pub cursor: Option<usize>,
Expand Down Expand Up @@ -33,6 +34,7 @@ pub struct ConfirmCfg {
impl InputCfg {
pub fn cd(cwd: Url) -> Self {
Self {
id: "cd".into(),
title: YAZI.input.cd_title.clone(),
value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() },
position: Position::new(YAZI.input.cd_origin, YAZI.input.cd_offset),
Expand All @@ -43,6 +45,7 @@ impl InputCfg {

pub fn create(dir: bool) -> Self {
Self {
id: "create".into(),
title: YAZI.input.create_title[dir as usize].clone(),
position: Position::new(YAZI.input.create_origin, YAZI.input.create_offset),
..Default::default()
Expand All @@ -51,6 +54,7 @@ impl InputCfg {

pub fn rename() -> Self {
Self {
id: "rename".into(),
title: YAZI.input.rename_title.clone(),
position: Position::new(YAZI.input.rename_origin, YAZI.input.rename_offset),
..Default::default()
Expand All @@ -59,6 +63,7 @@ impl InputCfg {

pub fn filter() -> Self {
Self {
id: "filter".into(),
title: YAZI.input.filter_title.clone(),
position: Position::new(YAZI.input.filter_origin, YAZI.input.filter_offset),
realtime: true,
Expand All @@ -78,13 +83,15 @@ impl InputCfg {
pub fn search(name: &str) -> Self {
Self {
title: YAZI.input.search_title.replace("{n}", name),
id: "search".into(),
position: Position::new(YAZI.input.search_origin, YAZI.input.search_offset),
..Default::default()
}
}

pub fn shell(block: bool) -> Self {
Self {
id: "shell".into(),
title: YAZI.input.shell_title[block as usize].clone(),
position: Position::new(YAZI.input.shell_origin, YAZI.input.shell_offset),
..Default::default()
Expand All @@ -93,6 +100,7 @@ impl InputCfg {

pub fn tab_rename() -> Self {
Self {
id: "tab_rename".into(),
title: "Rename tab:".to_owned(),
position: Position::new(Origin::TopCenter, Offset {
x: 0,
Expand Down
90 changes: 90 additions & 0 deletions yazi-core/src/input/history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use std::{collections::VecDeque, mem};

use yazi_widgets::input::InputSnaps;

// TODO: make configurable?
const MAX_LENGTH: usize = 20;

#[derive(Default)]
pub struct InputHistory {
entries: VecDeque<String>,
entry_snaps: VecDeque<Option<InputSnaps>>,
idx: Option<usize>,
draft: Option<InputSnaps>,
}

impl InputHistory {
pub const fn new() -> Self {
Self {
entries: VecDeque::new(),
entry_snaps: VecDeque::new(),
idx: None,
draft: None,
}
}

pub fn push(&mut self, value: String) {
if value.is_empty() {
return;
}
if self.entries.back().map(String::as_str) != Some(&value) {
if self.entries.len() >= MAX_LENGTH {
self.entries.pop_front();
self.entry_snaps.pop_front();
}
self.entries.push_back(value);
self.entry_snaps.push_back(None);
}
self.reset();
}

pub fn reset(&mut self) {
self.idx = None;
self.draft = None;
}

pub fn navigate(&mut self, step: i64, snaps: &mut InputSnaps, limit: usize) -> bool {
if self.entries.is_empty() || step == 0 {
return false;
}

let len = self.entries.len() as i64;
let pos = self.idx.map_or(len, |i| i as i64);
let new_pos = (pos + step).clamp(0, len);

if new_pos == pos {
return false;
}

let mode = snaps.current().mode;
let cursor = snaps.current().cursor;

// Save current snaps into draft or the slot we're leaving
let old = mem::take(snaps);
if let Some(old_idx) = self.idx {
self.entry_snaps[old_idx] = Some(old);
} else {
self.draft = Some(old);
}

// Load target snaps
*snaps = if new_pos == len {
self.idx = None;
self.draft.take().unwrap_or_default()
} else {
let new_idx = new_pos as usize;
self.idx = Some(new_idx);
if self.entry_snaps[new_idx].is_none() {
let value = self.entries[new_idx].clone();
// history does not trigger on obscured inputs
self.entry_snaps[new_idx] = Some(InputSnaps::new(value, false, limit));
}
self.entry_snaps[new_idx].take().unwrap()
};

// Preserve mode and cursor position from before navigation
snaps.update_current(mode, cursor, limit);

true
}
}
29 changes: 29 additions & 0 deletions yazi-core/src/input/input.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,45 @@
use std::ops::{Deref, DerefMut};

use anyhow::Result;
use yazi_config::popup::Position;
use yazi_macro::{render, succ};
use yazi_shared::{data::Data, SStr};
use yazi_widgets::input::{InputOp, parser::HistoryOpt};
use crate::input::InputHistory;

#[derive(Default)]
pub struct Input {
pub(super) inner: yazi_widgets::input::Input,
pub history: std::collections::HashMap<SStr, InputHistory>,

pub visible: bool,
pub title: String,
pub position: Position,
}

impl Input {
pub fn history(&mut self) -> &mut InputHistory {
if !self.history.contains_key(&self.inner.id) {
self.history.insert(self.inner.id.clone(), InputHistory::new());
}
self.history.get_mut(&self.inner.id).unwrap()
}
pub fn navigate_history(&mut self, opt: HistoryOpt) -> Result<Data> {
if self.inner.snap().op != InputOp::None || self.inner.obscure {
succ!();
}
match self.history.get_mut(&self.inner.id) {
Some(history) => {
if !history.navigate(opt.offset, &mut self.inner.snaps, self.inner.limit) {
succ!();
}
}
None => succ!(),
}
succ!(render!());
}
}

impl Deref for Input {
type Target = yazi_widgets::input::Input;

Expand Down
2 changes: 1 addition & 1 deletion yazi-core/src/input/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
yazi_macro::mod_flat!(input);
yazi_macro::mod_flat!(history input);
1 change: 1 addition & 0 deletions yazi-fm/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ impl<'a> Executor<'a> {
on!(escape);
on!(show);
on!(close);
on!(history);

match mode {
InputMode::Normal => {
Expand Down
3 changes: 3 additions & 0 deletions yazi-parser/src/spark/spark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ pub enum Spark<'a> {
InputEscape(crate::VoidForm),
InputForward(yazi_widgets::input::parser::ForwardOpt),
InputInsert(yazi_widgets::input::parser::InsertOpt),
InputHistory(yazi_widgets::input::parser::HistoryOpt),
InputKill(yazi_widgets::input::parser::KillOpt),
InputMove(yazi_widgets::input::parser::MoveOpt),
InputPaste(yazi_widgets::input::parser::PasteOpt),
Expand Down Expand Up @@ -310,6 +311,7 @@ impl<'a> IntoLua for Spark<'a> {
Self::InputEscape(b) => b.into_lua(lua),
Self::InputForward(b) => b.into_lua(lua),
Self::InputInsert(b) => b.into_lua(lua),
Self::InputHistory(b) => b.into_lua(lua),
Self::InputKill(b) => b.into_lua(lua),
Self::InputMove(b) => b.into_lua(lua),
Self::InputPaste(b) => b.into_lua(lua),
Expand Down Expand Up @@ -448,6 +450,7 @@ try_from_spark!(crate::which::ActivateForm, which:activate);
try_from_spark!(yazi_dds::Payload<'a>, app:accept_payload);
try_from_spark!(yazi_widgets::input::InputOpt, input:show);
try_from_spark!(yazi_widgets::input::parser::BackspaceOpt, input:backspace);
try_from_spark!(yazi_widgets::input::parser::HistoryOpt, input:history);
try_from_spark!(yazi_widgets::input::parser::BackwardOpt, input:backward);
try_from_spark!(yazi_widgets::input::parser::CompleteOpt, input:complete);
try_from_spark!(yazi_widgets::input::parser::DeleteOpt, input:delete);
Expand Down
3 changes: 2 additions & 1 deletion yazi-plugin/src/utils/layer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{str::FromStr, time::Duration};

use mlua::{ExternalError, ExternalResult, Function, IntoLuaMulti, Lua, Table, Value};
use mlua::{ExternalError, ExternalResult, Function, IntoLuaMulti, Lua, Table, Value, prelude::LuaError};
use tokio_stream::wrappers::UnboundedReceiverStream;
use yazi_binding::{InputRx, elements::{Line, Pos, Text}, runtime};
use yazi_config::{Platform, keymap::{Chord, ChordCow, Key}, popup::{ConfirmCfg, InputCfg}};
Expand Down Expand Up @@ -52,6 +52,7 @@ impl Utils {

let realtime = t.raw_get("realtime")?;
let rx = UnboundedReceiverStream::new(InputProxy::show(InputCfg {
id: (t.raw_get("id") as Result<String, LuaError>).unwrap_or_default().into(),
title: t.raw_get("title")?,
value: t.raw_get("value").unwrap_or_default(),
cursor: None, // TODO
Expand Down
4 changes: 3 additions & 1 deletion yazi-widgets/src/input/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ use crossterm::cursor::SetCursorStyle;
use tokio::sync::mpsc;
use yazi_config::YAZI;
use yazi_macro::act;
use yazi_shared::Ids;
use yazi_shared::{Ids, SStr};
use yazi_shim::path::CROSS_SEPARATOR;

use super::{InputSnap, InputSnaps, mode::InputMode, op::InputOp};
use crate::{CLIPBOARD, input::{InputEvent, InputOpt}};

#[derive(Default)]
pub struct Input {
pub id: SStr,
pub snaps: InputSnaps,
pub limit: usize,
pub obscure: bool,
Expand All @@ -27,6 +28,7 @@ impl Input {
pub fn new(opt: InputOpt) -> Result<Self> {
let limit = opt.cfg.position.offset.width.saturating_sub(YAZI.input.border()) as usize;
let mut input = Self {
id: opt.cfg.id,
snaps: InputSnaps::new(opt.cfg.value, opt.cfg.obscure, limit),
limit,
obscure: opt.cfg.obscure,
Expand Down
29 changes: 29 additions & 0 deletions yazi-widgets/src/input/parser/history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use serde::Deserialize;
use yazi_shared::event::ActionCow;

#[derive(Debug, Deserialize)]
pub struct HistoryOpt {
#[serde(alias = "0", default)]
pub offset: i64,
}

impl TryFrom<ActionCow> for HistoryOpt {
type Error = anyhow::Error;

fn try_from(a: ActionCow) -> Result<Self, Self::Error> {
Ok(a.deserialize()?)
}
}

impl FromLua for HistoryOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> {
Err("unsupported".into_lua_err())
}
}

impl IntoLua for HistoryOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> {
Err("unsupported".into_lua_err())
}
}
2 changes: 1 addition & 1 deletion yazi-widgets/src/input/parser/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
yazi_macro::mod_flat!(backspace backward casefy complete delete forward insert kill paste r#move);
yazi_macro::mod_flat!(backspace backward casefy complete delete forward history insert kill paste r#move);
13 changes: 13 additions & 0 deletions yazi-widgets/src/input/snaps.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::mem;

use crate::input::InputMode;
use super::InputSnap;

#[derive(PartialEq, Eq)]
Expand Down Expand Up @@ -76,4 +77,16 @@ impl InputSnaps {

#[inline]
pub(super) fn current_mut(&mut self) -> &mut InputSnap { &mut self.current }

/// Updates the current snap.
///
/// * `mode`: New mode to use.
/// * `cursor`: Cursor position.
/// * `limit`: New size of the snap.
pub fn update_current(&mut self, mode: InputMode, cursor: usize, limit: usize) {
let snap = self.current_mut();
snap.mode = mode;
snap.cursor = cursor.min(snap.count().saturating_sub(mode.delta()));
snap.resize(limit);
}
}
Loading