diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8c7716aa..a0e4288f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,3 +19,4 @@ If it has already been detailed in the associated issue, please skip this sectio ## Checklist - [ ] 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) diff --git a/.github/workflows/validate-form.yml b/.github/workflows/validate-issue.yml similarity index 93% rename from .github/workflows/validate-form.yml rename to .github/workflows/validate-issue.yml index 81ccc28a..7e997036 100644 --- a/.github/workflows/validate-form.yml +++ b/.github/workflows/validate-issue.yml @@ -1,4 +1,4 @@ -name: Validate Form +name: Validate Issue on: issues: @@ -24,7 +24,7 @@ jobs: cd scripts/validate-form npm ci - - name: Validate Form + - name: Validate Issue uses: actions/github-script@v9 with: script: | diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 00000000..17d9009e --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index eec968e8..0a64090e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Drag and drop ([#4005]) - Bulk create ([#3793]) - Make help menu a command palette ([#4074]) +- Input history ([#4104]) - Make visual mode support wraparound scrolling ([#4101]) - H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) - 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 [#4096]: https://github.com/sxyazi/yazi/pull/4096 [#4101]: https://github.com/sxyazi/yazi/pull/4101 +[#4104]: https://github.com/sxyazi/yazi/pull/4104 diff --git a/scripts/validate-form/main.js b/scripts/validate-form/main.js index bd3fddb8..186d2689 100644 --- a/scripts/validate-form/main.js +++ b/scripts/validate-form/main.js @@ -2,6 +2,15 @@ const LABEL_NAME = "needs info" 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_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) { 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 }) => { async function nightlyHash() { try { @@ -214,19 +229,23 @@ Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.y } async function main() { - const hash = await nightlyHash() - if (!hash) return - if (context.eventName === "schedule") { await closeOldIssues() return } + if (skipValidation(context)) { + return + } + if (context.eventName === "issues") { const id = context.payload.issue.number const content = context.payload.issue.body || "" const creator = context.payload.issue.user.login + const hash = await nightlyHash() + if (!hash) return + if (await hasLabel(id, "bug")) { const body = bugReportBody(creator, content, hash) 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") { 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) + } } } diff --git a/yazi-actor/src/input/close.rs b/yazi-actor/src/input/close.rs index 19ae12e7..c6ef387f 100644 --- a/yazi-actor/src/input/close.rs +++ b/yazi-actor/src/input/close.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use yazi_core::input::InputMutGuard; use yazi_macro::{act, render, succ}; use yazi_parser::{input::CloseForm, spark::SparkKind}; use yazi_shared::{Source, data::Data}; @@ -20,11 +21,16 @@ impl Actor for Close { guard.ticket.next(); 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) }); } - 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; act!(cmp:close, cx)?; diff --git a/yazi-actor/src/input/mod.rs b/yazi-actor/src/input/mod.rs index 16e44b0e..8f2c1b56 100644 --- a/yazi-actor/src/input/mod.rs +++ b/yazi-actor/src/input/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(close complete escape show); +yazi_macro::mod_flat!(close complete escape recall remember show); diff --git a/yazi-actor/src/input/recall.rs b/yazi-actor/src/input/recall.rs new file mode 100644 index 00000000..98a99c2f --- /dev/null +++ b/yazi-actor/src/input/recall.rs @@ -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 { + 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) + } + } + } +} diff --git a/yazi-actor/src/input/remember.rs b/yazi-actor/src/input/remember.rs new file mode 100644 index 00000000..f83039ce --- /dev/null +++ b/yazi-actor/src/input/remember.rs @@ -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 { + 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!(); + } +} diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index d39c00e8..db1fa4ff 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -310,6 +310,14 @@ keymap = [ { on = "U", run = "casefy upper", desc = "Uppercase" }, { on = "", 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 = "", run = "recall -1", desc = "Recall previous input" }, + { on = "", run = "recall 1", desc = "Recall next input" }, + { on = "", run = "recall -1", desc = "Recall previous input" }, + { on = "", run = "recall 1", desc = "Recall next input" }, + # Help { on = "~", run = "help", desc = "Open help" }, { on = "", run = "help", desc = "Open help" }, diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index 8d04c6f8..8abebacf 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -50,6 +50,7 @@ impl Input { name: "cd".to_owned(), title: self.cd_title.clone(), 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), completion: true, ..Default::default() @@ -60,6 +61,7 @@ impl Input { InputOpt { name: format!("create-{}", if dir { "dir" } else { "file" }), title: self.create_title[dir as usize].clone(), + history: "shared".to_owned(), position: Position::new(self.create_origin, self.create_offset), ..Default::default() } @@ -69,6 +71,7 @@ impl Input { InputOpt { name: format!("rename-{}", if is_dir { "dir" } else { "file" }), title: self.rename_title.clone(), + history: "shared".to_owned(), position: Position::new(self.rename_origin, self.rename_offset), ..Default::default() } @@ -78,6 +81,7 @@ impl Input { InputOpt { name: "filter".to_owned(), title: self.filter_title.clone(), + history: "shared".to_owned(), position: Position::new(self.filter_origin, self.filter_offset), realtime: true, ..Default::default() @@ -88,6 +92,7 @@ impl Input { InputOpt { name: "find".to_owned(), title: self.find_title[prev as usize].clone(), + history: "shared".to_owned(), position: Position::new(self.find_origin, self.find_offset), realtime: true, ..Default::default() @@ -98,6 +103,7 @@ impl Input { InputOpt { name: "search".to_owned(), title: self.search_title.replace("{n}", name), + history: "shared".to_owned(), position: Position::new(self.search_origin, self.search_offset), ..Default::default() } @@ -107,6 +113,7 @@ impl Input { InputOpt { name: "shell".to_owned(), title: self.shell_title[block as usize].clone(), + history: "shared".to_owned(), position: Position::new(self.shell_origin, self.shell_offset), ..Default::default() } @@ -116,6 +123,7 @@ impl Input { InputOpt { name: "tab-rename".to_owned(), title: "Rename tab:".to_owned(), + history: "shared".to_owned(), position: Position::new(Origin::TopCenter, Offset { x: 0, y: 2, diff --git a/yazi-core/src/input/guard.rs b/yazi-core/src/input/guard.rs index c0328928..e7998c41 100644 --- a/yazi-core/src/input/guard.rs +++ b/yazi-core/src/input/guard.rs @@ -1,6 +1,8 @@ use std::ops::{Deref, DerefMut}; -use parking_lot::MutexGuard; +use parking_lot::{ArcMutexGuard, MutexGuard, RawMutex}; + +use crate::input::Input; // --- InputGuard pub enum InputGuard<'a> { @@ -21,8 +23,8 @@ impl Deref for InputGuard<'_> { // --- InputMutGuard pub enum InputMutGuard<'a> { - Main(&'a mut yazi_widgets::input::Input), - Alt(MutexGuard<'a, yazi_widgets::input::Input>), + Main(&'a mut Input), + Alt(&'a mut Input, ArcMutexGuard), } impl Deref for InputMutGuard<'_> { @@ -30,8 +32,8 @@ impl Deref for InputMutGuard<'_> { fn deref(&self) -> &Self::Target { match self { - Self::Main(main) => main, - Self::Alt(alt) => alt, + Self::Main(input) => &input.main.inner, + Self::Alt(_, guard) => guard, } } } @@ -39,8 +41,8 @@ impl Deref for InputMutGuard<'_> { impl DerefMut for InputMutGuard<'_> { fn deref_mut(&mut self) -> &mut Self::Target { match self { - Self::Main(main) => main, - Self::Alt(alt) => alt, + Self::Main(input) => &mut input.main.inner, + Self::Alt(_, guard) => guard, } } } diff --git a/yazi-core/src/input/history.rs b/yazi-core/src/input/history.rs new file mode 100644 index 00000000..9bda9297 --- /dev/null +++ b/yazi-core/src/input/history.rs @@ -0,0 +1,31 @@ +use hashbrown::HashMap; + +#[derive(Default)] +pub struct InputHistories { + inner: HashMap>, +} + +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 + } +} diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 425cc277..56e6afc6 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -4,12 +4,13 @@ use parking_lot::Mutex; use ratatui_widgets::block::Padding; use yazi_binding::{elements::Spatial, position::Position}; -use crate::input::{InputGuard, InputMutGuard}; +use crate::input::{InputGuard, InputHistories, InputMutGuard}; #[derive(Default)] pub struct Input { - pub main: InputMain, - pub alt: Option, + pub main: InputMain, + pub alt: Option, + pub histories: InputHistories, } impl Input { @@ -39,9 +40,10 @@ impl Input { pub fn lock_mut(&mut self) -> Option> { if self.main.visible { - Some(InputMutGuard::Main(&mut self.main.inner)) - } else if let Some(alt) = &self.alt { - Some(InputMutGuard::Alt(alt.inner.lock())) + Some(InputMutGuard::Main(self)) + } else if let Some(alt) = &mut self.alt { + let guard = alt.inner.lock_arc(); + Some(InputMutGuard::Alt(self, guard)) } else { None } @@ -51,11 +53,11 @@ impl Input { // --- InputMain #[derive(Default)] pub struct InputMain { - inner: yazi_widgets::input::Input, - pub name: String, - pub title: String, - pub position: Position, - pub visible: bool, + pub(super) inner: yazi_widgets::input::Input, + pub name: String, + pub title: String, + pub position: Position, + pub visible: bool, } impl Deref for InputMain { diff --git a/yazi-core/src/input/mod.rs b/yazi-core/src/input/mod.rs index 60b5cf6d..4aa69d91 100644 --- a/yazi-core/src/input/mod.rs +++ b/yazi-core/src/input/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(guard input); +yazi_macro::mod_flat!(guard history input); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 431b97b0..61e06097 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -267,6 +267,8 @@ impl<'a> Executor<'a> { on!(escape); on!(show); on!(close); + on!(recall); + on!(remember); guard = match self.app.core.input.lock_mut() { Some(g) => g, diff --git a/yazi-parser/src/input/mod.rs b/yazi-parser/src/input/mod.rs index 37e61514..ed31961f 100644 --- a/yazi-parser/src/input/mod.rs +++ b/yazi-parser/src/input/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(close show); +yazi_macro::mod_flat!(close recall show); diff --git a/yazi-parser/src/input/recall.rs b/yazi-parser/src/input/recall.rs new file mode 100644 index 00000000..5d002fcf --- /dev/null +++ b/yazi-parser/src/input/recall.rs @@ -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 for RecallForm { + type Error = anyhow::Error; + + fn try_from(a: ActionCow) -> Result { Ok(a.deserialize()?) } +} + +impl FromLua for RecallForm { + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for RecallForm { + fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} diff --git a/yazi-parser/src/spark/spark.rs b/yazi-parser/src/spark/spark.rs index 46745ac6..4f88962d 100644 --- a/yazi-parser/src/spark/spark.rs +++ b/yazi-parser/src/spark/spark.rs @@ -126,6 +126,8 @@ pub enum Spark<'a> { InputKill(yazi_widgets::input::parser::KillOpt), InputMove(yazi_widgets::input::parser::MoveOpt), InputPaste(yazi_widgets::input::parser::PasteOpt), + InputRecall(crate::input::RecallForm), + InputRemember(crate::VoidForm), InputShow(crate::input::ShowForm), // Notify @@ -319,6 +321,8 @@ impl<'a> IntoLua for Spark<'a> { Self::InputKill(b) => b.into_lua(lua), Self::InputMove(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), // Notify @@ -376,6 +380,7 @@ try_from_spark!( mgr:suspend, mgr:unyank, mgr:watch, + input:remember, 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::MoveOpt, input:move); try_from_spark!(yazi_widgets::input::parser::PasteOpt, input:paste); +try_from_spark!(crate::input::RecallForm, input:recall); diff --git a/yazi-widgets/src/input/actor/backspace.rs b/yazi-widgets/src/input/actor/backspace.rs index 11407702..549cae58 100644 --- a/yazi-widgets/src/input/actor/backspace.rs +++ b/yazi-widgets/src/input/actor/backspace.rs @@ -21,7 +21,7 @@ impl Input { act!(r#move, self, -1)?; } - self.flush_type(); + self.flush_all(); succ!(render!()); } } diff --git a/yazi-widgets/src/input/actor/casefy.rs b/yazi-widgets/src/input/actor/casefy.rs index 3db1ce00..4544a367 100644 --- a/yazi-widgets/src/input/actor/casefy.rs +++ b/yazi-widgets/src/input/actor/casefy.rs @@ -22,7 +22,7 @@ impl Input { snap.value.replace_range(start..end, &casefied); snap.op = InputOp::None; 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)?; succ!(render!()); diff --git a/yazi-widgets/src/input/actor/complete.rs b/yazi-widgets/src/input/actor/complete.rs index 00884ecb..c54980c4 100644 --- a/yazi-widgets/src/input/actor/complete.rs +++ b/yazi-widgets/src/input/actor/complete.rs @@ -25,7 +25,7 @@ impl Input { snap.value = new; act!(r#move, self, delta)?; - self.flush_type(); + self.flush_all(); succ!(render!()); } } diff --git a/yazi-widgets/src/input/actor/kill.rs b/yazi-widgets/src/input/actor/kill.rs index c2f44931..51969ee5 100644 --- a/yazi-widgets/src/input/actor/kill.rs +++ b/yazi-widgets/src/input/actor/kill.rs @@ -45,7 +45,7 @@ impl Input { } act!(r#move, self)?; - self.flush_type(); + self.flush_all(); succ!(render!()); } diff --git a/yazi-widgets/src/input/actor/mod.rs b/yazi-widgets/src/input/actor/mod.rs index 7913df7d..999e9597 100644 --- a/yazi-widgets/src/input/actor/mod.rs +++ b/yazi-widgets/src/input/actor/mod.rs @@ -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); diff --git a/yazi-widgets/src/input/actor/recall.rs b/yazi-widgets/src/input/actor/recall.rs new file mode 100644 index 00000000..e4f1159a --- /dev/null +++ b/yazi-widgets/src/input/actor/recall.rs @@ -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 { + 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 { + 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!()); + } +} diff --git a/yazi-widgets/src/input/actor/replace.rs b/yazi-widgets/src/input/actor/replace.rs index 39a47d4f..a0ac14d2 100644 --- a/yazi-widgets/src/input/actor/replace.rs +++ b/yazi-widgets/src/input/actor/replace.rs @@ -29,7 +29,7 @@ impl Input { (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!()); } } diff --git a/yazi-widgets/src/input/actor/type.rs b/yazi-widgets/src/input/actor/type.rs index 90d1390c..2de0332c 100644 --- a/yazi-widgets/src/input/actor/type.rs +++ b/yazi-widgets/src/input/actor/type.rs @@ -31,7 +31,7 @@ impl Input { } act!(r#move, self, s.chars().count() as isize)?; - self.flush_type(); + self.flush_all(); succ!(render!()); } } diff --git a/yazi-widgets/src/input/history.rs b/yazi-widgets/src/input/history.rs new file mode 100644 index 00000000..f82de7f8 --- /dev/null +++ b/yazi-widgets/src/input/history.rs @@ -0,0 +1,18 @@ +use std::mem; + +#[derive(Debug, Default)] +pub struct InputHistory { + pub name: String, + + pub(super) at: Option, + 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) + } +} diff --git a/yazi-widgets/src/input/input.rs b/yazi-widgets/src/input/input.rs index 62d47ead..cf54f7d9 100644 --- a/yazi-widgets/src/input/input.rs +++ b/yazi-widgets/src/input/input.rs @@ -7,13 +7,14 @@ use yazi_shared::id::Ids; use yazi_shim::path::CROSS_SEPARATOR; 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}}; #[derive(Debug, Default)] pub struct Input { pub size: Size, pub snaps: InputSnaps, + pub history: InputHistory, pub styles: InputStyles, pub obscure: bool, pub realtime: bool, @@ -27,6 +28,7 @@ impl Input { pub fn new(opt: InputOpt) -> Result { let mut input = Self { snaps: InputSnaps::new(opt.value, opt.obscure), + history: InputHistory::new(opt.history), styles: opt.styles, obscure: opt.obscure, realtime: opt.realtime, @@ -89,11 +91,16 @@ impl Input { return false; } 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 } + pub(super) fn flush_all(&mut self) { + self.history.take(); + self.flush_type(); + } + pub(super) fn flush_type(&mut self) { self.ticket.next(); if let Some(cb) = self.cb.as_ref().filter(|_| self.realtime) { diff --git a/yazi-widgets/src/input/mod.rs b/yazi-widgets/src/input/mod.rs index 57b06015..f37955fc 100644 --- a/yazi-widgets/src/input/mod.rs +++ b/yazi-widgets/src/input/mod.rs @@ -1,3 +1,3 @@ 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); diff --git a/yazi-widgets/src/input/option.rs b/yazi-widgets/src/input/option.rs index d3d50c65..3bcc3648 100644 --- a/yazi-widgets/src/input/option.rs +++ b/yazi-widgets/src/input/option.rs @@ -9,6 +9,7 @@ pub struct InputOpt { pub name: String, pub title: String, pub value: String, + pub history: String, pub styles: InputStyles, pub cursor: Option, pub obscure: bool, @@ -46,6 +47,7 @@ impl TryFrom<&Table> for InputOpt { name: t.raw_get("name").unwrap_or_default(), title: t.raw_get("title").unwrap_or_default(), value: t.raw_get("value").unwrap_or_default(), + history: t.raw_get("history").unwrap_or_default(), styles: t.raw_get("styles")?, cursor: None, obscure: t.raw_get("obscure")?,