feat: input history (#4104)

This commit is contained in:
三咲雅 misaki masa 2026-07-06 18:43:08 +08:00 committed by GitHub
parent c92c4aba88
commit aa850f9a3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 324 additions and 38 deletions

View file

@ -19,3 +19,4 @@ If it has already been detailed in the associated issue, please skip this sectio
## Checklist ## Checklist
- [ ] I have read [CONTRIBUTING.md](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md) - [ ] I have read [CONTRIBUTING.md](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md)
- [ ] I confirm this PR follows the [AI Policy](https://github.com/sxyazi/yazi/blob/main/CONTRIBUTING.md#ai-policy)

View file

@ -1,4 +1,4 @@
name: Validate Form name: Validate Issue
on: on:
issues: issues:
@ -24,7 +24,7 @@ jobs:
cd scripts/validate-form cd scripts/validate-form
npm ci npm ci
- name: Validate Form - name: Validate Issue
uses: actions/github-script@v9 uses: actions/github-script@v9
with: with:
script: | script: |

36
.github/workflows/validate-pr.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Validate PR
on:
pull_request_target:
types: [opened, edited, reopened, synchronize]
permissions:
contents: read
pull-requests: read
jobs:
check-list:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.repository.default_branch }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 20
- name: Install Dependencies
run: |
cd scripts/validate-form
npm ci
- name: Validate PR
uses: actions/github-script@v9
with:
script: |
const script = require('./scripts/validate-form/main.js')
await script({github, context, core})
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Drag and drop ([#4005]) - Drag and drop ([#4005])
- Bulk create ([#3793]) - Bulk create ([#3793])
- Make help menu a command palette ([#4074]) - Make help menu a command palette ([#4074])
- Input history ([#4104])
- Make visual mode support wraparound scrolling ([#4101]) - Make visual mode support wraparound scrolling ([#4101])
- H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) - H/M/L Vim-like motion for moving cursor relative to viewport ([#3970])
- Context-aware icons for inputs ([#4080]) - Context-aware icons for inputs ([#4080])
@ -1771,3 +1772,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#4080]: https://github.com/sxyazi/yazi/pull/4080 [#4080]: https://github.com/sxyazi/yazi/pull/4080
[#4096]: https://github.com/sxyazi/yazi/pull/4096 [#4096]: https://github.com/sxyazi/yazi/pull/4096
[#4101]: https://github.com/sxyazi/yazi/pull/4101 [#4101]: https://github.com/sxyazi/yazi/pull/4101
[#4104]: https://github.com/sxyazi/yazi/pull/4104

View file

@ -2,6 +2,15 @@ const LABEL_NAME = "needs info"
const RE_VERSION = /Yazi\s+Version\s*:\s\d+\.\d+\.\d+\s\(/gm const RE_VERSION = /Yazi\s+Version\s*:\s\d+\.\d+\.\d+\s\(/gm
const RE_DEPENDENCIES = /Dependencies\s+[/a-z]+\s*:\s/gm const RE_DEPENDENCIES = /Dependencies\s+[/a-z]+\s*:\s/gm
const RE_CHECKLIST = /#{3}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm const RE_CHECKLIST = /#{3}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm
const RE_PR_CHECKLIST = /#{2}\s+Checklist\s+(?:^-\s+\[x]\s+.+?(?:\n|\r\n|$)){2}/gm
function pullRequestBody(content) {
if (RE_PR_CHECKLIST.test(content)) {
return null
}
return "All required checklist items must be checked in the PR description."
}
function bugReportBody(creator, content, hash) { function bugReportBody(creator, content, hash) {
if (RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content) && new RegExp(` \\(${hash}[a-f0-9]? `).test(content)) { if (RE_DEPENDENCIES.test(content) && RE_CHECKLIST.test(content) && new RegExp(` \\(${hash}[a-f0-9]? `).test(content)) {
@ -41,6 +50,12 @@ Our maintainers work on Yazi in their free time, this helps them work efficientl
` `
} }
function skipValidation(context) {
const login = context.payload.issue?.user?.login || context.payload.pull_request?.user?.login
const owner = context.payload.repository?.owner?.login || context.repo.owner
return !!login && login === owner
}
module.exports = async ({ github, context, core }) => { module.exports = async ({ github, context, core }) => {
async function nightlyHash() { async function nightlyHash() {
try { try {
@ -214,19 +229,23 @@ Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.y
} }
async function main() { async function main() {
const hash = await nightlyHash()
if (!hash) return
if (context.eventName === "schedule") { if (context.eventName === "schedule") {
await closeOldIssues() await closeOldIssues()
return return
} }
if (skipValidation(context)) {
return
}
if (context.eventName === "issues") { if (context.eventName === "issues") {
const id = context.payload.issue.number const id = context.payload.issue.number
const content = context.payload.issue.body || "" const content = context.payload.issue.body || ""
const creator = context.payload.issue.user.login const creator = context.payload.issue.user.login
const hash = await nightlyHash()
if (!hash) return
if (await hasLabel(id, "bug")) { if (await hasLabel(id, "bug")) {
const body = bugReportBody(creator, content, hash) const body = bugReportBody(creator, content, hash)
await updateLabels(id, !!body, body) await updateLabels(id, !!body, body)
@ -236,6 +255,13 @@ Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.y
} else if (context.payload.action === "opened") { } else if (context.payload.action === "opened") {
await closeUnsupportedIssue(id) await closeUnsupportedIssue(id)
} }
} else if (context.eventName === "pull_request" || context.eventName === "pull_request_target") {
const content = context.payload.pull_request.body || ""
const body = pullRequestBody(content)
if (body) {
core.setFailed(body)
}
} }
} }

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use yazi_core::input::InputMutGuard;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::{input::CloseForm, spark::SparkKind}; use yazi_parser::{input::CloseForm, spark::SparkKind};
use yazi_shared::{Source, data::Data}; use yazi_shared::{Source, data::Data};
@ -20,11 +21,16 @@ impl Actor for Close {
guard.ticket.next(); guard.ticket.next();
if let Some(cb) = guard.cb.take() { if let Some(cb) = guard.cb.take() {
let value = guard.snap().value.clone(); let value = guard.value().to_owned();
cb(if form.submit { InputEvent::Submit(value) } else { InputEvent::Cancel(value) }); cb(if form.submit { InputEvent::Submit(value) } else { InputEvent::Cancel(value) });
} }
drop(guard); if form.submit
&& let InputMutGuard::Main(input) = guard
{
input.histories.remember(&input.main.history.name, input.main.value());
}
cx.input.main.visible = false; cx.input.main.visible = false;
act!(cmp:close, cx)?; act!(cmp:close, cx)?;

View file

@ -1 +1 @@
yazi_macro::mod_flat!(close complete escape show); yazi_macro::mod_flat!(close complete escape recall remember show);

View file

@ -0,0 +1,32 @@
use anyhow::Result;
use yazi_core::input::InputMutGuard;
use yazi_macro::succ;
use yazi_parser::input::RecallForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Recall;
impl Actor for Recall {
type Form = RecallForm;
const NAME: &str = "recall";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some(input) = cx.input.lock_mut() else {
succ!();
};
match input {
InputMutGuard::Main(input) => {
let entries = input.histories.get(&input.main.history.name);
input.main.recall(entries, form.step)
}
InputMutGuard::Alt(input, mut guard) => {
let entries = input.histories.get(&guard.history.name);
guard.recall(entries, form.step)
}
}
}
}

View file

@ -0,0 +1,33 @@
use anyhow::Result;
use yazi_core::input::InputMutGuard;
use yazi_macro::succ;
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Remember;
impl Actor for Remember {
type Form = VoidForm;
const NAME: &str = "remember";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let Some(mut input) = cx.input.lock_mut() else {
succ!();
};
match &mut input {
InputMutGuard::Main(input) => {
input.histories.remember(&input.main.history.name, input.main.value());
}
InputMutGuard::Alt(input, guard) => {
input.histories.remember(&guard.history.name, guard.value());
}
}
input.history.take();
succ!();
}
}

View file

@ -310,6 +310,14 @@ keymap = [
{ on = "U", run = "casefy upper", desc = "Uppercase" }, { on = "U", run = "casefy upper", desc = "Uppercase" },
{ on = "<C-r>", run = "redo", desc = "Redo the last operation" }, { on = "<C-r>", run = "redo", desc = "Redo the last operation" },
# History
{ on = "k", run = "recall -1", desc = "Recall previous input" },
{ on = "j", run = "recall 1", desc = "Recall next input" },
{ on = "<Up>", run = "recall -1", desc = "Recall previous input" },
{ on = "<Down>", run = "recall 1", desc = "Recall next input" },
{ on = "<C-p>", run = "recall -1", desc = "Recall previous input" },
{ on = "<C-n>", run = "recall 1", desc = "Recall next input" },
# Help # Help
{ on = "~", run = "help", desc = "Open help" }, { on = "~", run = "help", desc = "Open help" },
{ on = "<F1>", run = "help", desc = "Open help" }, { on = "<F1>", run = "help", desc = "Open help" },

View file

@ -50,6 +50,7 @@ impl Input {
name: "cd".to_owned(), name: "cd".to_owned(),
title: self.cd_title.clone(), title: self.cd_title.clone(),
value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() }, value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() },
history: "shared".to_owned(),
position: Position::new(self.cd_origin, self.cd_offset), position: Position::new(self.cd_origin, self.cd_offset),
completion: true, completion: true,
..Default::default() ..Default::default()
@ -60,6 +61,7 @@ impl Input {
InputOpt { InputOpt {
name: format!("create-{}", if dir { "dir" } else { "file" }), name: format!("create-{}", if dir { "dir" } else { "file" }),
title: self.create_title[dir as usize].clone(), title: self.create_title[dir as usize].clone(),
history: "shared".to_owned(),
position: Position::new(self.create_origin, self.create_offset), position: Position::new(self.create_origin, self.create_offset),
..Default::default() ..Default::default()
} }
@ -69,6 +71,7 @@ impl Input {
InputOpt { InputOpt {
name: format!("rename-{}", if is_dir { "dir" } else { "file" }), name: format!("rename-{}", if is_dir { "dir" } else { "file" }),
title: self.rename_title.clone(), title: self.rename_title.clone(),
history: "shared".to_owned(),
position: Position::new(self.rename_origin, self.rename_offset), position: Position::new(self.rename_origin, self.rename_offset),
..Default::default() ..Default::default()
} }
@ -78,6 +81,7 @@ impl Input {
InputOpt { InputOpt {
name: "filter".to_owned(), name: "filter".to_owned(),
title: self.filter_title.clone(), title: self.filter_title.clone(),
history: "shared".to_owned(),
position: Position::new(self.filter_origin, self.filter_offset), position: Position::new(self.filter_origin, self.filter_offset),
realtime: true, realtime: true,
..Default::default() ..Default::default()
@ -88,6 +92,7 @@ impl Input {
InputOpt { InputOpt {
name: "find".to_owned(), name: "find".to_owned(),
title: self.find_title[prev as usize].clone(), title: self.find_title[prev as usize].clone(),
history: "shared".to_owned(),
position: Position::new(self.find_origin, self.find_offset), position: Position::new(self.find_origin, self.find_offset),
realtime: true, realtime: true,
..Default::default() ..Default::default()
@ -98,6 +103,7 @@ impl Input {
InputOpt { InputOpt {
name: "search".to_owned(), name: "search".to_owned(),
title: self.search_title.replace("{n}", name), title: self.search_title.replace("{n}", name),
history: "shared".to_owned(),
position: Position::new(self.search_origin, self.search_offset), position: Position::new(self.search_origin, self.search_offset),
..Default::default() ..Default::default()
} }
@ -107,6 +113,7 @@ impl Input {
InputOpt { InputOpt {
name: "shell".to_owned(), name: "shell".to_owned(),
title: self.shell_title[block as usize].clone(), title: self.shell_title[block as usize].clone(),
history: "shared".to_owned(),
position: Position::new(self.shell_origin, self.shell_offset), position: Position::new(self.shell_origin, self.shell_offset),
..Default::default() ..Default::default()
} }
@ -116,6 +123,7 @@ impl Input {
InputOpt { InputOpt {
name: "tab-rename".to_owned(), name: "tab-rename".to_owned(),
title: "Rename tab:".to_owned(), title: "Rename tab:".to_owned(),
history: "shared".to_owned(),
position: Position::new(Origin::TopCenter, Offset { position: Position::new(Origin::TopCenter, Offset {
x: 0, x: 0,
y: 2, y: 2,

View file

@ -1,6 +1,8 @@
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use parking_lot::MutexGuard; use parking_lot::{ArcMutexGuard, MutexGuard, RawMutex};
use crate::input::Input;
// --- InputGuard // --- InputGuard
pub enum InputGuard<'a> { pub enum InputGuard<'a> {
@ -21,8 +23,8 @@ impl Deref for InputGuard<'_> {
// --- InputMutGuard // --- InputMutGuard
pub enum InputMutGuard<'a> { pub enum InputMutGuard<'a> {
Main(&'a mut yazi_widgets::input::Input), Main(&'a mut Input),
Alt(MutexGuard<'a, yazi_widgets::input::Input>), Alt(&'a mut Input, ArcMutexGuard<RawMutex, yazi_widgets::input::Input>),
} }
impl Deref for InputMutGuard<'_> { impl Deref for InputMutGuard<'_> {
@ -30,8 +32,8 @@ impl Deref for InputMutGuard<'_> {
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
match self { match self {
Self::Main(main) => main, Self::Main(input) => &input.main.inner,
Self::Alt(alt) => alt, Self::Alt(_, guard) => guard,
} }
} }
} }
@ -39,8 +41,8 @@ impl Deref for InputMutGuard<'_> {
impl DerefMut for InputMutGuard<'_> { impl DerefMut for InputMutGuard<'_> {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
match self { match self {
Self::Main(main) => main, Self::Main(input) => &mut input.main.inner,
Self::Alt(alt) => alt, Self::Alt(_, guard) => guard,
} }
} }
} }

View file

@ -0,0 +1,31 @@
use hashbrown::HashMap;
#[derive(Default)]
pub struct InputHistories {
inner: HashMap<String, Vec<String>>,
}
impl InputHistories {
pub fn get(&self, group: &str) -> &[String] {
self.inner.get(group).map(Vec::as_slice).unwrap_or(&[])
}
pub fn remember(&mut self, group: &str, value: &str) -> bool {
if group.is_empty() || value.is_empty() {
return false;
}
let entries = self.inner.entry_ref(group).or_default();
if entries.last().is_some_and(|last| last == value) {
return false;
}
entries.retain(|entry| entry != value);
entries.push(value.to_owned());
if entries.len() > 20 {
entries.drain(..entries.len() - 20);
}
true
}
}

View file

@ -4,12 +4,13 @@ use parking_lot::Mutex;
use ratatui_widgets::block::Padding; use ratatui_widgets::block::Padding;
use yazi_binding::{elements::Spatial, position::Position}; use yazi_binding::{elements::Spatial, position::Position};
use crate::input::{InputGuard, InputMutGuard}; use crate::input::{InputGuard, InputHistories, InputMutGuard};
#[derive(Default)] #[derive(Default)]
pub struct Input { pub struct Input {
pub main: InputMain, pub main: InputMain,
pub alt: Option<InputAlt>, pub alt: Option<InputAlt>,
pub histories: InputHistories,
} }
impl Input { impl Input {
@ -39,9 +40,10 @@ impl Input {
pub fn lock_mut(&mut self) -> Option<InputMutGuard<'_>> { pub fn lock_mut(&mut self) -> Option<InputMutGuard<'_>> {
if self.main.visible { if self.main.visible {
Some(InputMutGuard::Main(&mut self.main.inner)) Some(InputMutGuard::Main(self))
} else if let Some(alt) = &self.alt { } else if let Some(alt) = &mut self.alt {
Some(InputMutGuard::Alt(alt.inner.lock())) let guard = alt.inner.lock_arc();
Some(InputMutGuard::Alt(self, guard))
} else { } else {
None None
} }
@ -51,11 +53,11 @@ impl Input {
// --- InputMain // --- InputMain
#[derive(Default)] #[derive(Default)]
pub struct InputMain { pub struct InputMain {
inner: yazi_widgets::input::Input, pub(super) inner: yazi_widgets::input::Input,
pub name: String, pub name: String,
pub title: String, pub title: String,
pub position: Position, pub position: Position,
pub visible: bool, pub visible: bool,
} }
impl Deref for InputMain { impl Deref for InputMain {

View file

@ -1 +1 @@
yazi_macro::mod_flat!(guard input); yazi_macro::mod_flat!(guard history input);

View file

@ -267,6 +267,8 @@ impl<'a> Executor<'a> {
on!(escape); on!(escape);
on!(show); on!(show);
on!(close); on!(close);
on!(recall);
on!(remember);
guard = match self.app.core.input.lock_mut() { guard = match self.app.core.input.lock_mut() {
Some(g) => g, Some(g) => g,

View file

@ -1 +1 @@
yazi_macro::mod_flat!(close show); yazi_macro::mod_flat!(close recall show);

View file

@ -0,0 +1,24 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use serde::Deserialize;
use yazi_shared::event::ActionCow;
use yazi_widgets::Step;
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct RecallForm {
#[serde(alias = "0")]
pub step: Step,
}
impl TryFrom<ActionCow> for RecallForm {
type Error = anyhow::Error;
fn try_from(a: ActionCow) -> Result<Self, Self::Error> { Ok(a.deserialize()?) }
}
impl FromLua for RecallForm {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for RecallForm {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -126,6 +126,8 @@ pub enum Spark<'a> {
InputKill(yazi_widgets::input::parser::KillOpt), InputKill(yazi_widgets::input::parser::KillOpt),
InputMove(yazi_widgets::input::parser::MoveOpt), InputMove(yazi_widgets::input::parser::MoveOpt),
InputPaste(yazi_widgets::input::parser::PasteOpt), InputPaste(yazi_widgets::input::parser::PasteOpt),
InputRecall(crate::input::RecallForm),
InputRemember(crate::VoidForm),
InputShow(crate::input::ShowForm), InputShow(crate::input::ShowForm),
// Notify // Notify
@ -319,6 +321,8 @@ impl<'a> IntoLua for Spark<'a> {
Self::InputKill(b) => b.into_lua(lua), Self::InputKill(b) => b.into_lua(lua),
Self::InputMove(b) => b.into_lua(lua), Self::InputMove(b) => b.into_lua(lua),
Self::InputPaste(b) => b.into_lua(lua), Self::InputPaste(b) => b.into_lua(lua),
Self::InputRecall(b) => b.into_lua(lua),
Self::InputRemember(b) => b.into_lua(lua),
Self::InputShow(b) => b.into_lua(lua), Self::InputShow(b) => b.into_lua(lua),
// Notify // Notify
@ -376,6 +380,7 @@ try_from_spark!(
mgr:suspend, mgr:suspend,
mgr:unyank, mgr:unyank,
mgr:watch, mgr:watch,
input:remember,
which:dismiss which:dismiss
); );
@ -464,3 +469,4 @@ try_from_spark!(yazi_widgets::input::parser::InsertOpt, input:insert);
try_from_spark!(yazi_widgets::input::parser::KillOpt, input:kill); try_from_spark!(yazi_widgets::input::parser::KillOpt, input:kill);
try_from_spark!(yazi_widgets::input::parser::MoveOpt, input:move); try_from_spark!(yazi_widgets::input::parser::MoveOpt, input:move);
try_from_spark!(yazi_widgets::input::parser::PasteOpt, input:paste); try_from_spark!(yazi_widgets::input::parser::PasteOpt, input:paste);
try_from_spark!(crate::input::RecallForm, input:recall);

View file

@ -21,7 +21,7 @@ impl Input {
act!(r#move, self, -1)?; act!(r#move, self, -1)?;
} }
self.flush_type(); self.flush_all();
succ!(render!()); succ!(render!());
} }
} }

View file

@ -22,7 +22,7 @@ impl Input {
snap.value.replace_range(start..end, &casefied); snap.value.replace_range(start..end, &casefied);
snap.op = InputOp::None; snap.op = InputOp::None;
snap.cursor = range.start; snap.cursor = range.start;
self.snaps.tag(self.size.width as usize).then(|| self.flush_type()); self.snaps.tag(self.size.width as usize).then(|| self.flush_all());
act!(r#move, self)?; act!(r#move, self)?;
succ!(render!()); succ!(render!());

View file

@ -25,7 +25,7 @@ impl Input {
snap.value = new; snap.value = new;
act!(r#move, self, delta)?; act!(r#move, self, delta)?;
self.flush_type(); self.flush_all();
succ!(render!()); succ!(render!());
} }
} }

View file

@ -45,7 +45,7 @@ impl Input {
} }
act!(r#move, self)?; act!(r#move, self)?;
self.flush_type(); self.flush_all();
succ!(render!()); succ!(render!());
} }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(actor backspace backward casefy complete delete escape forward insert kill paste r#move r#type redo replace undo visual yank); yazi_macro::mod_flat!(actor backspace backward casefy complete delete escape forward insert kill paste recall r#move r#type redo replace undo visual yank);

View file

@ -0,0 +1,40 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_shared::data::Data;
use crate::{Step, input::{Input, InputSnap}};
impl Input {
pub fn recall(&mut self, items: &[String], step: Step) -> Result<Data> {
if items.is_empty() {
succ!();
}
let pos = self.history.at.unwrap_or(items.len());
let next = step.add(pos, items.len() + 1, 0, 0, 0);
if next == pos {
succ!();
} else if next == items.len() {
let draft = self.history.take();
return self.recall_to(draft);
} else if self.history.at.is_none() {
self.history.draft = self.value().to_owned();
}
self.history.at = Some(next);
self.recall_to(items[next].clone())
}
fn recall_to(&mut self, value: String) -> Result<Data> {
let mode = self.mode();
let mut snap = InputSnap::new(value, self.obscure);
snap.mode = mode;
snap.resize(self.size.width as usize);
*self.snap_mut() = snap;
self.flush_type();
succ!(render!());
}
}

View file

@ -29,7 +29,7 @@ impl Input {
(Some(_), Some((len, _))) => snap.value.replace_range(start..start + len, &s), (Some(_), Some((len, _))) => snap.value.replace_range(start..start + len, &s),
} }
self.snaps.tag(self.size.width as usize).then(|| self.flush_type()); self.snaps.tag(self.size.width as usize).then(|| self.flush_all());
succ!(render!()); succ!(render!());
} }
} }

View file

@ -31,7 +31,7 @@ impl Input {
} }
act!(r#move, self, s.chars().count() as isize)?; act!(r#move, self, s.chars().count() as isize)?;
self.flush_type(); self.flush_all();
succ!(render!()); succ!(render!());
} }
} }

View file

@ -0,0 +1,18 @@
use std::mem;
#[derive(Debug, Default)]
pub struct InputHistory {
pub name: String,
pub(super) at: Option<usize>,
pub(super) draft: String,
}
impl InputHistory {
pub(super) fn new(name: String) -> Self { Self { name, ..Default::default() } }
pub fn take(&mut self) -> String {
self.at = None;
mem::take(&mut self.draft)
}
}

View file

@ -7,13 +7,14 @@ use yazi_shared::id::Ids;
use yazi_shim::path::CROSS_SEPARATOR; use yazi_shim::path::CROSS_SEPARATOR;
use yazi_tty::sequence::SetCursorStyle; use yazi_tty::sequence::SetCursorStyle;
use super::{InputSnap, InputSnaps, mode::InputMode, op::InputOp}; use super::{InputHistory, InputSnap, InputSnaps, mode::InputMode, op::InputOp};
use crate::{CLIPBOARD, input::{InputCallback, InputEvent, InputOpt, InputStyles}}; use crate::{CLIPBOARD, input::{InputCallback, InputEvent, InputOpt, InputStyles}};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Input { pub struct Input {
pub size: Size, pub size: Size,
pub snaps: InputSnaps, pub snaps: InputSnaps,
pub history: InputHistory,
pub styles: InputStyles, pub styles: InputStyles,
pub obscure: bool, pub obscure: bool,
pub realtime: bool, pub realtime: bool,
@ -27,6 +28,7 @@ impl Input {
pub fn new(opt: InputOpt) -> Result<Self> { pub fn new(opt: InputOpt) -> Result<Self> {
let mut input = Self { let mut input = Self {
snaps: InputSnaps::new(opt.value, opt.obscure), snaps: InputSnaps::new(opt.value, opt.obscure),
history: InputHistory::new(opt.history),
styles: opt.styles, styles: opt.styles,
obscure: opt.obscure, obscure: opt.obscure,
realtime: opt.realtime, realtime: opt.realtime,
@ -89,11 +91,16 @@ 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(self.size.width as usize).then(|| self.flush_type()); self.snaps.tag(self.size.width as usize).then(|| self.flush_all());
} }
true true
} }
pub(super) fn flush_all(&mut self) {
self.history.take();
self.flush_type();
}
pub(super) fn flush_type(&mut self) { pub(super) fn flush_type(&mut self) {
self.ticket.next(); self.ticket.next();
if let Some(cb) = self.cb.as_ref().filter(|_| self.realtime) { if let Some(cb) = self.cb.as_ref().filter(|_| self.realtime) {

View file

@ -1,3 +1,3 @@
yazi_macro::mod_pub!(actor parser); yazi_macro::mod_pub!(actor parser);
yazi_macro::mod_flat!(callback chars event gait input input_arc mode op option snap snaps stream styles widget); yazi_macro::mod_flat!(callback chars event gait history input input_arc mode op option snap snaps stream styles widget);

View file

@ -9,6 +9,7 @@ pub struct InputOpt {
pub name: String, pub name: String,
pub title: String, pub title: String,
pub value: String, pub value: String,
pub history: String,
pub styles: InputStyles, pub styles: InputStyles,
pub cursor: Option<usize>, pub cursor: Option<usize>,
pub obscure: bool, pub obscure: bool,
@ -46,6 +47,7 @@ impl TryFrom<&Table> for InputOpt {
name: t.raw_get("name").unwrap_or_default(), name: t.raw_get("name").unwrap_or_default(),
title: t.raw_get("title").unwrap_or_default(), title: t.raw_get("title").unwrap_or_default(),
value: t.raw_get("value").unwrap_or_default(), value: t.raw_get("value").unwrap_or_default(),
history: t.raw_get("history").unwrap_or_default(),
styles: t.raw_get("styles")?, styles: t.raw_get("styles")?,
cursor: None, cursor: None,
obscure: t.raw_get("obscure")?, obscure: t.raw_get("obscure")?,