mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 14:51:03 +00:00
feat: input history (#4104)
This commit is contained in:
parent
c92c4aba88
commit
aa850f9a3b
31 changed files with 324 additions and 38 deletions
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: |
|
||||
36
.github/workflows/validate-pr.yml
vendored
Normal file
36
.github/workflows/validate-pr.yml
vendored
Normal 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 }}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(close complete escape show);
|
||||
yazi_macro::mod_flat!(close complete escape recall remember show);
|
||||
|
|
|
|||
32
yazi-actor/src/input/recall.rs
Normal file
32
yazi-actor/src/input/recall.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
yazi-actor/src/input/remember.rs
Normal file
33
yazi-actor/src/input/remember.rs
Normal 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!();
|
||||
}
|
||||
}
|
||||
|
|
@ -310,6 +310,14 @@ keymap = [
|
|||
{ on = "U", run = "casefy upper", desc = "Uppercase" },
|
||||
{ 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
|
||||
{ on = "~", run = "help", desc = "Open help" },
|
||||
{ on = "<F1>", run = "help", desc = "Open help" },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<RawMutex, yazi_widgets::input::Input>),
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
yazi-core/src/input/history.rs
Normal file
31
yazi-core/src/input/history.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<InputAlt>,
|
||||
pub histories: InputHistories,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
|
|
@ -39,9 +40,10 @@ impl Input {
|
|||
|
||||
pub fn lock_mut(&mut self) -> Option<InputMutGuard<'_>> {
|
||||
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,7 +53,7 @@ impl Input {
|
|||
// --- InputMain
|
||||
#[derive(Default)]
|
||||
pub struct InputMain {
|
||||
inner: yazi_widgets::input::Input,
|
||||
pub(super) inner: yazi_widgets::input::Input,
|
||||
pub name: String,
|
||||
pub title: String,
|
||||
pub position: Position,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(guard input);
|
||||
yazi_macro::mod_flat!(guard history input);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(close show);
|
||||
yazi_macro::mod_flat!(close recall show);
|
||||
|
|
|
|||
24
yazi-parser/src/input/recall.rs
Normal file
24
yazi-parser/src/input/recall.rs
Normal 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()) }
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ impl Input {
|
|||
act!(r#move, self, -1)?;
|
||||
}
|
||||
|
||||
self.flush_type();
|
||||
self.flush_all();
|
||||
succ!(render!());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!());
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ impl Input {
|
|||
snap.value = new;
|
||||
|
||||
act!(r#move, self, delta)?;
|
||||
self.flush_type();
|
||||
self.flush_all();
|
||||
succ!(render!());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ impl Input {
|
|||
}
|
||||
|
||||
act!(r#move, self)?;
|
||||
self.flush_type();
|
||||
self.flush_all();
|
||||
succ!(render!());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
40
yazi-widgets/src/input/actor/recall.rs
Normal file
40
yazi-widgets/src/input/actor/recall.rs
Normal 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!());
|
||||
}
|
||||
}
|
||||
|
|
@ -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!());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ impl Input {
|
|||
}
|
||||
|
||||
act!(r#move, self, s.chars().count() as isize)?;
|
||||
self.flush_type();
|
||||
self.flush_all();
|
||||
succ!(render!());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
yazi-widgets/src/input/history.rs
Normal file
18
yazi-widgets/src/input/history.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Self> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<usize>,
|
||||
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")?,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue