mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +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
|
## 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)
|
||||||
|
|
|
||||||
|
|
@ -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
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])
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)?;
|
||||||
|
|
|
||||||
|
|
@ -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 = "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" },
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 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 {
|
||||||
|
|
|
||||||
|
|
@ -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!(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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
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);
|
||||||
|
|
|
||||||
|
|
@ -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!());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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!());
|
||||||
|
|
|
||||||
|
|
@ -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!());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ impl Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
act!(r#move, self)?;
|
act!(r#move, self)?;
|
||||||
self.flush_type();
|
self.flush_all();
|
||||||
succ!(render!());
|
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),
|
(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!());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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!());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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_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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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")?,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue