Compare commits

...

155 commits

Author SHA1 Message Date
三咲雅 misaki masa
2c3f174eb5
feat: allow restoring trashed items recursively (#4159)
Some checks are pending
Cachix / Publish Flake (push) Waiting to run
Check / clippy (push) Waiting to run
Check / rustfmt (push) Waiting to run
Check / stylua (push) Waiting to run
Draft / build-unix (gcc-aarch64-linux-gnu, ubuntu-latest, aarch64-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (gcc-i686-linux-gnu, ubuntu-latest, i686-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (gcc-riscv64-linux-gnu, ubuntu-latest, riscv64gc-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (gcc-sparc64-linux-gnu, ubuntu-latest, sparc64-unknown-linux-gnu) (push) Waiting to run
Draft / build-unix (macos-latest, aarch64-apple-darwin) (push) Waiting to run
Draft / build-unix (macos-latest, x86_64-apple-darwin) (push) Waiting to run
Draft / build-unix (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Waiting to run
Draft / build-windows (windows-latest, aarch64-pc-windows-msvc) (push) Waiting to run
Draft / build-windows (windows-latest, x86_64-pc-windows-msvc) (push) Waiting to run
Draft / build-musl (aarch64-unknown-linux-musl) (push) Waiting to run
Draft / build-musl (x86_64-unknown-linux-musl) (push) Waiting to run
Draft / build-snap (amd64, ubuntu-latest) (push) Waiting to run
Draft / build-snap (arm64, ubuntu-24.04-arm) (push) Waiting to run
Draft / snap (push) Blocked by required conditions
Draft / draft (push) Blocked by required conditions
Draft / nightly (push) Blocked by required conditions
Test / test (macos-latest) (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
Test / test (windows-latest) (push) Waiting to run
2026-07-25 11:39:19 +08:00
三咲雅 misaki masa
58f1013348
feat: trash bin (#4144)
Some checks failed
Cachix / Publish Flake (push) Has been cancelled
Check / clippy (push) Has been cancelled
Check / rustfmt (push) Has been cancelled
Check / stylua (push) Has been cancelled
Draft / build-unix (gcc-aarch64-linux-gnu, ubuntu-latest, aarch64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-i686-linux-gnu, ubuntu-latest, i686-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-riscv64-linux-gnu, ubuntu-latest, riscv64gc-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-sparc64-linux-gnu, ubuntu-latest, sparc64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (macos-latest, aarch64-apple-darwin) (push) Has been cancelled
Draft / build-unix (macos-latest, x86_64-apple-darwin) (push) Has been cancelled
Draft / build-unix (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-windows (windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
Draft / build-windows (windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
Draft / build-musl (aarch64-unknown-linux-musl) (push) Has been cancelled
Draft / build-musl (x86_64-unknown-linux-musl) (push) Has been cancelled
Draft / build-snap (amd64, ubuntu-latest) (push) Has been cancelled
Draft / build-snap (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Test / test (macos-latest) (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
Test / test (windows-latest) (push) Has been cancelled
Draft / snap (push) Has been cancelled
Draft / draft (push) Has been cancelled
Draft / nightly (push) Has been cancelled
2026-07-23 01:59:39 +08:00
nabsei
6aa75594db
docs: add a note about following the contribution guidelines in the PR template (#4140)
Co-authored-by: 三咲雅 misaki masa <sxyazi@gmail.com>
2026-07-21 08:59:20 +08:00
Omenabaka
60d94608cb
fix: magick preview of multiframe files (#4024)
Co-authored-by: 三咲雅 misaki masa <sxyazi@gmail.com>
2026-07-19 23:58:16 +08:00
三咲雅 misaki masa
52640fcece
feat: new hub VFS kind (#4131) 2026-07-17 03:04:19 +08:00
三咲雅 misaki masa
b25ae9f82d
fix: fail when package source is missing (#4124) 2026-07-14 01:11:34 +08:00
三咲雅 misaki masa
4dab480347
fix: preserve search results on refresh (#4123) 2026-07-13 22:51:42 +08:00
三咲雅 misaki masa
d39637f40c
feat!: support VFS services with same name but different schemes (#4120) 2026-07-13 20:01:47 +08:00
三咲雅 misaki masa
ddc6632505
fix: flush last input before submitting the completion (#4119) 2026-07-13 09:01:59 +08:00
三咲雅 misaki masa
6d84921e70
feat!: custom VFS provider (#4118) 2026-07-13 02:01:37 +08:00
三咲雅 misaki masa
dbb0cc0d55
feat: experimental %y, %Y, %t, %T, %yN, %YN, %tN, %TN shell formatting parameters (#4108) 2026-07-08 17:04:12 +08:00
三咲雅 misaki masa
aa850f9a3b
feat: input history (#4104) 2026-07-06 18:43:08 +08:00
三咲雅 misaki masa
c92c4aba88
feat: make visual mode support wraparound scrolling (#4101) 2026-07-05 18:06:25 +08:00
ebugusey
6e0aaee822
feat: add ya CLI completions to Windows package (#4099) 2026-07-03 22:09:57 +08:00
三咲雅 misaki masa
b8ceaea940
feat!: show file icons in trash/delete/overwrite confirmations (#4096) 2026-07-02 04:09:12 +08:00
三咲雅 misaki masa
21550a7eb5
feat: new target parameter for create action (#4088) 2026-06-28 02:10:23 +08:00
三咲雅 misaki masa
39527f488f
perf: switch memory allocator back to jemalloc (#4084) 2026-06-26 22:38:48 +08:00
三咲雅 misaki masa
de611b0e6b
perf: switch memory allocator to mimalloc (#4083) 2026-06-26 14:34:28 +08:00
三咲雅 misaki masa
ef654f4420
feat: context-aware icons for inputs (#4080) 2026-06-24 23:14:16 +08:00
Alastair
2a106aa231
fix: filter keybindings by chord in command palette (#4078)
Co-authored-by: Alastair Scheuermann <alastairsounds@users.noreply.github.com>
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-06-24 21:50:51 +08:00
SpiritCroc
b1ee2e72ff
feat: H/M/L Vim-like motion for moving cursor relative to viewport (#3970)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-06-24 17:42:20 +08:00
三咲雅 misaki masa
581886a355
feat!: make help menu a command palette (#4074) 2026-06-22 23:57:29 +08:00
三咲雅 misaki masa
4f45ddb514
fix: ignore NumLock state when comparing keybindings (#4071) 2026-06-20 23:57:12 +08:00
三咲雅 misaki masa
0ba1811af3
fix: use cleaned and normalized $PWD as startup CWD to avoid MSYS2/git-bash path quirks (#4068) 2026-06-20 12:47:00 +08:00
三咲雅 misaki masa
feeea673f1
fix: normalize \\?\-prefixed Verbatim paths when creating relative symlinks on Windows (#4067) 2026-06-20 12:07:33 +08:00
三咲雅 misaki masa
ef5db6e257
fix: sort interactive cd candidates case-insensitively so short exact matches match first (#4066) 2026-06-20 11:15:40 +08:00
三咲雅 misaki masa
0daa5df644
fix: use WebP as magick preset preloader cache format to keep image transparency (#4065) 2026-06-20 10:37:24 +08:00
三咲雅 misaki masa
6144d2748e
fix: keep package hashes indifferent to line endings when ya pkg pulls packages (#4064) 2026-06-20 09:34:41 +08:00
三咲雅 misaki masa
84a5be0b6a
feat: Lua API for printing escape sequences to the terminal (#4059) 2026-06-19 23:14:13 +08:00
Alastair
ab8d634f8e
fix: split graceful vs passive stop in terminal stream (#4055)
Co-authored-by: Alastair Scheuermann <alastairsounds@users.noreply.github.com>
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-06-19 09:30:52 +08:00
三咲雅 misaki masa
85132ff343
fix: quit once event stream closes in case terminal won't send a SIGHUP signal (#4052) 2026-06-18 21:21:38 +08:00
三咲雅 misaki masa
53d36b5186
refactor: switch to ratatui-core (#4050) 2026-06-18 16:23:26 +08:00
三咲雅 misaki masa
446a5721bf
refactor: move Lua bindings down into each subcrates (#4049) 2026-06-18 15:06:31 +08:00
三咲雅 misaki masa
f1e93d7f52
feat: new input DDS event watching changes in input content (#4043) 2026-06-14 19:58:23 +08:00
三咲雅 misaki masa
b37be0529a
feat: new ui.Input element (#4040) 2026-06-13 19:37:29 +08:00
三咲雅 misaki masa
9b920ed2e3
refactor: unify rendering behavior with a new renderer (#4038) 2026-06-11 20:44:22 +08:00
三咲雅 misaki masa
07a5deb109
feat: dynamic keymap Lua API (#4031) 2026-06-10 15:09:49 +08:00
Biswapriyo Nath
4086fbb166
feat(nix): shell completions for ya CLI (#4030) 2026-06-10 13:22:31 +08:00
sxyazi
9cfeb67db7
refactor: make Lua bindings Send (#4027) 2026-06-09 22:52:43 +08:00
三咲雅 misaki masa
f6113d3865
feat: advanced app overall style (#4023) 2026-06-07 21:46:07 +08:00
三咲雅 misaki masa
ea7fa2127c
perf: eliminate hacks in image and UI conflict resolution (#4022) 2026-06-06 14:35:12 +08:00
cyanyux
8a4b0b208e
fix(snap): make bundled magick find libgomp at runtime (#4016) 2026-06-06 13:53:59 +08:00
三咲雅 misaki masa
9bdd6777e4
feat: keymap Lua bindings (#4021) 2026-06-06 13:45:52 +08:00
三咲雅 misaki masa
8e80798984
feat: new gait for input backward and forward actions (#4012) 2026-06-01 13:16:01 +08:00
三咲雅 misaki masa
e892bf7d90
fix: decouple yazi_boot crate init from yazi_term init (#4011) 2026-05-31 13:07:11 +08:00
三咲雅 misaki masa
601cb10b32
feat: Unix domain socket on Windows as the underlying transport layer for DDS (#4009) 2026-05-29 15:33:03 +08:00
三咲雅 misaki masa
6c30430666
fix: a TOCTOU race condition while creating temporary directories (#4008) 2026-05-28 18:07:31 +08:00
三咲雅 misaki masa
7a51626505
feat: drag and drop (#4005) 2026-05-28 12:05:06 +08:00
三咲雅 misaki masa
cf8b54179e
feat: terminal drag and drop protocol (#3999) 2026-05-25 10:30:04 +08:00
三咲雅 misaki masa
f7fea2b19a
docs: update contributing guidelines 2026-05-25 09:31:04 +08:00
Dongda Li
d9cd1907d9
feat: image preview with Überzug++ on Niri (#3990)
Co-authored-by: 三咲雅 misaki masa <sxyazi@gmail.com>
2026-05-23 11:02:40 +08:00
JinHyuk Sung
5d643ee01a
fix: restrict preview cache directory permissions (#3984)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-05-22 18:56:19 +08:00
三咲雅 misaki masa
c037855c4f
feat!: new virtual terminal backend (#3989) 2026-05-22 17:00:06 +08:00
三咲雅 misaki masa
3f5cc47a48
fix: normalize main entry IDs for consistent internal sharing (#3966) 2026-05-15 07:30:38 +08:00
Daniel Vincent
fde563380b
feat: bulk create (#3793)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-05-12 02:12:08 +08:00
三咲雅 misaki masa
247f925e53
fix: get rid of the Cargo curse (#3952) 2026-05-10 00:57:57 +08:00
Immanuel Tikhonov
22fb9e0d09
fix: keep repeated ! negations in a condition right-associative (#3946)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-05-09 22:32:18 +08:00
三咲雅 misaki masa
92b9ea3794
fix: make the input component sync and reactive (#3949) 2026-05-09 09:26:07 +08:00
三咲雅 misaki masa
3968c4799c
docs: rename id of the fetcher rule to group (#3943) 2026-05-08 08:57:50 +08:00
三咲雅 misaki masa
aa526434f0
chore: bump to v26.5.6 (#3935) 2026-05-06 02:02:39 +08:00
三咲雅 misaki masa
10fc76db07
feat: custom styles for plugins (#3934) 2026-05-05 06:03:25 +08:00
三咲雅 misaki masa
5ad1e003f2
chore: use Tombi for TOML schema validation (#3932) 2026-05-05 01:37:17 +08:00
三咲雅 misaki masa
62dd22b8da
fix: put package cache in cache dir instead of state dir (#3930) 2026-05-02 22:15:25 +08:00
三咲雅 misaki masa
740d891989
feat: new key-close DDS event (#3925) 2026-05-01 23:09:13 +08:00
三咲雅 misaki masa
1caa5807c6
fix: mock OS-level reaping on Windows to avoid child processes potentially detached (#3922) 2026-04-30 18:50:51 +08:00
三咲雅 misaki masa
c4c533e3ef
feat: connect to SFTP servers with no authentication (#3916)
Co-authored-by: yadokani389 <155296849+yadokani389@users.noreply.github.com>
2026-04-26 16:07:11 +08:00
yadokani389
88848932fc
fix: use cspell package in devShell (#3912) 2026-04-26 15:27:21 +08:00
WINLAIC
a2996908de
feat: drag-resize panes with mouse (#3890)
Co-authored-by: Lingxuan Ye <yelingxuan@xiaomi.com>
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-04-22 19:35:19 +08:00
sxyazi
4a2e5addcd
feat: new app:theme action that hot-reload user themes/flavors (#3906) 2026-04-21 23:34:56 +08:00
三咲雅 misaki masa
369c47539c
feat: dynamic open/opener Lua API (#3901) 2026-04-19 16:58:57 +08:00
三咲雅 misaki masa
d1a27bca1f
feat: warn ya.mgr_emit() of its deprecation (#3902) 2026-04-19 16:51:56 +08:00
三咲雅 misaki masa
ae4c138f49
fix: paste --force doesn't work on existing read-only files with the same name (#3894) 2026-04-17 00:16:29 +08:00
sxyazi
463150848d
feat: dynamic previewer Lua API (#3891) 2026-04-16 17:05:46 +08:00
sxyazi
80a54c3dd5
fix: use u64 for parsing Linux partition blocks (#3873) 2026-04-11 09:25:17 +08:00
三咲雅 misaki masa
9abbc12212
docs: update readme 2026-04-11 08:12:19 +08:00
三咲雅 misaki masa
74d903a49a
feat: expose stash action and give it a new search source (#3872) 2026-04-11 07:59:38 +08:00
三咲雅 misaki masa
f6408656b4
fix!: make ui.Style immutable (#3862) 2026-04-07 11:06:31 +08:00
sxyazi
354cdef907
feat: hover cursor over the new file after extracting (#3854) 2026-04-06 15:32:18 +08:00
sxyazi
ec178fdb52
refactor: unify progress representation and add cleanup state (#3853) 2026-04-05 14:05:42 +08:00
三咲雅 misaki masa
4bb4f37555
refactor: simplify the remote progressive file copier (#3852) 2026-04-04 21:28:57 +08:00
三咲雅 misaki masa
0cedbd9c7b
feat: hover cursor over the new file after copying/cutting/linking/hardlinking (#3846) 2026-04-03 20:45:34 +08:00
sxyazi
ad655eda52
refactor: simplify FromStr implementations with strum 2026-04-03 09:13:52 +08:00
三咲雅 misaki masa
4857d46918
feat: make Action a deserializer (#3841) 2026-04-02 19:15:36 +08:00
三咲雅 misaki masa
6b143c1f4d
fix: enable sysfs files in the preset empty previewer (#3833) 2026-03-31 22:13:14 +08:00
三咲雅 misaki masa
4225c34fea
refactor: pull the process API out into yazi-binding crate (#3830) 2026-03-31 22:04:12 +08:00
三咲雅 misaki masa
607bc0dd2b
refactor: prepare for custom VFS provider (#3821) 2026-03-31 19:18:18 +08:00
Daniel Ramos Rodríguez
2514725e56
fix: handle file URIs in the desktop entry (#3827) 2026-03-31 10:50:12 +08:00
Omenabaka
935eb6b298
feat: allow active tab's inner separator to blend with its background (#3817) 2026-03-27 07:22:20 +08:00
sxyazi
b67798ea19
feat: Vim-like lua action that runs an inline Lua snippet (#3813) 2026-03-25 19:57:31 +08:00
三咲雅 misaki masa
5b2dd41e64
fix: ensure the status bar always has the directory length available (#3810) 2026-03-24 22:12:23 +08:00
三咲雅 misaki masa
e2fae151a5
refactor: simplify the text wrapper (#3808) 2026-03-24 20:14:04 +08:00
sxyazi
5c05350d52
perf: avoid unnecessary allocations in code highlighting (#3804) 2026-03-23 18:06:04 +08:00
三咲雅 misaki masa
d4924ebcad
fix: treat nfs4 mounts as soundless for directory refresh (#3800) 2026-03-22 06:36:34 +08:00
三咲雅 misaki masa
c703332b4b
feat: new bulk_exit action that customizes the prompt for bulk operations (#3792) 2026-03-21 17:26:44 +08:00
Mario Stief
7b61eb1595
feat: new --discard for ya pkg that discard local changes made to packages (#3781)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-03-20 15:47:06 +08:00
三咲雅 misaki masa
a246e9c7c0
feat: make cmp component follow input cursor (#3784) 2026-03-19 21:19:10 +08:00
三咲雅 misaki masa
cc2414728c
feat: new ya.exec() API and ya exec subcommand to execute an action and await its result (#3780) 2026-03-18 19:18:36 +08:00
三咲雅 misaki masa
6a1ddb71e0
feat: deserialize from Data (#3778) 2026-03-18 14:13:23 +08:00
sxyazi
da96795303
refactor: make Clippy happy 2026-03-17 09:41:46 +08:00
三咲雅 misaki masa
de01a56ac5
fix: use edit opener for files with MIME application/wine-extension-ini (#3771) 2026-03-16 13:29:03 +08:00
三咲雅 misaki masa
d22c96b69c
perf: reuse previewed and spotted widgets when possible (#3765) 2026-03-15 15:18:58 +08:00
三咲雅 misaki masa
fa1ee46edc
refactor: implement FromLua instead of TryFrom<Value> (#3760) 2026-03-13 20:28:44 +08:00
三咲雅 misaki masa
3534ba0705
feat: new experimental ya.co() API that creates a coroutine (#3757) 2026-03-12 16:18:30 +08:00
三咲雅 misaki masa
e4829372db
feat: link to the quick-start in yazi --help where lists all common keybinds (#3754) 2026-03-11 13:34:11 +08:00
三咲雅 misaki masa
1be991ec72
docs: update image 2026-03-11 13:06:06 +08:00
IcEarthlight
dee27a2377
feat: new ind-hidden and key-hidden DDS events to change hidden status in Lua (#3748)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-03-10 12:56:07 +08:00
三咲雅 misaki masa
8932ddd66d
perf: preset multi spotter only updates sizes for folders to cut memory usage (#3751) 2026-03-10 12:39:19 +08:00
itsvyle
741f84e22b
feat: certificate authentication for SFTP VFS provider (#3716)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-03-07 15:45:27 +08:00
三咲雅 misaki masa
f10b8910ad
fix: be a little defensive while parsing the output of 7zz -ba (#3744) 2026-03-06 23:09:40 +08:00
Daniel Vincent
dbf81deeab
feat: multi-file spotter (#3733)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-03-06 09:27:32 +08:00
三咲雅 misaki masa
be91b4111c
fix: shift of auto-wrap points in both code and JSON previewers (#3739) 2026-03-05 20:16:03 +08:00
三咲雅 misaki masa
9648d8a43b
fix: wrong intersection check in preview and spot rendering (#3736) 2026-03-04 15:36:13 +08:00
Yashank
b92b576ce1
feat: new hovered condition specifying different icons for hovered files (#3728)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-03-03 20:18:55 +08:00
bitbloxhub
0efeaf5f64
chore: update flake inputs (#3731) 2026-03-03 09:41:15 +08:00
三咲雅 misaki masa
0a83f0bd27
feat: keep span info when config parsing fails (#3727) 2026-03-02 16:47:28 +08:00
三咲雅 misaki masa
3cdc3ecb70
feat: new hey DDS event fires when static messages are restored from persistence (#3725) 2026-02-28 13:01:03 +08:00
三咲雅 misaki masa
884c265de1
fix: avoid blocking on read-half of the watch channel (#3721) 2026-02-26 20:36:22 +08:00
三咲雅 misaki masa
05aeae3b09
fix: ensure cargo-deb picks up the right build artifacts (#3720) 2026-02-26 14:18:00 +08:00
三咲雅 misaki masa
9eb4d03a44
feat: dual watcher for local files (#3713) 2026-02-26 08:16:58 +08:00
三咲雅 misaki masa
0bb6379a2a
docs: update readme 2026-02-26 08:08:36 +08:00
三咲雅 misaki masa
8b773a62a5
fix: make group order of files from flat view stable on select (#3712) 2026-02-23 19:01:04 +08:00
三咲雅 misaki masa
16aa3cd0fc
fix: escape backslashes in ImageMagick font path parameter (#3708) 2026-02-22 19:59:47 +08:00
三咲雅 misaki masa
d84f0b1265
refactor: async Command over emitting shell actions (#3706) 2026-02-22 10:31:15 +08:00
三咲雅 misaki masa
e49cdfe220
fix: fallback in Windows casefold for MS-DOS devices (#3700) 2026-02-19 13:22:14 +08:00
三咲雅 misaki masa
9af792ef69
feat: new --in for search action to set search directory (#3696) 2026-02-18 17:16:05 +08:00
三咲雅 misaki masa
578016e00f
refactor: use the term "action" instead of "command" (#3694) 2026-02-18 17:08:48 +08:00
三咲雅 misaki masa
7f339381f1
feat: new marker_symbol option to specify the symbol used for marking files (#3689) 2026-02-16 21:42:36 +08:00
三咲雅 misaki masa
31cfc58188
feat: new download DDS event fires when remote files are downloaded (#3687) 2026-02-16 12:52:58 +08:00
SuniRein
c0f96f553b
feat: new sort_fallback option to control fallback sorting behavior (#3077)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-02-15 14:00:58 +08:00
三咲雅 misaki masa
13bdb164a4
feat!: new ind-app-title DDS event to customize the app title (#3684) 2026-02-13 13:03:44 +08:00
三咲雅 misaki masa
c3bc4fecad
fix: Chafa v1.18.1 causes random ghost keypresses when previewing images (#3678) 2026-02-12 00:12:42 +08:00
三咲雅 misaki masa
98b1614016
feat: new fs.unique() creates a unique file or directory (#3677) 2026-02-11 12:51:49 +08:00
三咲雅 misaki masa
a8a4b684a1
feat: deduplicate empty directories in preset archive previewer (#3676) 2026-02-11 11:07:23 +08:00
三咲雅 misaki masa
30ec603441
refactor: async fs.access() instead of sync io.open() (#3670) 2026-02-10 11:29:46 +08:00
三咲雅 misaki masa
1a121bbfb0
feat: new fs.access() API to access the filesystem (#3668) 2026-02-09 08:50:01 +08:00
三咲雅 misaki masa
0aeeb3b0f7
feat!: custom tab name (#3666) 2026-02-08 10:46:38 +08:00
sxyazi
dc66f79cef
feat!: finer control over concurrent workers (#3661) 2026-02-06 22:27:08 +08:00
三咲雅 misaki masa
6757fed5aa
feat: new which:dismiss command to dismiss the which component (#3654) 2026-02-02 02:53:02 +08:00
sxyazi
339002ee48
fix: make ya pkg ignore default remote name in user Git config (#3648) 2026-01-31 18:43:25 +08:00
sxyazi
a015d4a7ef
feat: new relay-notify-push DDS event to allow custom notification handlers (#3642) 2026-01-31 09:25:17 +08:00
三咲雅 misaki masa
dfdb235d74
feat: allow using ps.sub() in init.lua directly without a plugin (#3638) 2026-01-30 10:12:21 +08:00
三咲雅 misaki masa
1ea0ea072c
refactor: move Lua bindings into their own yazi-binding crate (#3636) 2026-01-29 23:12:29 +08:00
三咲雅 misaki masa
eaeda8b4fa
perf: reduce memory allocations by using Lua 5.5 external strings (#3634) 2026-01-29 07:58:15 +08:00
三咲雅 misaki masa
cfd9a22125
feat!: upgrade Lua to 5.5 (#3633) 2026-01-29 01:08:22 +08:00
三咲雅 misaki masa
6d942ea011
feat: subscribe to and intercept app-level events through DDS (#3632) 2026-01-29 00:34:28 +08:00
三咲雅 misaki masa
583345296f
refactor: move Term to its own yazi-term crate (#3629) 2026-01-28 22:26:47 +08:00
三咲雅 misaki masa
24c60419bb
refactor: new yazi-emulator crate (#3624) 2026-01-27 22:37:34 +08:00
三咲雅 misaki masa
e0911f2507
refactor: new yazi-tty crate (#3623) 2026-01-27 22:03:48 +08:00
三咲雅 misaki masa
6150571738
fix: ignore target-dir to avoid deadlocks during cargo install (#3620) 2026-01-27 08:49:39 +08:00
三咲雅 misaki masa
592e70a1c9
feat: new cx.which API to access the which component state (#3617) 2026-01-27 01:29:14 +08:00
三咲雅 misaki masa
06c665a086
fix: account for border when calculating message widths (#3615) 2026-01-26 17:23:57 +08:00
三咲雅 misaki masa
92880b844b
refactor: make command data cloneable (#3613) 2026-01-26 07:36:35 +08:00
三咲雅 misaki masa
face6aed40
feat: new ind-which-show DDS event to change the which component behavior (#3608) 2026-01-25 03:06:50 +08:00
Ciarán O'Brien
afc694c122
fix: archive extraction fails for target paths with non-ASCII characters on Windows (#3607)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-01-24 18:03:51 +08:00
Jared Cervantes
49b8324bbf
fix: remove duplicate categories in the .desktop file (#3602)
Signed-off-by: Jared Cervantes <jared@jaredcervantes.com>
2026-01-23 21:14:41 +08:00
1284 changed files with 37819 additions and 20785 deletions

View file

@ -22,10 +22,10 @@ body:
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: debug id: env
attributes: attributes:
label: "`yazi --debug` output" label: "`ya env` output"
description: Please run `yazi --debug` and paste the debug information here. description: Run `ya env` and paste the environment information here.
render: Shell render: Shell
validations: validations:
required: true required: true

View file

@ -26,10 +26,10 @@ body:
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: debug id: env
attributes: attributes:
label: "`yazi --debug` output" label: "`ya env` output"
description: Please run `yazi --debug` and paste the debug information here. description: Run `ya env` and paste the environment information here.
render: Shell render: Shell
validations: validations:
required: true required: true
@ -65,6 +65,6 @@ body:
options: options:
- label: I tried the [latest nightly build](https://yazi-rs.github.io/docs/installation#binaries), and the issue is still reproducible - label: I tried the [latest nightly build](https://yazi-rs.github.io/docs/installation#binaries), and the issue is still reproducible
required: true required: true
- label: I updated the debug information (`yazi --debug`) input box to the nightly that I tried - label: I updated the environment information (`ya env`) field to the nightly that I tried
required: true required: true
- label: I can reproduce it after disabling all custom configs/plugins (`mv ~/.config/yazi ~/.config/yazi-backup`) - label: I can reproduce it after disabling all custom configs/plugins (`mv ~/.config/yazi ~/.config/yazi-backup`)

View file

@ -1,5 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: 🚧 Build Issues
url: https://github.com/sxyazi/yazi/discussions/new?category=3-build-issues
about: If you have issues building Yazi from source code
- name: 📝 Documentation Improvement - name: 📝 Documentation Improvement
url: https://github.com/yazi-rs/yazi-rs.github.io url: https://github.com/yazi-rs/yazi-rs.github.io
about: If you'd like to help improve the documentation about: If you'd like to help improve the documentation

View file

@ -4,10 +4,10 @@ labels: [feature]
assignees: [] assignees: []
body: body:
- type: textarea - type: textarea
id: debug id: env
attributes: attributes:
label: "`yazi --debug` output" label: "`ya env` output"
description: Please run `yazi --debug` and paste the debug information here. description: Run `ya env` and paste the environment information here.
render: Shell render: Shell
validations: validations:
required: true required: true

View file

@ -15,3 +15,10 @@ A clear and concise description of the rationale of the changes, to help our rev
If it has already been detailed in the associated issue, please skip this section. If it has already been detailed in the associated issue, please skip this section.
--> -->
## 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)
<!-- AI bots are not allowed to open PRs in this repository. All PRs must be made by humans and comply with the AI policy. -->

View file

@ -15,13 +15,13 @@ jobs:
os: [ubuntu-latest, macos-latest] os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Install Nix - name: Install Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@v31
- name: Authenticate with Cachix - name: Authenticate with Cachix
uses: cachix/cachix-action@v16 uses: cachix/cachix-action@v17
with: with:
name: yazi name: yazi
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"

View file

@ -17,7 +17,7 @@ jobs:
clippy: clippy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup Rust toolchain - name: Setup Rust toolchain
run: | run: |
@ -25,7 +25,7 @@ jobs:
rustup component add clippy rustup component add clippy
- name: Setup sccache - name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9 uses: mozilla-actions/sccache-action@v0.0.10
- name: Clippy - name: Clippy
run: cargo clippy --all run: cargo clippy --all
@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Setup Rust toolchain - name: Setup Rust toolchain
run: | run: |
@ -42,16 +42,16 @@ jobs:
rustup component add rustfmt --toolchain nightly rustup component add rustfmt --toolchain nightly
- name: Setup sccache - name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9 uses: mozilla-actions/sccache-action@v0.0.10
- name: Rustfmt - name: Rustfmt
run: rustfmt +nightly --check **/*.rs run: find . -name '*.rs' -not -path './target/*' -exec rustfmt +nightly --check {} +
stylua: stylua:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: JohnnyMorganz/stylua-action@v4 - uses: JohnnyMorganz/stylua-action@v5
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
version: latest version: latest

View file

@ -42,7 +42,7 @@ jobs:
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
CARGO_TARGET_SPARC64_UNKNOWN_LINUX_GNU_LINKER: sparc64-linux-gnu-gcc CARGO_TARGET_SPARC64_UNKNOWN_LINUX_GNU_LINKER: sparc64-linux-gnu-gcc
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Install gcc - name: Install gcc
if: matrix.gcc != '' if: matrix.gcc != ''
@ -53,13 +53,13 @@ jobs:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Setup sccache - name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9 uses: mozilla-actions/sccache-action@v0.0.10
- name: Build - name: Build
run: ./scripts/build.sh ${{ matrix.target }} run: ./scripts/build.sh ${{ matrix.target }}
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.target }} name: ${{ matrix.target }}
path: | path: |
@ -81,14 +81,14 @@ jobs:
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: lld-link.exe CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: lld-link.exe
CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER: lld-link.exe CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER: lld-link.exe
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Setup sccache - name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9 uses: mozilla-actions/sccache-action@v0.0.10
- name: Build - name: Build
run: cargo build --profile release-windows --locked --target ${{ matrix.target }} run: cargo build --profile release-windows --locked --target ${{ matrix.target }}
@ -97,15 +97,16 @@ jobs:
env: env:
TARGET_NAME: yazi-${{ matrix.target }} TARGET_NAME: yazi-${{ matrix.target }}
run: | run: |
New-Item -ItemType Directory -Path ${env:TARGET_NAME} New-Item -ItemType Directory -Path "${env:TARGET_NAME}\completions"
Copy-Item -Path "target\${{ matrix.target }}\release-windows\ya.exe" -Destination ${env:TARGET_NAME} Copy-Item -Path "target\${{ matrix.target }}\release-windows\ya.exe" -Destination ${env:TARGET_NAME}
Copy-Item -Path "target\${{ matrix.target }}\release-windows\yazi.exe" -Destination ${env:TARGET_NAME} Copy-Item -Path "target\${{ matrix.target }}\release-windows\yazi.exe" -Destination ${env:TARGET_NAME}
Copy-Item -Path "yazi-boot\completions" -Destination ${env:TARGET_NAME} -Recurse Copy-Item -Path "yazi-cli\completions\*" -Destination "${env:TARGET_NAME}\completions"
Copy-Item -Path "yazi-boot\completions\*" -Destination "${env:TARGET_NAME}\completions"
Copy-Item -Path "README.md", "LICENSE" -Destination ${env:TARGET_NAME} Copy-Item -Path "README.md", "LICENSE" -Destination ${env:TARGET_NAME}
Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip" Compress-Archive -Path ${env:TARGET_NAME} -DestinationPath "${env:TARGET_NAME}.zip"
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.target }} name: ${{ matrix.target }}
path: yazi-${{ matrix.target }}.zip path: yazi-${{ matrix.target }}.zip
@ -120,20 +121,20 @@ jobs:
container: container:
image: docker://ghcr.io/cross-rs/${{ matrix.target }}:edge image: docker://ghcr.io/cross-rs/${{ matrix.target }}:edge
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Setup sccache - name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9 uses: mozilla-actions/sccache-action@v0.0.10
- name: Build - name: Build
run: ./scripts/build.sh ${{ matrix.target }} run: ./scripts/build.sh ${{ matrix.target }}
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.target }} name: ${{ matrix.target }}
path: | path: |
@ -150,7 +151,7 @@ jobs:
arch: arm64 arch: arm64
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@ -167,7 +168,7 @@ jobs:
run: mv yazi_*.snap yazi-${{ matrix.arch }}.snap run: mv yazi_*.snap yazi-${{ matrix.arch }}.snap
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: snap-${{ matrix.arch }} name: snap-${{ matrix.arch }}
path: yazi-${{ matrix.arch }}.snap path: yazi-${{ matrix.arch }}.snap
@ -176,7 +177,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build-snap] needs: [build-snap]
steps: steps:
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v8
with: with:
pattern: snap-* pattern: snap-*
merge-multiple: true merge-multiple: true
@ -205,12 +206,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build-unix, build-windows, build-musl, build-snap] needs: [build-unix, build-windows, build-musl, build-snap]
steps: steps:
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v8
with: with:
merge-multiple: true merge-multiple: true
- name: Draft - name: Draft
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
draft: true draft: true
files: | files: |
@ -233,9 +234,9 @@ jobs:
echo "Nightly changelog: https://github.com/sxyazi/yazi/blob/main/CHANGELOG.md#unreleased" >> $GITHUB_ENV echo "Nightly changelog: https://github.com/sxyazi/yazi/blob/main/CHANGELOG.md#unreleased" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v8
with: with:
merge-multiple: true merge-multiple: true
@ -246,7 +247,7 @@ jobs:
git tag --force nightly && git push --force origin tag nightly git tag --force nightly && git push --force origin tag nightly
- name: Nightly - name: Nightly
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
tag_name: nightly tag_name: nightly
prerelease: true prerelease: true

View file

@ -11,7 +11,7 @@ jobs:
winget: winget:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v8
with: with:
merge-multiple: true merge-multiple: true
@ -25,7 +25,7 @@ jobs:
snapcraft: snapcraft:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v8
with: with:
merge-multiple: true merge-multiple: true

View file

@ -21,13 +21,13 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup Rust toolchain - name: Setup Rust toolchain
run: rustup toolchain install stable --profile minimal run: rustup toolchain install stable --profile minimal
- name: Setup sccache - name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9 uses: mozilla-actions/sccache-action@v0.0.10
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose

View file

@ -1,4 +1,4 @@
name: Validate Form name: Validate Issue
on: on:
issues: issues:
@ -12,10 +12,10 @@ jobs:
permissions: permissions:
issues: write issues: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v7
with: with:
node-version: 20 node-version: 20
@ -24,8 +24,8 @@ jobs:
cd scripts/validate-form cd scripts/validate-form
npm ci npm ci
- name: Validate Form - name: Validate Issue
uses: actions/github-script@v8 uses: actions/github-script@v9
with: with:
script: | script: |
const script = require('./scripts/validate-form/main.js') const script = require('./scripts/validate-form/main.js')

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

@ -0,0 +1,36 @@
name: Validate PR
on:
pull_request:
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@v7
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

@ -1,6 +1,9 @@
{ {
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime.version": "Lua 5.4", "runtime.version": "Lua 5.5",
"runtime.special": {
"fail": "error"
},
"workspace.library": ["~/.config/yazi/plugins/types.yazi/"], "workspace.library": ["~/.config/yazi/plugins/types.yazi/"],
"diagnostics.disable": ["redefined-local"] "diagnostics.disable": ["redefined-local"]
} }

45
AGENTS.md Normal file
View file

@ -0,0 +1,45 @@
# AGENTS.md
## Rules
- Applies repo-wide. Keep changes scoped; do not create issues or pull requests, or post comments.
## Project
- Rust 2024 Cargo workspace containing all `yazi-*` crates; default members are `yazi-fm` (`yazi`) and `yazi-cli` (`ya`).
## Style
- Follow nearby code and use idiomatic Rust and Lua. Rust uses `snake_case` for modules, functions, and fields and `PascalCase` for types, traits, and variants. Lua uses PascalCase component tables, `local M` plugin modules, `snake_case` methods/locals, and `_name` private fields.
- Preserve established terms and type families: `Url`/`UrlBuf`/`UrlCow`, `PathDyn`/`PathBufDyn`/`PathCow`, `*Ref`, `*Arc`, `*Opt`, `*State`, `*Job`, `*Prog`, `File`, `Folder`, `Tab`, `Mgr`, and `Task`. Use `Url` for logical locations and `Path` for filesystem paths.
- Reuse established plugin and event names (`fetch`, `preload`, `peek`, `seek`, `spot`, `entry`, `setup`, `yank`, `hover`, and `select`) across Rust, Lua, and configuration.
- Use Rust prefixes (`as_`, `to_`, `into_`, `try_`, `is_`, `has_`) according to their usual semantics; prefer descriptive names.
- Name variables, modules, methods, and other symbols simply, elegantly, and expressively. Be creative while keeping names clear, consistent with established terminology, and idiomatic.
- When passing arguments, use the parameter's conversion traits directly (such as `Into<_>` or `AsRef<_>`); avoid eager conversions like `.to_string()`, `.to_owned()`, and `.as_ref()` unless ownership, type inference, or semantics require them.
- Prefer general-purpose traits and conversion APIs already provided by the codebase or its dependencies over manual construction or adapter closures; for example, use `into_lua()` where applicable.
## Code Changes
- Search and reuse first. For new features, extend existing infrastructure or data structures with general, reusable capabilities when that keeps the final code concise.
- For refactors, inspect the whole target module and its callers first. Look for duplicated work, redundant I/O, underpowered return values, one-use wrappers, and reusable cross-platform abstractions; implement high-confidence, behavior-preserving simplifications while preserving error, fallback, and platform semantics.
- Keep diffs minimal and avoid unrelated refactors. Prefer clear code over custom patterns or comments; comment only behavior the code cannot explain.
- Put reusable code in the lowest suitable shared layer; avoid unnecessary dependencies and allocations. Prefer borrowed values and existing wrappers.
- Use stable Rust APIs; nightly is formatting-only. Use only `pub`, `pub(super)`, and `pub(crate)`—never `pub(in ...)`.
- Keep async I/O non-blocking, preserve platform/fork behavior, and follow existing error boundaries with `?`.
- For renames or refactors, update all related variables, functions, parameters, modules, methods, types, derived types, exports, tests, configuration keys, documentation, and Lua bindings.
- Do not add or modify tests unless requested.
## Validation
- Prefer targeted debug checks; use multiple `-p` flags for affected crates before the whole workspace.
```sh
cargo check -p <package>
cargo test -p <package>
cargo clippy -p <package>
find . -name '*.rs' -not -path './target/*' -exec rustfmt +nightly --check {} +
stylua --color always --check .
```
- Use `cargo check` instead of `cargo build` unless artifacts are needed. Do not use `--release` unless requested; use `scripts/build.sh <target>` for release or cross-target packaging.
- Run relevant existing tests when needed, then inspect `git diff` and verify that only intended files changed.

View file

@ -12,6 +12,112 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
## [Unreleased] ## [Unreleased]
### Added
- Drag and drop ([#4005])
- Trash bin ([#4144])
- Bulk create ([#3793])
- Make help menu a command palette ([#4074])
- Input history ([#4104])
- Experimental `%y`, `%Y`, `%t`, `%T`, `%yN`, `%YN`, `%tN`, `%TN` shell formatting parameters ([#4108])
- Custom VFS provider ([#4118])
- 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])
- Show file icons in trash/delete/overwrite confirmations ([#4096])
- Dynamic keymap Lua API ([#4031])
- New `ui.Input` element ([#4040])
- Image preview with Überzug++ on Niri ([#3990])
- New gait for input `backward` and `forward` actions ([#4012])
### Changed
- Rename SFTP sections in `vfs.toml` from `[services.domain]` to `[sftp.domain]` ([#4120]).
- Rename `<BackTab>` to `<S-Tab>` ([#3989])
- Remove `Url.is_archive` - `archive://` is no longer built in and can now be registered by plugins ([#4118])
- Make `mgr::Yanked`, `tab::Selected`, and the `@yank` DDS event return `File` instead of `Url` from `__pairs()` ([#4096])
- Remove `help:filter` action since the filter input is now always available ([#4074])
- `[help]` of `theme.toml`: supersede `on` with `chord`, supersede `run` and `desc` with `action`, remove `footer` ([#4074])
- Remove Legacy Console Mode on Windows ([#3989])
### Deprecated
- Deprecate `backward --far` and `forward --far` in favor of `backward wide` and `forward wide`, respectively ([#4012])
- Deprecate `tab::Mode.is_visual` in favor of the new `tab::Mode.is_normal` ([#4101])
- Deprecate `Url.is_regular`, `Url.is_search`, and `Url.domain` in favor of `Url.spec.is_regular`, `Url.spec.is_search`, and `Url.spec.domain`, respectively ([#4118])
### Fixed
- Normalize `\\?\`-prefixed Verbatim paths when creating relative symlinks on Windows ([#4067])
- Keep package hashes indifferent to line endings when `ya pkg` pulls packages ([#4064])
- Use WebP as `magick` preset preloader cache format to keep image transparency ([#4065])
- Use cleaned and normalized `$PWD` as startup CWD to avoid MSYS2/git-bash path quirks ([#4068])
### Improved
- Eliminate hacks in image and UI conflict resolution ([#4022])
## [v26.5.6]
### Added
- Custom tab name ([#3666])
- New `--in` for `search` action to set search directory ([#3696])
- Hover cursor over the new file after copying/cutting/linking/hardlinking/extracting ([#3846], [#3854])
- Drag-resize panes with mouse ([#3890])
- Multi-file spotter ([#3733])
- New `app:theme` action that hot-reload user themes/flavors ([#3906])
- Dynamic open/opener Lua API ([#3901])
- Dynamic previewer Lua API ([#3891])
- Custom styles for plugins ([#3934])
- Vim-like `lua` action that runs an inline Lua snippet ([#3813])
- Certificate authentication for SFTP VFS provider ([#3716])
- New `hovered` condition specifying different icons for hovered files ([#3728])
- Allow using `ps.sub()` in `init.lua` directly without a plugin ([#3638])
- New `ya.exec()` API and `ya exec` subcommand to execute an action and await its result ([#3780])
- New `sort_fallback` option to control fallback sorting behavior ([#3077])
- New `fs.access()` API to access the filesystem ([#3668])
- New `relay-notify-push` DDS event to customize the notification handler ([#3642])
- New `ind-app-title` DDS event to customize the app title ([#3684])
- New `ind-hidden` and `key-hidden` DDS events to change hidden status in Lua ([#3748])
- New `marker_symbol` option to specify the symbol used for marking files ([#3689])
- New `--discard` for `ya pkg` that discard local changes made to packages ([#3781])
- New `bulk_exit` action that customizes the prompt for bulk operations ([#3792])
- New `fs.unique()` creates a unique file or directory ([#3677])
- New `download` DDS event fires when remote files are downloaded ([#3687])
- New `ind-which-activate` DDS event to change the which component behavior ([#3608])
- New `hey` DDS event fires when static messages are restored from persistence ([#3725])
- New `cx.which` API to access the which component state ([#3617])
- New experimental `ya.co()` API that creates a coroutine ([#3757])
### Changed
- Upgrade Lua to 5.5 ([#3633])
- Change preset <kbd>t</kbd> for creating tabs to <kbd>t</kbd><kbd>t</kbd> to avoid conflict with new <kbd>t</kbd><kbd>r</kbd> for renaming tabs ([#3666])
- Make `ui.Style` immutable ([#3862])
- Remove `title_format` in favor of new `ind-app-title` DDS event for flexible title customization ([#3684])
- Remove `micro_workers` and `macro_workers` in favor of finer control over concurrent workers ([#3661])
- Rename `id` of the fetcher rule to `group` ([#3943])
### Deprecated
- Deprecate `fs.unique_name()` in favor of `fs.unique()` to fix a TOCTOU race condition ([#3677])
### Fixed
- Chafa v1.18.1 causes random ghost keypresses when previewing images ([#3678])
- `paste --force` doesn't work on existing read-only files with the same name ([#3894])
- Be a little defensive while parsing the output of `7zz -ba` ([#3744])
- Make `ya pkg` ignore default remote name in user Git config ([#3648])
- Archive extraction fails for target paths with non-ASCII characters on Windows ([#3607])
- Escape backslashes in ImageMagick font path parameter ([#3708])
### Improved
- Reduce memory allocations by using Lua 5.5 external strings ([#3634])
- Reuse previewed and spotted widgets when possible ([#3765])
- Avoid unnecessary allocations in code highlighting ([#3804])
## [v26.1.22] ## [v26.1.22]
### Added ### Added
@ -57,11 +163,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- New experimental `ya.async()` API ([#3422]) - New experimental `ya.async()` API ([#3422])
- New `overall` option to set the overall background color ([#3317]) - New `overall` option to set the overall background color ([#3317])
- Rounded corners for indicator bar ([#3419]) - Rounded corners for indicator bar ([#3419])
- New `bulk_rename` command always renames files with the editor ([#2984]) - New `bulk_rename` action always renames files with the editor ([#2984])
- `key-*` DDS events to allow changing or canceling user key events ([#3005], [#3037]) - `key-*` DDS events to allow changing or canceling user key events ([#3005], [#3037])
- New `--bg` specifying image background color in the preset SVG and ImageMagick previewers ([#3189]) - New `--bg` specifying image background color in the preset SVG and ImageMagick previewers ([#3189])
- `filter` by full path (prefix + filename) in search view instead of just filename ([#2915]) - `filter` by full path (prefix + filename) in search view instead of just filename ([#2915])
- New `casefy` command for case conversion of the input content ([#3235]) - New `casefy` action for case conversion of the input content ([#3235])
- Allow dynamic adjustment of layout ratio via `rt.mgr.ratio` ([#2964]) - Allow dynamic adjustment of layout ratio via `rt.mgr.ratio` ([#2964])
- Support `.deb` packages ([#2807], [#3128], [#3209]) - Support `.deb` packages ([#2807], [#3128], [#3209])
- Port several widespread GUI keys to the input component ([#2849]) - Port several widespread GUI keys to the input component ([#2849])
@ -82,7 +188,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Rename `name` to `url` for open, fetchers, spotters, preloaders, previewers, filetype, and `globs` icon rules to support virtual file system ([#3034]) - Rename `name` to `url` for open, fetchers, spotters, preloaders, previewers, filetype, and `globs` icon rules to support virtual file system ([#3034])
- Rename `mime` fetcher to `mime.local`, and introduce `mime.dir` fetcher to support folder MIME types ([#3222]) - Rename `mime` fetcher to `mime.local`, and introduce `mime.dir` fetcher to support folder MIME types ([#3222])
- Reclassify `hovered` and `preview_hovered` under `[mgr]` of `theme.toml` into `[indicator]` as `current` and `preview`, respectively ([#3419]) - Reclassify `hovered` and `preview_hovered` under `[mgr]` of `theme.toml` into `[indicator]` as `current` and `preview`, respectively ([#3419])
- Remove `$0` parameter in opener rules to make the `open` command work under empty directories ([#3226]) - Remove `$0` parameter in opener rules to make the `open` action work under empty directories ([#3226])
- Return `Path` instead of `Url` from `Url:strip_prefix()` and `File.link_to` to enforce type safety ([#3361], [#3385]) - Return `Path` instead of `Url` from `Url:strip_prefix()` and `File.link_to` to enforce type safety ([#3361], [#3385])
- Use `body` instead of the term `content` in confirmations ([#2921]) - Use `body` instead of the term `content` in confirmations ([#2921])
- Use `u16` instead of `u32` as the type of `max_width` and `max_height` options to avoid memory exhaustion ([#3313]) - Use `u16` instead of `u32` as the type of `max_width` and `max_height` options to avoid memory exhaustion ([#3313])
@ -90,7 +196,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Deprecated ### Deprecated
- Deprecate `$n`, `$@` (\*nix) and `%n`, `%*` (Windows) in `shell` command and opener rules in favor of new shell formatting ([#3232]) - Deprecate `$n`, `$@` (\*nix) and `%n`, `%*` (Windows) in `shell` action and opener rules in favor of new shell formatting ([#3232])
- Deprecate `ya.hide`, `ya.render`, and `ya.truncate` in favor of `ui.hide`, `ui.render`, and `ui.truncate` ([#2939]) - Deprecate `ya.hide`, `ya.render`, and `ya.truncate` in favor of `ui.hide`, `ui.render`, and `ui.truncate` ([#2939])
- Deprecate `position` property of `ya.input()` in favor of `pos` to align with `ya.confirm()` and its type `ui.Pos` ([#2921]) - Deprecate `position` property of `ya.input()` in favor of `pos` to align with `ya.confirm()` and its type `ui.Pos` ([#2921])
- Deprecate `cx.tasks.progress` in favor of `cx.tasks.summary` ([#3131]) - Deprecate `cx.tasks.progress` in favor of `cx.tasks.summary` ([#3131])
@ -101,7 +207,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- User-prepended open rules do not override presets ([#3360]) - User-prepended open rules do not override presets ([#3360])
- Respect user's system media opener instead of hardcoding `mpv` ([#2959]) - Respect user's system media opener instead of hardcoding `mpv` ([#2959])
- Incorrect `$0` and `$@` parameters in `shell` command under empty directories ([#3225]) - Incorrect `$0` and `$@` parameters in `shell` action under empty directories ([#3225])
- Avoid appending a newline when reading clipboard contents ([#3059]) - Avoid appending a newline when reading clipboard contents ([#3059])
- Renew package `rev` only when it's empty ([#3200]) - Renew package `rev` only when it's empty ([#3200])
- Suspend only when there is a parent process ([#3008]) - Suspend only when there is a parent process ([#3008])
@ -155,15 +261,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- New `ya.emit()` API ([#2653]) - New `ya.emit()` API ([#2653])
- New `fs.calc_size()` API ([#2691]) - New `fs.calc_size()` API ([#2691])
- Allow custom exit code with `quit --code` ([#2609]) - Allow custom exit code with `quit --code` ([#2609])
- New `--hovered` for the `copy` command ([#2709]) - New `--hovered` for the `copy` action ([#2709])
- `s` and `S` keybinds in the input component ([#2678]) - `s` and `S` keybinds in the input component ([#2678])
- Limit memory usage for previewing large images ([#2602]) - Limit memory usage for previewing large images ([#2602])
- Show error when image preview fails ([#2706]) - Show error when image preview fails ([#2706])
- New `ui.Align`, `ui.Wrap`, and `ui.Edge` ([#2802]) - New `ui.Align`, `ui.Wrap`, and `ui.Edge` ([#2802])
- Make `ui.Line` renderable ([#2743]) - Make `ui.Line` renderable ([#2743])
- Checks in `ya pub` and `ya emit` subcommands to verify receiver exists and has necessary abilities ([#2696]) - Checks in `ya pub` and `ya emit` subcommands to verify receiver exists and has necessary abilities ([#2696])
- Make the hover state for `reveal`, `sort`, and `hidden` commands stable ([#2657]) - Make the hover state for `reveal`, `sort`, and `hidden` actions stable ([#2657])
- New `--no-dummy` option for `reveal` command ([#2664]) - New `--no-dummy` option for `reveal` action ([#2664])
- Fall back to `CSI 16 t` when PowerShell OpenSSH returns a fake terminal size ([#2636]) - Fall back to `CSI 16 t` when PowerShell OpenSSH returns a fake terminal size ([#2636])
### Changed ### Changed
@ -207,7 +313,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Enhance `fzf` integration ([#2553]) - Enhance `fzf` integration ([#2553])
- Platform-specific key binding ([#2526]) - Platform-specific key binding ([#2526])
- Custom search engine Lua API ([#2452]) - Custom search engine Lua API ([#2452])
- New `follow` command to follow files pointed to by symlinks ([#2543]) - New `follow` action to follow files pointed to by symlinks ([#2543])
- Allow `tab_swap` to cycle tabs ([#2456]) - Allow `tab_swap` to cycle tabs ([#2456])
- Show error message when directory fails to load ([#2527]) - Show error message when directory fails to load ([#2527])
- New `symlink_target` to style the target of symbolic links ([#2522]) - New `symlink_target` to style the target of symbolic links ([#2522])
@ -215,7 +321,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- New `<C-A>` and `<C-E>` keybindings to select entire line for the input component ([#2439]) - New `<C-A>` and `<C-E>` keybindings to select entire line for the input component ([#2439])
- New `fs.expand_url()` API ([#2476]) - New `fs.expand_url()` API ([#2476])
- New `ui.Text:scroll()` API for setting text to scroll horizontally or vertically ([#2589]) - New `ui.Text:scroll()` API for setting text to scroll horizontally or vertically ([#2589])
- Allow initializing input when opening it with commands like `rename`, `create`, `find`, `filter`, etc. ([#2578]) - Allow initializing input when opening it with actions like `rename`, `create`, `find`, `filter`, etc. ([#2578])
- New `@sync peek` annotation for sync previewers ([#2487]) - New `@sync peek` annotation for sync previewers ([#2487])
- New `ya.id("app")` to get `YAZI_ID` in plugins ([#2503]) - New `ya.id("app")` to get `YAZI_ID` in plugins ([#2503])
- New `base` field for the `Url` ([#2492]) - New `base` field for the `Url` ([#2492])
@ -225,7 +331,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Changed ### Changed
- Navigation wraparound with new `arrow prev` and `arrow next` commands ([#2485], [#2540]) - Navigation wraparound with new `arrow prev` and `arrow next` actions ([#2485], [#2540])
- Swap default key bindings for fzf and zoxide ([#2546]) - Swap default key bindings for fzf and zoxide ([#2546])
- Switch to `resvg` as the SVG renderer ([#2581]) - Switch to `resvg` as the SVG renderer ([#2581])
- Make `frag`, `name`, `stem`, `ext`, and `parent` on `Url`, `name` on `tab::Tab`, and `is_hovered` on `fs::File` properties ([#2572]) - Make `frag`, `name`, `stem`, `ext`, and `parent` on `Url`, `name` on `tab::Tab`, and `is_hovered` on `fs::File` properties ([#2572])
@ -267,11 +373,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Added ### Added
- Allow to specify layer for keymap commands ([#2399]) - Allow to specify layer for keymap actions ([#2399])
- New `rt` and `th` allow to access user configuration and theme scheme in sync/async plugins consistently ([#2389], [#2392], [#2393], [#2397]) - New `rt` and `th` allow to access user configuration and theme scheme in sync/async plugins consistently ([#2389], [#2392], [#2393], [#2397])
- New `tbl_col` and `tbl_cell` in theme system for spotter table styling ([#2391]) - New `tbl_col` and `tbl_cell` in theme system for spotter table styling ([#2391])
- Allow different separators to be applied individually to the left and right sides of the status bar ([#2313]) - Allow different separators to be applied individually to the left and right sides of the status bar ([#2313])
- `ripgrep-all` support for the `search` command ([#2383]) - `ripgrep-all` support for the `search` action ([#2383])
- Respect the user's `max_width` setting in the preset PDF preloader ([#2331]) - Respect the user's `max_width` setting in the preset PDF preloader ([#2331])
- Respect the user's `wrap` setting in the preset JSON previewer ([#2337]) - Respect the user's `wrap` setting in the preset JSON previewer ([#2337])
- Respect the user's `image_alloc` setting in the preset ImageMagick previewer ([#2403]) - Respect the user's `image_alloc` setting in the preset ImageMagick previewer ([#2403])
@ -312,15 +418,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Mount manager ([#2199]) - Mount manager ([#2199])
- New `ya.confirm()` API ([#2095]) - New `ya.confirm()` API ([#2095])
- New `arrow top` and `arrow bot` commands to jump to the top and bottom ([#2294]) - New `arrow top` and `arrow bot` actions to jump to the top and bottom ([#2294])
- Support end of options (`--`) marker for all commands ([#2298]) - Support end of options (`--`) marker for all actions ([#2298])
- Replace mode and Vim motions (`W`, `E`, `B`, `^`, `_`) for inputs ([#2143]) - Replace mode and Vim motions (`W`, `E`, `B`, `^`, `_`) for inputs ([#2143])
- New `ya pack -d` subcommand to delete packages ([#2181]) - New `ya pack -d` subcommand to delete packages ([#2181])
- `ya pack` supports adding and deleting multiple packages at once ([#2257]) - `ya pack` supports adding and deleting multiple packages at once ([#2257])
- Theme support for the spotter border and title ([#2002]) - Theme support for the spotter border and title ([#2002])
- Use positional argument instead of `--args` for the `plugin` command ([#2299]) - Use positional argument instead of `--args` for the `plugin` action ([#2299])
- Support and hide Windows system files by default ([#2149]) - Support and hide Windows system files by default ([#2149])
- New `--no-cwd-file` option for the `close` command ([#2185]) - New `--no-cwd-file` option for the `close` action ([#2185])
- Prompt users missing fzf in the zoxide plugin ([#2122]) - Prompt users missing fzf in the zoxide plugin ([#2122])
- More decent package locking mechanism ([#2168]) - More decent package locking mechanism ([#2168])
- Custom modal component API ([#2205]) - Custom modal component API ([#2205])
@ -336,7 +442,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Deprecated ### Deprecated
- Deprecate `--args` in the `plugin` command in favor of a 2nd positional parameter ([#2299]) - Deprecate `--args` in the `plugin` action in favor of a 2nd positional parameter ([#2299])
- Deprecate plugin entry file `init.lua` in favor of `main.lua` ([#2168]) - Deprecate plugin entry file `init.lua` in favor of `main.lua` ([#2168])
- Deprecate `arrow -99999999` and `arrow 99999999` in favor of `arrow top` and `arrow bot` ([#2294]) - Deprecate `arrow -99999999` and `arrow 99999999` in favor of `arrow top` and `arrow bot` ([#2294])
- Deprecate the numeric return value of preloaders in favor of a boolean return value ([#2253]) - Deprecate the numeric return value of preloaders in favor of a boolean return value ([#2253])
@ -363,7 +469,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- More supported archive formats to the preset config ([#1926]) - More supported archive formats to the preset config ([#1926])
- New `fs.create()` Lua API ([#2068]) - New `fs.create()` Lua API ([#2068])
- New `--cwd` parameter for the `shell` command and `fs.cwd()` API ([#2060]) - New `--cwd` parameter for the `shell` action and `fs.cwd()` API ([#2060])
- Allow `noop` for single-key chords by removing the mixing length limit ([#2064]) - Allow `noop` for single-key chords by removing the mixing length limit ([#2064])
- Support for Android platform in the `for` qualifier of opener ([#2041]) - Support for Android platform in the `for` qualifier of opener ([#2041])
@ -392,8 +498,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Spotter ([#1802]) - Spotter ([#1802])
- Support transparent image preview ([#1556]) - Support transparent image preview ([#1556])
- Auto switch between dark and light icons/flavors based on terminal backgrounds ([#1946]) - Auto switch between dark and light icons/flavors based on terminal backgrounds ([#1946])
- Allow disabling certain preset keybinds with the new `noop` virtual command ([#1882]) - Allow disabling certain preset keybinds with the new `noop` virtual action ([#1882])
- New `ya emit` and `ya emit-to` subcommands to emit commands to a specified instance for execution ([#1979]) - New `ya emit` and `ya emit-to` subcommands to emit actions to a specified instance for execution ([#1979])
- Custom styles for the `confirm` component ([#1789]) - Custom styles for the `confirm` component ([#1789])
- Make the builtin `extract` plugin support compressed tarballs (`*.tar.gz`, `*.tar.bz2`, etc.) ([#1583]) - Make the builtin `extract` plugin support compressed tarballs (`*.tar.gz`, `*.tar.bz2`, etc.) ([#1583])
- Launch from preset settings if the user's config cannot be parsed ([#1832]) - Launch from preset settings if the user's config cannot be parsed ([#1832])
@ -427,15 +533,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Use an `Error` userdata instead of a plain error code for I/O errors ([#1939]) - Use an `Error` userdata instead of a plain error code for I/O errors ([#1939])
- Remove `ui.ListItem` since it's no longer necessary ([#1772]) - Remove `ui.ListItem` since it's no longer necessary ([#1772])
- Decouple coordinates from `ui.List`, `ui.Bar`, `ui.Border`, and `ui.Gauge` ([#1782]) - Decouple coordinates from `ui.List`, `ui.Bar`, `ui.Border`, and `ui.Gauge` ([#1782])
- Make `backspace` command not close the input even when value is empty ([#1680]) - Make `backspace` action not close the input even when value is empty ([#1680])
- Remove the meaningless `--confirm` option to simplify the `shell` command ([#1982]) - Remove the meaningless `--confirm` option to simplify the `shell` action ([#1982])
- Use `dark` and `light` instead of `use` under `[flavor]` to support auto-switching between light and dark modes ([#1946]) - Use `dark` and `light` instead of `use` under `[flavor]` to support auto-switching between light and dark modes ([#1946])
- Unify the `fg_dark` and `fg_light` into one `fg` since `fg_light` is redundant and never used ([#1946]) - Unify the `fg_dark` and `fg_light` into one `fg` since `fg_light` is redundant and never used ([#1946])
- Extend the available styles for `mode` by separating `mode` from the `separator` styles ([#1953]) - Extend the available styles for `mode` by separating `mode` from the `separator` styles ([#1953])
### Deprecated ### Deprecated
- Deprecate `--sync` option for the `plugin` command ([#1891]) - Deprecate `--sync` option for the `plugin` action ([#1891])
- Deprecate `ui.Paragraph` in favor of `ui.Text` ([#1776]) - Deprecate `ui.Paragraph` in favor of `ui.Text` ([#1776])
- Deprecate the task info of `peek()`, `seek()`, and `preload()` from `self` in favor of a `job` parameter ([#1966]) - Deprecate the task info of `peek()`, `seek()`, and `preload()` from `self` in favor of a `job` parameter ([#1966])
- Deprecate parameter list of `entry()` from its first argument in favor of a `job` parameter ([#1966]) - Deprecate parameter list of `entry()` from its first argument in favor of a `job` parameter ([#1966])
@ -492,7 +598,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- New confirm component ([#1167]) - New confirm component ([#1167])
- Word wrapping in `code` previewer ([#1159]) - Word wrapping in `code` previewer ([#1159])
- New `--dir` option for `create` command ([#1505]) - New `--dir` option for `create` action ([#1505])
- New `ext()` method for `Url` ([#1528]) - New `ext()` method for `Url` ([#1528])
- Make the builtin `code` previewer handle invalid carriage return chars and binary streams better ([#1550]) - Make the builtin `code` previewer handle invalid carriage return chars and binary streams better ([#1550])
@ -515,10 +621,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Start with multiple tabs with different paths ([#1443]) - Start with multiple tabs with different paths ([#1443])
- Key notion shorthands such as `<C-S-x>` as `<C-X>` ([#1448]) - Key notion shorthands such as `<C-S-x>` as `<C-X>` ([#1448])
- Support `F13` - `F19` keys ([#1446]) - Support `F13` - `F19` keys ([#1446])
- New `--cursor` for the `shell` command ([#1422]) - New `--cursor` for the `shell` action ([#1422])
- New `search_do` command to make it easier to achieve a flat view ([#1431]) - New `search_do` action to make it easier to achieve a flat view ([#1431])
- Portrait orientation preview for EXIF image ([#1412]) - Portrait orientation preview for EXIF image ([#1412])
- Keybinding for the `hardlink` command ([#1461]) - Keybinding for the `hardlink` action ([#1461])
- New `empty` previewer for empty and `/proc/*` files ([#1482]) - New `empty` previewer for empty and `/proc/*` files ([#1482])
- Note about filtering in the help menu ([#1361]) - Note about filtering in the help menu ([#1361])
- New `tab` DDS event on tab switch ([#1474]) - New `tab` DDS event on tab switch ([#1474])
@ -546,12 +652,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Font preview ([#1048]) - Font preview ([#1048])
- SVG, HEIC, AVIF, and JPEG XL preview support ([#1050], [#1249]) - SVG, HEIC, AVIF, and JPEG XL preview support ([#1050], [#1249])
- Simplify keybindings ([#1241]) - Simplify keybindings ([#1241])
- New command `hardlink` to create hard links ([#1268]) - New `hardlink` action to create hard links ([#1268])
- Keep file creation time on macOS and Windows ([#1169]) - Keep file creation time on macOS and Windows ([#1169])
- Sort randomly ([#1291]) - Sort randomly ([#1291])
- New linemode to show file ownership ([#1238]) - New linemode to show file ownership ([#1238])
- New linemode to show file ctime ([#1295]) - New linemode to show file ctime ([#1295])
- New `--hovered` option for the `rename` and `remove` commands ([#1227]) - New `--hovered` option for the `rename` and `remove` actions ([#1227])
- Support Super/Command/Windows key with `D-` notation ([#1069]) - Support Super/Command/Windows key with `D-` notation ([#1069])
- Interactive `cd` path auto-completion supports `~` expansion ([#1081]) - Interactive `cd` path auto-completion supports `~` expansion ([#1081])
- Preview files containing non-UTF-8 characters ([#958]) - Preview files containing non-UTF-8 characters ([#958])
@ -576,7 +682,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Use Ctrl+c instead of Ctrl+q as the universal close key to follow the conventions - Use Ctrl+c instead of Ctrl+q as the universal close key to follow the conventions
- Replace Alt+k/Alt+j with K/J as the `seek` keybindings to avoid Alt key not working in certain terminals - Replace Alt+k/Alt+j with K/J as the `seek` keybindings to avoid Alt key not working in certain terminals
- Replace Ctrl+Enter with Shift+Enter as the alternative key for Shift+o so that it corresponds to Enter being `o` (without Shift) - Replace Ctrl+Enter with Shift+Enter as the alternative key for Shift+o so that it corresponds to Enter being `o` (without Shift)
- keep original state of `sort` command in favor of specifying `yes` or `no` to explicitly apply a new state to its `--reverse`, `--dir-first`, and `--translit` - keep original state of `sort` action in favor of specifying `yes` or `no` to explicitly apply a new state to its `--reverse`, `--dir-first`, and `--translit`
- Move `mime` plugin from `[plugin.preloaders]` to `[plugin.fetchers]` of yazi.toml - Move `mime` plugin from `[plugin.preloaders]` to `[plugin.fetchers]` of yazi.toml
- Turn `success` and `code` into properties of `Status` instead of methods - Turn `success` and `code` into properties of `Status` instead of methods
- Remove `fs.cha_follow(url)` in favor of `fs.cha(url, true)` - Remove `fs.cha_follow(url)` in favor of `fs.cha(url, true)`
@ -603,7 +709,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Data distribution service ([#826], [#855], [#861], [#867], [#868], [#871], [#880], [#895], [#913], [#928], [#933], [#940]) - Data distribution service ([#826], [#855], [#861], [#867], [#868], [#871], [#880], [#895], [#913], [#928], [#933], [#940])
- Re-implement fzf and zoxide as built-in plugins ([#884], [#881]) - Re-implement fzf and zoxide as built-in plugins ([#884], [#881])
- Preserve files' modified at timestamp while copying ([#926]) - Preserve files' modified at timestamp while copying ([#926])
- New `--orphan` option for `shell` command ([#887]) - New `--orphan` option for `shell` action ([#887])
- Smart-case for completion of interactive `cd` paths ([#910]) - Smart-case for completion of interactive `cd` paths ([#910])
- Allow creating a tab with the startup directory through `tab_create` without specifying a path ([#917]) - Allow creating a tab with the startup directory through `tab_create` without specifying a path ([#917])
- Bunch of new debugging information to `yazi --debug` ([#824]) - Bunch of new debugging information to `yazi --debug` ([#824])
@ -616,7 +722,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Deprecated ### Deprecated
- Deprecate the `jump` command in favor of `plugin fzf` and `plugin zoxide` ([#884], [#881]) - Deprecate the `jump` action in favor of `plugin fzf` and `plugin zoxide` ([#884], [#881])
### Fixed ### Fixed
@ -648,7 +754,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- New `<Home>`, `<End>`, and `<Delete>` keybindings for inputs ([#665]) - New `<Home>`, `<End>`, and `<Delete>` keybindings for inputs ([#665])
- New `<C-p>` and `<C-n>` for the select component to move the cursor up/down ([#779]) - New `<C-p>` and `<C-n>` for the select component to move the cursor up/down ([#779])
- New `Ctrl+[` as an alternative to the escape key ([#763]) - New `Ctrl+[` as an alternative to the escape key ([#763])
- New option `--hovered` for the `open` command allows only to open the currently hovered file ([#687]) - New option `--hovered` for the `open` action allows only to open the currently hovered file ([#687])
- Support musl builds for Linux ([#759]) - Support musl builds for Linux ([#759])
- New `--debug` flag to print debug information ([#794]) - New `--debug` flag to print debug information ([#794])
- Send a foreground notification to the user when the process fails to run ([#775]) - Send a foreground notification to the user when the process fails to run ([#775])
@ -659,7 +765,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Ability to sort candidates in the which component ([#662]) - Ability to sort candidates in the which component ([#662])
- Expose selected/yanked files as Lua API ([#674]) - Expose selected/yanked files as Lua API ([#674])
- New `cx.yanked` API to access yanked files ([#788]) - New `cx.yanked` API to access yanked files ([#788])
- New `$0` (Unix) / `%0` (Windows) to access the hovered file in `shell` command ([#738]) - New `$0` (Unix) / `%0` (Windows) to access the hovered file in `shell` action ([#738])
- New `ya.hide()` API to hide the UI temporarily ([#792]) - New `ya.hide()` API to hide the UI temporarily ([#792])
- Allow both `/` and `\` for folder creation on Windows ([#751]) - Allow both `/` and `\` for folder creation on Windows ([#751])
- New `parse()` method for the line elements to parse ANSI sequences - New `parse()` method for the line elements to parse ANSI sequences
@ -686,13 +792,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Added ### Added
- Preview image over SSH ([#585]) - Preview image over SSH ([#585])
- New `unyank` command ([#313]) - New `unyank` action ([#313])
- Customize number of columns of the which component ([#571]) - Customize number of columns of the which component ([#571])
- Support passing arguments to plugin ([#587]) - Support passing arguments to plugin ([#587])
- New `image_quality` and `sixel_fraction` options to configure the image preview quality ([#576]) - New `image_quality` and `sixel_fraction` options to configure the image preview quality ([#576])
- New `ya.which()` API for custom key events ([#617]) - New `ya.which()` API for custom key events ([#617])
- New `ya.quote()` API to quote strings safely - New `ya.quote()` API to quote strings safely
- `plugin` command for each layer - `plugin` action for each layer
- Plugin-specific state persistence ([#590]) - Plugin-specific state persistence ([#590])
- Allow to configure image filter ([#586]) - Allow to configure image filter ([#586])
- Shorten unit names and add more units to `ya.readable_size()` - Shorten unit names and add more units to `ya.readable_size()`
@ -730,7 +836,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Improved ### Improved
- New event system allows multiple commands to reuse a single render ([#561]) - New event system allows multiple actions to reuse a single render ([#561])
## [v0.2.1] ## [v0.2.1]
@ -742,17 +848,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Added ### Added
- New `filter` command to filter files on the fly ([#454]) - New `filter` action to filter files on the fly ([#454])
- Sort by file extension ([#405]) - Sort by file extension ([#405])
- Custom preloader and previewer ([#401]) - Custom preloader and previewer ([#401])
- New `plugin` command to run Lua plugins - New `plugin` action to run Lua plugins
- Auto-completion for input component ([#324], [#353], [#352]) - Auto-completion for input component ([#324], [#353], [#352])
- Start with the specified file hovers over ([#358]) - Start with the specified file hovers over ([#358])
- Emacs readline keybindings for inputs ([#345], [#382]) - Emacs readline keybindings for inputs ([#345], [#382])
- New `--empty` and `--cursor` options for the `rename` command ([#513]) - New `--empty` and `--cursor` options for the `rename` action ([#513])
- New `--follow` option for `paste` command ([#436]) - New `--follow` option for `paste` action ([#436])
- Make `copy` command work over SSH with OSC 52 ([#447]) - Make `copy` action work over SSH with OSC 52 ([#447])
- New `reveal` command ([#341]) - New `reveal` action ([#341])
- Support colored icons ([#503]) - Support colored icons ([#503])
- Support highlighting specific file types ([#510]) - Support highlighting specific file types ([#510])
- Make the position of input and select components customizable ([#361]) - Make the position of input and select components customizable ([#361])
@ -774,8 +880,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Changed ### Changed
- Rename the option `layout` to `ratio` to make it more self-explanatory - Rename the option `layout` to `ratio` to make it more self-explanatory
- Rename the `peek` command to `seek` to better convey the action of "seeking for" content to preview - Rename the `peek` action to `seek` to better convey the action of "seeking for" content to preview
- Rename the `--dir_first` option of `sort` command to `--dir-first` to make it consistent with the style of other commands - Rename the `--dir_first` option of `sort` action to `--dir-first` to make it consistent with the style of other actions
- Replace `[plugins.preload]` with the `init.lua` entry file - Replace `[plugins.preload]` with the `init.lua` entry file
### Fixed ### Fixed
@ -800,22 +906,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Added ### Added
- New `find` command to find files ([#104]) - New `find` action to find files ([#104])
- Linemode to show extra file info ([#291]) - Linemode to show extra file info ([#291])
- New `sort_sensitive` option ([#155]) - New `sort_sensitive` option ([#155])
- Cross-platform opener rules ([#289]) - Cross-platform opener rules ([#289])
- Multiple openers for a single open rule ([#154]) - Multiple openers for a single open rule ([#154])
- Vim-like `gg`, `G` in the preset key mappings for boundary jumps - Vim-like `gg`, `G` in the preset key mappings for boundary jumps
- Theme system ([#161]) - Theme system ([#161])
- New `--force` option for `remove`, `create`, `rename` commands ([#179], [#208]) - New `--force` option for `remove`, `create`, `rename` actions ([#179], [#208])
- Image preview within tmux ([#147]) - Image preview within tmux ([#147])
- New `link` command that creates symlinks to the yanked files ([#167]) - New `link` action creates symlinks to the yanked files ([#167])
- New `orphan` option for opener rules to detach processes from the task scheduler ([#216]) - New `orphan` option for opener rules to detach processes from the task scheduler ([#216])
- New `backward` and `forward` commands - New `backward` and `forward` actions
- New `--smart` option for the `find` command to support smart case ([#240]) - New `--smart` option for the `find` action to support smart case ([#240])
- Sorting for each tab individually ([#131]) - Sorting for each tab individually ([#131])
- Suspend process with `Ctrl+z` ([#120]) - Suspend process with `Ctrl+z` ([#120])
- Percentage values for the `arrow` command to scroll half/full page (with newly added Vi-like `<C-u>`, `<C-d>`, `<C-b>`, and `<C-f>` keybindings) ([#213]) - Percentage values for the `arrow` action to scroll half/full page (with newly added Vi-like `<C-u>`, `<C-d>`, `<C-b>`, and `<C-f>` keybindings) ([#213])
- Show keywords when in search mode ([#152]) - Show keywords when in search mode ([#152])
- Tab switch wraparound ([#160]) - Tab switch wraparound ([#160])
- Highlight matched keywords in find mode ([#211]) - Highlight matched keywords in find mode ([#211])
@ -824,7 +930,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Use of environment variables in `cd` paths ([#241]) - Use of environment variables in `cd` paths ([#241])
- Nix Flakes package ([#205]) - Nix Flakes package ([#205])
- New `V`, `D`, `C` Vim-like keybindings for the input component - New `V`, `D`, `C` Vim-like keybindings for the input component
- New `--no-cwd-file` option for the `quit` command to exit without writing the CWD file ([#245]) - New `--no-cwd-file` option for the `quit` action to exit without writing the CWD file ([#245])
- Fallback to built-in code highlighting if `jq` is not installed ([#151]) - Fallback to built-in code highlighting if `jq` is not installed ([#151])
- New `realtime` option for the input to support real-time input feedback ([#127]) - New `realtime` option for the input to support real-time input feedback ([#127])
- RGBA-16 image preview ([#250]) - RGBA-16 image preview ([#250])
@ -861,7 +967,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Scrollable preview ([#86]) - Scrollable preview ([#86])
- Natural sorting ([#82]) - Natural sorting ([#82])
- Windows support - Windows support
- New `copy` command to copy file paths to clipboard ([#72]) - New `copy` action to copy file paths to clipboard ([#72])
- File chooser mode ([#69]) - File chooser mode ([#69])
- Show symlink path ([#67]) - Show symlink path ([#67])
- Respect `$EDITOR` environment variable when opening text files ([#91]) - Respect `$EDITOR` environment variable when opening text files ([#91])
@ -894,8 +1000,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Code highlighting supports more languages ([#22]) - Code highlighting supports more languages ([#22])
- Change the shell CWD on exit with the shell wrapper ([#40]) - Change the shell CWD on exit with the shell wrapper ([#40])
- Allow customizing the display name of openers ([#31]) - Allow customizing the display name of openers ([#31])
- New `shell` command ([#24]) - New `shell` action ([#24])
- Command template support for the `shell` command ([#48]) - Command template support for the `shell` action ([#48])
- Interactive `cd` ([#43]) - Interactive `cd` ([#43])
- Show the output of running tasks in real time ([#17]) - Show the output of running tasks in real time ([#17])
- Allow using the current directory name as tab name ([#41]) - Allow using the current directory name as tab name ([#41])
@ -910,7 +1016,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- PDF cache cannot be generated with a large `max_width` value ([#28]) - PDF cache cannot be generated with a large `max_width` value ([#28])
- `show_hidden` option not working ([#47]) - `show_hidden` option not working ([#47])
- Wrong task name when `shell` command has no arguments - Wrong task name when `shell` action has no arguments
### Improved ### Improved
@ -923,7 +1029,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Added ### Added
- New `sort` command to change sorting method on the fly ([#7]) - New `sort` action to change sorting method on the fly ([#7])
- Which-key component to support multi-key chords ([#4]) - Which-key component to support multi-key chords ([#4])
- Hover the cursor over newly created files automatically ([#10]) - Hover the cursor over newly created files automatically ([#10])
- Make folders openable ([#9]) - Make folders openable ([#9])
@ -944,8 +1050,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Arrow keys are now bound for navigation by default (along with existing Vim-style bindings) - Arrow keys are now bound for navigation by default (along with existing Vim-style bindings)
- Horizontal scrolling support for the `input` component - Horizontal scrolling support for the `input` component
- Visual mode for the input component - Visual mode for the input component
- New `yank` and `paste` commands for the input component - New `yank` and `paste` actions for the input component
- New `undo` and `redo` commands for the `input` component - New `undo` and `redo` actions for the `input` component
### Fixed ### Fixed
@ -960,7 +1066,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Added ### Added
- Preset configurations - Preset configurations
- New `open` command - New `open` action
- Select component for interactive `open` - Select component for interactive `open`
- Plain text and archive preview - Plain text and archive preview
- Search files with `fd` and `rg` - Search files with `fd` and `rg`
@ -1008,6 +1114,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[v25.12.29]: https://github.com/sxyazi/yazi/compare/v25.5.31...v25.12.29 [v25.12.29]: https://github.com/sxyazi/yazi/compare/v25.5.31...v25.12.29
[v26.1.4]: https://github.com/sxyazi/yazi/compare/v25.12.29...v26.1.4 [v26.1.4]: https://github.com/sxyazi/yazi/compare/v25.12.29...v26.1.4
[v26.1.22]: https://github.com/sxyazi/yazi/compare/v26.1.4...v26.1.22 [v26.1.22]: https://github.com/sxyazi/yazi/compare/v26.1.4...v26.1.22
[v26.5.6]: https://github.com/sxyazi/yazi/compare/v26.1.22...v26.5.6
[#4]: https://github.com/sxyazi/yazi/pull/4 [#4]: https://github.com/sxyazi/yazi/pull/4
[#5]: https://github.com/sxyazi/yazi/pull/5 [#5]: https://github.com/sxyazi/yazi/pull/5
[#6]: https://github.com/sxyazi/yazi/pull/6 [#6]: https://github.com/sxyazi/yazi/pull/6
@ -1545,6 +1652,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3038]: https://github.com/sxyazi/yazi/pull/3038 [#3038]: https://github.com/sxyazi/yazi/pull/3038
[#3059]: https://github.com/sxyazi/yazi/pull/3059 [#3059]: https://github.com/sxyazi/yazi/pull/3059
[#3067]: https://github.com/sxyazi/yazi/pull/3067 [#3067]: https://github.com/sxyazi/yazi/pull/3067
[#3077]: https://github.com/sxyazi/yazi/pull/3077
[#3083]: https://github.com/sxyazi/yazi/pull/3083 [#3083]: https://github.com/sxyazi/yazi/pull/3083
[#3084]: https://github.com/sxyazi/yazi/pull/3084 [#3084]: https://github.com/sxyazi/yazi/pull/3084
[#3091]: https://github.com/sxyazi/yazi/pull/3091 [#3091]: https://github.com/sxyazi/yazi/pull/3091
@ -1612,3 +1720,66 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3566]: https://github.com/sxyazi/yazi/pull/3566 [#3566]: https://github.com/sxyazi/yazi/pull/3566
[#3582]: https://github.com/sxyazi/yazi/pull/3582 [#3582]: https://github.com/sxyazi/yazi/pull/3582
[#3594]: https://github.com/sxyazi/yazi/pull/3594 [#3594]: https://github.com/sxyazi/yazi/pull/3594
[#3607]: https://github.com/sxyazi/yazi/pull/3607
[#3608]: https://github.com/sxyazi/yazi/pull/3608
[#3617]: https://github.com/sxyazi/yazi/pull/3617
[#3633]: https://github.com/sxyazi/yazi/pull/3633
[#3634]: https://github.com/sxyazi/yazi/pull/3634
[#3638]: https://github.com/sxyazi/yazi/pull/3638
[#3642]: https://github.com/sxyazi/yazi/pull/3642
[#3648]: https://github.com/sxyazi/yazi/pull/3648
[#3661]: https://github.com/sxyazi/yazi/pull/3661
[#3666]: https://github.com/sxyazi/yazi/pull/3666
[#3668]: https://github.com/sxyazi/yazi/pull/3668
[#3677]: https://github.com/sxyazi/yazi/pull/3677
[#3678]: https://github.com/sxyazi/yazi/pull/3678
[#3684]: https://github.com/sxyazi/yazi/pull/3684
[#3687]: https://github.com/sxyazi/yazi/pull/3687
[#3689]: https://github.com/sxyazi/yazi/pull/3689
[#3696]: https://github.com/sxyazi/yazi/pull/3696
[#3708]: https://github.com/sxyazi/yazi/pull/3708
[#3716]: https://github.com/sxyazi/yazi/pull/3716
[#3725]: https://github.com/sxyazi/yazi/pull/3725
[#3728]: https://github.com/sxyazi/yazi/pull/3728
[#3733]: https://github.com/sxyazi/yazi/pull/3733
[#3744]: https://github.com/sxyazi/yazi/pull/3744
[#3748]: https://github.com/sxyazi/yazi/pull/3748
[#3757]: https://github.com/sxyazi/yazi/pull/3757
[#3765]: https://github.com/sxyazi/yazi/pull/3765
[#3780]: https://github.com/sxyazi/yazi/pull/3780
[#3781]: https://github.com/sxyazi/yazi/pull/3781
[#3792]: https://github.com/sxyazi/yazi/pull/3792
[#3793]: https://github.com/sxyazi/yazi/pull/3793
[#3804]: https://github.com/sxyazi/yazi/pull/3804
[#3813]: https://github.com/sxyazi/yazi/pull/3813
[#3846]: https://github.com/sxyazi/yazi/pull/3846
[#3854]: https://github.com/sxyazi/yazi/pull/3854
[#3862]: https://github.com/sxyazi/yazi/pull/3862
[#3890]: https://github.com/sxyazi/yazi/pull/3890
[#3891]: https://github.com/sxyazi/yazi/pull/3891
[#3894]: https://github.com/sxyazi/yazi/pull/3894
[#3901]: https://github.com/sxyazi/yazi/pull/3901
[#3906]: https://github.com/sxyazi/yazi/pull/3906
[#3934]: https://github.com/sxyazi/yazi/pull/3934
[#3943]: https://github.com/sxyazi/yazi/pull/3943
[#3970]: https://github.com/sxyazi/yazi/pull/3970
[#3989]: https://github.com/sxyazi/yazi/pull/3989
[#3990]: https://github.com/sxyazi/yazi/pull/3990
[#4005]: https://github.com/sxyazi/yazi/pull/4005
[#4012]: https://github.com/sxyazi/yazi/pull/4012
[#4022]: https://github.com/sxyazi/yazi/pull/4022
[#4031]: https://github.com/sxyazi/yazi/pull/4031
[#4040]: https://github.com/sxyazi/yazi/pull/4040
[#4064]: https://github.com/sxyazi/yazi/pull/4064
[#4065]: https://github.com/sxyazi/yazi/pull/4065
[#4067]: https://github.com/sxyazi/yazi/pull/4067
[#4068]: https://github.com/sxyazi/yazi/pull/4068
[#4074]: https://github.com/sxyazi/yazi/pull/4074
[#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
[#4108]: https://github.com/sxyazi/yazi/pull/4108
[#4118]: https://github.com/sxyazi/yazi/pull/4118
[#4120]: https://github.com/sxyazi/yazi/pull/4120
[#4144]: https://github.com/sxyazi/yazi/pull/4144

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
AGENTS.md

View file

@ -11,6 +11,7 @@ This guide will help you understand how to contribute to the project.
3. [Development Setup](#development-setup) 3. [Development Setup](#development-setup)
4. [How to Contribute](#how-to-contribute) 4. [How to Contribute](#how-to-contribute)
5. [Pull Requests](#pull-requests) 5. [Pull Requests](#pull-requests)
6. [AI Policy](#ai-policy)
## Getting Started ## Getting Started
@ -164,3 +165,10 @@ We want you to succeed, and it can be discouraging to find that a lot of re-work
- Your code passes all tests and lints. - Your code passes all tests and lints.
- Your pull request description clearly explains the changes and why they are needed. - Your pull request description clearly explains the changes and why they are needed.
4. Address any review comments. Make sure to push updates to the same branch on your fork. 4. Address any review comments. Make sure to push updates to the same branch on your fork.
## AI Policy
1. All issue, PR, and discussion descriptions must be written by humans, not AI.
2. Any use of AI must be disclosed. You must declare which model you used and the extent of AI assistance.
3. Any AI-generated code must be reviewed, tested, and simplified by a human before publishing. This requires you to fully understand how it interacts with the greater system without AI assistance.
4. Any AI tools used must explicitly state they do not assert copyright over the work.

3449
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,17 @@
[workspace] [workspace]
resolver = "2" resolver = "3"
members = [ "yazi-*" ] members = [ "yazi-*" ]
default-members = [ "yazi-fm", "yazi-cli" ] default-members = [ "yazi-fm", "yazi-cli" ]
[workspace.package]
edition = "2024"
version = "26.5.6"
license = "MIT"
authors = [ "sxyazi <sxyazi@gmail.com>" ]
homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
rust-version = "1.95.0"
[profile.dev] [profile.dev]
debug = "line-tables-only" debug = "line-tables-only"
@ -27,42 +36,52 @@ debug = false
[workspace.dependencies] [workspace.dependencies]
ansi-to-tui = "8.0.1" ansi-to-tui = "8.0.1"
anyhow = "1.0.100" anyhow = "1.0.104"
base64 = "0.22.1" arc-swap = { version = "1.9.2", features = [ "serde" ] }
bitflags = { version = "2.10.0", features = [ "serde" ] } base64 = "0.23.0"
clap = { version = "4.5.54", features = [ "derive" ] } bitflags = { version = "2.13.1", features = [ "serde" ] }
chrono = "0.4.45"
clap = { version = "4.6.4", features = [ "derive" ] }
compact_str = { version = "0.10.0", features = [ "serde" ] }
core-foundation-sys = "0.8.7" core-foundation-sys = "0.8.7"
crossterm = { version = "0.29.0", features = [ "event-stream" ] } data-encoding = "2.11.0"
dirs = "6.0.0" dirs = "6.0.0"
dyn-clone = "1.0.20"
either = { version = "1.17.0" }
foldhash = "0.2.0" foldhash = "0.2.0"
futures = "0.3.31" futures = "0.3.33"
globset = "0.4.18" globset = "0.4.19"
hashbrown = { version = "0.16.1", features = [ "serde" ] } hashbrown = { version = "0.17.1", features = [ "serde" ] }
indexmap = { version = "2.13.0", features = [ "serde" ] } image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] }
libc = "0.2.180" indexmap = { version = "2.14.0", features = [ "serde" ] }
lru = "0.16.3" inventory = "0.3.24"
mlua = { version = "0.11.5", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } libc = "0.2.189"
objc2 = "0.6.3" lru = "0.18.1"
ordered-float = { version = "5.1.0", features = [ "serde" ] } mlua = { version = "0.12.0", features = [ "anyhow", "async", "error-send", "lua55", "macros", "serde" ] }
objc2 = "0.6.4"
ordered-float = { version = "5.3.0", features = [ "serde" ] }
parking_lot = "0.12.5" parking_lot = "0.12.5"
paste = "1.0.15" paste = "1.0.15"
percent-encoding = "2.3.2" percent-encoding = "2.3.2"
rand = { version = "0.9.2", default-features = false, features = [ "os_rng", "small_rng", "std" ] } rand = { version = "0.10.2", default-features = false, features = [ "std", "sys_rng" ] }
ratatui = { version = "0.30.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] } ratatui-core = { version = "0.1.2", default-features = false, features = [ "std", "layout-cache", "serde", "underline-color" ] }
regex = "1.12.2" ratatui-widgets = { version = "0.3.2", default-features = false, features = [ "std", "unstable-rendered-line-info" ] }
russh = { version = "0.56.0", default-features = false, features = [ "ring", "rsa" ] } regex = "1.13.1"
russh = { version = "0.62.4", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0" scopeguard = "1.2.0"
serde = { version = "1.0.228", features = [ "derive" ] } serde = { version = "1.0.229", features = [ "derive" ] }
serde_json = "1.0.149" serde_json = "1.0.151"
serde_with = "3.21.0"
strum = { version = "0.28.0", features = [ "derive" ] }
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
thiserror = "2.0.18" thiserror = "2.0.19"
tokio = { version = "1.49.0", features = [ "full" ] } tokio = { version = "1.53.1", features = [ "full" ] }
tokio-stream = "0.1.18" tokio-stream = "0.1.19"
tokio-util = "0.7.18" tokio-util = "0.7.19"
toml = { version = "0.9.11" } toml = { version = "1.1.3" }
tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] } tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } twox-hash = { version = "2.1.3", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
typed-path = "0.12.0" typed-path = "0.12.3"
unicode-width = { version = "0.2.2", default-features = false } unicode-width = { version = "0.2.2", default-features = false }
uzers = "0.12.2" uzers = "0.12.2"

View file

@ -1,16 +1,10 @@
<div align="center"> <div align="center">
<a href="https://go.warp.dev/yazi" target="_blank"> <sup>Special thanks to:</sup><br>
<sup>Special thanks to:</sup>
<br>
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-02.png">
<br>
<h>Warp, built for coding with multiple AI agents</b>
<br>
<sup>Available for macOS, Linux and Windows</sup>
</a>
</div>
<br> | <a href="https://go.warp.dev/yazi" target="_blank"><img alt="Warp sponsorship" width=350 src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-02.png"><br><b>Warp, built for coding with multiple AI agents</b><br><sup>Available for macOS, Linux and Windows</sup></a> |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
</div>
## Yazi - ⚡️ Blazing Fast Terminal File Manager ## Yazi - ⚡️ Blazing Fast Terminal File Manager
@ -29,8 +23,8 @@ Yazi (means "duck") is a terminal file manager written in Rust, based on non-blo
- 🧰 Integration with ripgrep, fd, fzf, zoxide - 🧰 Integration with ripgrep, fd, fzf, zoxide
- 💫 Vim-like input/pick/confirm/which/notify component, auto-completion for cd paths - 💫 Vim-like input/pick/confirm/which/notify component, auto-completion for cd paths
- 🏷️ Multi-Tab Support, Cross-directory selection, Scrollable Preview (for videos, PDFs, archives, code, directories, etc.) - 🏷️ Multi-Tab Support, Cross-directory selection, Scrollable Preview (for videos, PDFs, archives, code, directories, etc.)
- 🔄 Bulk Renaming, Archive Extraction, Visual Mode, File Chooser, [Git Integration](https://github.com/yazi-rs/plugins/tree/main/git.yazi), [Mount Manager](https://github.com/yazi-rs/plugins/tree/main/mount.yazi) - 🔄 Bulk Rename/Create, Archive Extraction, Visual Mode, File Chooser, [Git Integration](https://github.com/yazi-rs/plugins/tree/main/git.yazi), [Mount Manager](https://github.com/yazi-rs/plugins/tree/main/mount.yazi)
- 🎨 Theme System, Mouse Support, Trash Bin, Custom Layouts, CSI u, OSC 52 - 🎨 Theme System, Mouse Support, Drag and Drop, Trash Bin, Custom Layouts, CSI u, OSC 52
- ... and more! - ... and more!
https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265cc7 https://github.com/sxyazi/yazi/assets/17523360/92ff23fa-0cd5-4f04-b387-894c12265cc7
@ -53,24 +47,24 @@ Yazi is currently in heavy development, expect breaking changes.
## Image Preview ## Image Preview
| Platform | Protocol | Support | | Platform | Protocol | Support |
| ---------------------------------------------------------------------------- | -------------------------------------- | ---------------------------------------- | | ---------------------------------------------------------------------------- | -------------------------------------- | -------------------------------------- |
| [kitty](https://github.com/kovidgoyal/kitty) (>= 0.28.0) | [Kitty unicode placeholders][kgp] | ✅ Built-in | | [kitty](https://github.com/kovidgoyal/kitty) (>= 0.28.0) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [iTerm2](https://iterm2.com) | [Inline images protocol][iip] | ✅ Built-in | | [iTerm2](https://iterm2.com) | [Inline images protocol][iip] | ✅ Built-in |
| [WezTerm](https://github.com/wez/wezterm) | [Inline images protocol][iip] | ✅ Built-in | | [WezTerm](https://github.com/wez/wezterm) | [Inline images protocol][iip] | ✅ Built-in |
| [Konsole](https://invent.kde.org/utilities/konsole) | [Kitty old protocol][kgp-old] | ✅ Built-in | | [Konsole](https://invent.kde.org/utilities/konsole) | [Kitty old protocol][kgp-old] | ✅ Built-in |
| [foot](https://codeberg.org/dnkl/foot) | [Sixel graphics format][sixel] | ✅ Built-in | | [foot](https://codeberg.org/dnkl/foot) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Ghostty](https://github.com/ghostty-org/ghostty) | [Kitty unicode placeholders][kgp] | ✅ Built-in | | [Ghostty](https://github.com/ghostty-org/ghostty) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [Windows Terminal](https://github.com/microsoft/terminal) (>= v1.22.10352.0) | [Sixel graphics format][sixel] | ✅ Built-in | | [Windows Terminal](https://github.com/microsoft/terminal) (>= v1.22.10352.0) | [Sixel graphics format][sixel] | ✅ Built-in |
| [st with Sixel patch](https://github.com/bakkeby/st-flexipatch) | [Sixel graphics format][sixel] | ✅ Built-in | | [st with Sixel patch](https://github.com/bakkeby/st-flexipatch) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Warp](https://www.warp.dev) (macOS/Linux only) | [Inline images protocol][iip] | ✅ Built-in | | [Warp](https://www.warp.dev) (macOS/Linux only) | [Inline images protocol][iip] | ✅ Built-in |
| [Tabby](https://github.com/Eugeny/tabby) | [Inline images protocol][iip] | ✅ Built-in | | [Tabby](https://github.com/Eugeny/tabby) | [Inline images protocol][iip] | ✅ Built-in |
| [VSCode](https://github.com/microsoft/vscode) | [Inline images protocol][iip] | ✅ Built-in | | [VSCode](https://github.com/microsoft/vscode) | [Inline images protocol][iip] | ✅ Built-in |
| [Rio](https://github.com/raphamorim/rio) | [Inline images protocol][iip] | ❌ Rio renders images at incorrect sizes | | [Rio](https://github.com/raphamorim/rio) (>= 0.3.9) | [Kitty unicode placeholders][kgp] | ✅ Built-in |
| [Black Box](https://gitlab.gnome.org/raggesilver/blackbox) | [Sixel graphics format][sixel] | ✅ Built-in | | [Black Box](https://gitlab.gnome.org/raggesilver/blackbox) | [Sixel graphics format][sixel] | ✅ Built-in |
| [Bobcat](https://github.com/ismail-yilmaz/Bobcat) | [Inline images protocol][iip] | ✅ Built-in | | [Bobcat](https://github.com/ismail-yilmaz/Bobcat) | [Inline images protocol][iip] | ✅ Built-in |
| X11 / Wayland | Window system protocol | ☑️ [Überzug++][ueberzug] required | | X11 / Wayland | Window system protocol | ☑️ [Überzug++][ueberzug] required |
| Fallback | [ASCII art (Unicode block)][ascii-art] | ☑️ [Chafa][chafa] required | | Fallback | [ASCII art (Unicode block)][ascii-art] | ☑️ [Chafa][chafa] required (>= 1.16.0) |
See https://yazi-rs.github.io/docs/image-preview for details. See https://yazi-rs.github.io/docs/image-preview for details.

View file

@ -1,11 +1,11 @@
[Desktop Entry] [Desktop Entry]
Name=Yazi Name=Yazi File Manager
Icon=yazi Icon=yazi
Comment=Blazing fast terminal file manager written in Rust, based on async I/O Comment=Blazing fast terminal file manager written in Rust, based on async I/O
Terminal=true Terminal=true
TryExec=yazi TryExec=yazi
Exec=yazi %u Exec=yazi %f
Type=Application Type=Application
MimeType=inode/directory MimeType=inode/directory
Categories=Utility;Core;System;FileTools;FileManager;ConsoleOnly Categories=System;FileManager;FileTools;ConsoleOnly
Keywords=File;Manager;Explorer;Browser;Launcher Keywords=File;Manager;Explorer;Browser;Launcher

View file

@ -1 +1 @@
{"language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize"],"version":"0.2"} {"language":"en","version":"0.2","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm","mimetypes","Mimelist","renderables","redrawer","Padable","ents"]}

12
flake.lock generated
View file

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1768875095, "lastModified": 1783279667,
"narHash": "sha256-dYP3DjiL7oIiiq3H65tGIXXIT1Waiadmv93JS0sS+8A=", "narHash": "sha256-/NAkDSsve+GNM0Bt6tleJdCGfsTlK89nPjkVOzZMo0s=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ed142ab1b3a092c4d149245d0c4126a5d7ea00b0", "rev": "f205b5574fd0cb7da5b702a2da51507b7f4fdd1b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -48,11 +48,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1769091129, "lastModified": 1783320166,
"narHash": "sha256-Jj/vIHjiu4OdDIrDXZ3xOPCJrMZZKzhE2UIVXV/NYzY=", "narHash": "sha256-l7C/OsjcnWDOk2K3ssj+SBduwL67LashjBqis9+t468=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "131e22d6a6d54ab72aeef6a5a661ab7005b4c596", "rev": "20ee15370c9256669d66968b89ee20a4b0a4e673",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -32,7 +32,7 @@
rev = self.shortRev or self.dirtyShortRev or "dirty"; rev = self.shortRev or self.dirtyShortRev or "dirty";
date = self.lastModifiedDate or self.lastModified or "19700101"; date = self.lastModifiedDate or self.lastModified or "19700101";
version = version =
(builtins.fromTOML (builtins.readFile ./yazi-fm/Cargo.toml)).package.version (builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.package.version
+ "pre${builtins.substring 0 8 date}_${rev}"; + "pre${builtins.substring 0 8 date}_${rev}";
in in
{ {

View file

@ -2,7 +2,7 @@
mkShell, mkShell,
yazi, yazi,
toolchain, toolchain,
nodePackages, cspell,
yazi-unwrapped, yazi-unwrapped,
}: }:
@ -16,7 +16,7 @@ mkShell {
"clippy" "clippy"
]; ];
}) })
nodePackages.cspell cspell
]; ];
inputsFrom = [ yazi-unwrapped ]; inputsFrom = [ yazi-unwrapped ];

View file

@ -28,9 +28,6 @@ rustPlatform.buildRustPackage (finalAttrs: {
cargoLock = { cargoLock = {
lockFile = "${src}/Cargo.lock"; lockFile = "${src}/Cargo.lock";
#outputHashes = {
# "mlua-0.10.0" = "sha256-Xg6/jc+UP8tbJJ6x1sbAgt8ZHt051xEBBcjmikQqYlw=";
#};
}; };
env = { env = {
@ -54,6 +51,11 @@ rustPlatform.buildRustPackage (finalAttrs: {
--fish ./yazi-boot/completions/yazi.fish \ --fish ./yazi-boot/completions/yazi.fish \
--zsh ./yazi-boot/completions/_yazi --zsh ./yazi-boot/completions/_yazi
installShellCompletion --cmd ya \
--bash ./yazi-cli/completions/ya.bash \
--fish ./yazi-cli/completions/ya.fish \
--zsh ./yazi-cli/completions/_ya
# Resize logo # Resize logo
for RES in 16 24 32 48 64 128 256; do for RES in 16 24 32 48 64 128 256; do
mkdir -p $out/share/icons/hicolor/"$RES"x"$RES"/apps mkdir -p $out/share/icons/hicolor/"$RES"x"$RES"/apps
@ -75,7 +77,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
hash = "sha256-kEVXejDg4ChFoMNBvKlwdFEyUuTcY2VuK9j0PdafKus="; hash = "sha256-kEVXejDg4ChFoMNBvKlwdFEyUuTcY2VuK9j0PdafKus=";
}; };
}; };
meta = { meta = {
description = "Blazing fast terminal file manager written in Rust, based on async I/O"; description = "Blazing fast terminal file manager written in Rust, based on async I/O";

View file

@ -8,6 +8,11 @@ export YAZI_GEN_COMPLETIONS=1
git config --global --add safe.directory "*" git config --global --add safe.directory "*"
cargo build --release --locked --target "$1" cargo build --release --locked --target "$1"
# Copy the binaries to a known location
mkdir -p "target/release"
cp "target/$1/release/ya" "target/release/ya"
cp "target/$1/release/yazi" "target/release/yazi"
# Package deb # Package deb
if [[ "$ARTIFACT_NAME" == *-linux-* ]] && { [[ "$ARTIFACT_NAME" == *-aarch64-* ]] || [[ "$ARTIFACT_NAME" == *-x86_64-* ]]; }; then if [[ "$ARTIFACT_NAME" == *-linux-* ]] && { [[ "$ARTIFACT_NAME" == *-aarch64-* ]] || [[ "$ARTIFACT_NAME" == *-x86_64-* ]]; }; then
cargo install cargo-deb cargo install cargo-deb
@ -16,8 +21,8 @@ fi
# Create the artifact # Create the artifact
mkdir -p "$ARTIFACT_NAME/completions" mkdir -p "$ARTIFACT_NAME/completions"
cp "target/$1/release/ya" "$ARTIFACT_NAME" cp "target/release/ya" "$ARTIFACT_NAME"
cp "target/$1/release/yazi" "$ARTIFACT_NAME" cp "target/release/yazi" "$ARTIFACT_NAME"
cp yazi-cli/completions/* "$ARTIFACT_NAME/completions" cp yazi-cli/completions/* "$ARTIFACT_NAME/completions"
cp yazi-boot/completions/* "$ARTIFACT_NAME/completions" cp yazi-boot/completions/* "$ARTIFACT_NAME/completions"
cp README.md LICENSE "$ARTIFACT_NAME" cp README.md LICENSE "$ARTIFACT_NAME"

View file

@ -17,4 +17,4 @@ prev_ver=$(grep -oE '^\[v[0-9][^]]+\]' CHANGELOG.md | tail -n1 | tr -d '[]')
link="\[v$1\]: https://github.com/sxyazi/yazi/compare/$prev_ver...v$1" link="\[v$1\]: https://github.com/sxyazi/yazi/compare/$prev_ver...v$1"
perl -pi -e 's{(\['"$prev_ver"'\]:[^\n]+\n)}{$1'"$link"'\n}s' CHANGELOG.md perl -pi -e 's{(\['"$prev_ver"'\]:[^\n]+\n)}{$1'"$link"'\n}s' CHANGELOG.md
ESLINT_USE_FLAT_CONFIG=true eslint -c ~/.config/rules/eslint/eslint.config.cjs --fix -- $TOML_FILES eslint -c ~/.config/rules/eslint/eslint.config.js --fix -- $TOML_FILES

View file

@ -2,19 +2,32 @@ 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)) {
return null return null
} }
return `Hey @${creator}, I noticed that you did not correctly follow the issue template. Please ensure that: return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it!
I noticed that you did not correctly follow the issue template. Please ensure that:
- The bug can still be reproduced on the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries). - The bug can still be reproduced on the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries).
- The debug information (\`yazi --debug\`) is updated for the newest nightly. - The environment info (\`ya env\`) is updated for the newest nightly.
- The non-optional items in the checklist are checked. - The non-optional items in the checklist are checked.
Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity. Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity.
Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding! 🙏
` `
} }
@ -23,16 +36,26 @@ function featureRequestBody(creator, content) {
return null return null
} }
return `Hey @${creator}, I noticed that you did not correctly follow the issue template. Please ensure that: return `Hey @${creator}, thank you for opening the issue to help improve Yazi, appreciate it!
I noticed that you did not correctly follow the issue template. Please ensure that:
- The requested feature does not exist in the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries). - The requested feature does not exist in the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries).
- The debug information (\`yazi --debug\`) is updated for the newest nightly. - The environment info (\`ya env\`) is updated for the newest nightly.
- The non-optional items in the checklist are checked. - The non-optional items in the checklist are checked.
Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity. Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity.
Our maintainers work on Yazi in their free time, this helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thanks for your understanding! 🙏
` `
} }
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 {
@ -66,7 +89,7 @@ module.exports = async ({ github, context, core }) => {
const { data: events } = await github.rest.issues.listEvents({ const { data: events } = await github.rest.issues.listEvents({
...context.repo, ...context.repo,
issue_number: id, issue_number: id,
per_page : 100, per_page: 100,
}) })
const all = events.filter(v => v.event === "labeled" && v.label?.name === LABEL_NAME) const all = events.filter(v => v.event === "labeled" && v.label?.name === LABEL_NAME)
@ -82,7 +105,7 @@ module.exports = async ({ github, context, core }) => {
const { data: events } = await github.rest.issues.listEvents({ const { data: events } = await github.rest.issues.listEvents({
...context.repo, ...context.repo,
issue_number: id, issue_number: id,
per_page : 100, per_page: 100,
}) })
const all = events.filter(v => v.event === "unlabeled" && v.label?.name === LABEL_NAME) const all = events.filter(v => v.event === "unlabeled" && v.label?.name === LABEL_NAME)
@ -101,14 +124,14 @@ module.exports = async ({ github, context, core }) => {
await github.rest.issues.removeLabel({ await github.rest.issues.removeLabel({
...context.repo, ...context.repo,
issue_number: id, issue_number: id,
name : LABEL_NAME, name: LABEL_NAME,
}) })
await hideOldComments(id) await hideOldComments(id)
} else if (mark && !marked && !await removedLabelManually(id)) { } else if (mark && !marked && !await removedLabelManually(id)) {
await github.rest.issues.addLabels({ await github.rest.issues.addLabels({
...context.repo, ...context.repo,
issue_number: id, issue_number: id,
labels : [LABEL_NAME], labels: [LABEL_NAME],
}) })
await hideOldComments(id) await hideOldComments(id)
await github.rest.issues.createComment({ await github.rest.issues.createComment({
@ -127,7 +150,7 @@ module.exports = async ({ github, context, core }) => {
const comments = await github.paginate(github.rest.issues.listComments, { const comments = await github.paginate(github.rest.issues.listComments, {
...context.repo, ...context.repo,
issue_number: id, issue_number: id,
per_page : 100, per_page: 100,
}) })
for (const c of comments) { for (const c of comments) {
@ -157,7 +180,7 @@ module.exports = async ({ github, context, core }) => {
try { try {
const { data: issues } = await github.rest.issues.listForRepo({ const { data: issues } = await github.rest.issues.listForRepo({
...context.repo, ...context.repo,
state : "open", state: "open",
labels: LABEL_NAME, labels: LABEL_NAME,
}) })
@ -170,13 +193,13 @@ module.exports = async ({ github, context, core }) => {
await github.rest.issues.update({ await github.rest.issues.update({
...context.repo, ...context.repo,
issue_number: issue.number, issue_number: issue.number,
state : "closed", state: "closed",
state_reason: "not_planned", state_reason: "not_planned",
}) })
await github.rest.issues.createComment({ await github.rest.issues.createComment({
...context.repo, ...context.repo,
issue_number: issue.number, issue_number: issue.number,
body : `This issue has been automatically closed because it was marked as \`${LABEL_NAME}\` for more than 2 days without updates. body: `This issue has been automatically closed because it was marked as \`${LABEL_NAME}\` for more than 2 days without updates.
If the problem persists, please file a new issue and complete the issue template so we can capture all the details necessary to investigate further.`, If the problem persists, please file a new issue and complete the issue template so we can capture all the details necessary to investigate further.`,
}) })
} }
@ -191,13 +214,13 @@ If the problem persists, please file a new issue and complete the issue template
await github.rest.issues.update({ await github.rest.issues.update({
...context.repo, ...context.repo,
issue_number: id, issue_number: id,
state : "closed", state: "closed",
state_reason: "not_planned", state_reason: "not_planned",
}) })
await github.rest.issues.createComment({ await github.rest.issues.createComment({
...context.repo, ...context.repo,
issue_number: id, issue_number: id,
body : `Unsupported issue template. body: `Unsupported issue template.
Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) or [Feature Request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml) template should be used.`, Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) or [Feature Request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml) template should be used.`,
}) })
} catch (e) { } catch (e) {
@ -206,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)
@ -228,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

@ -8,247 +8,225 @@
"name": "validate-form", "name": "validate-form",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@actions/core": "^2.0.2", "@actions/core": "^3.0.1",
"@actions/github": "^7.0.0" "@actions/github": "^9.1.1"
} }
}, },
"node_modules/@actions/core": { "node_modules/@actions/core": {
"version": "2.0.2", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz",
"integrity": "sha512-Ast1V7yHbGAhplAsuVlnb/5J8Mtr/Zl6byPPL+Qjq3lmfIgWF1ak1iYfF/079cRERiuTALTXkSuEUdZeDCfGtA==", "integrity": "sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/exec": "^2.0.0", "@actions/exec": "^3.0.0",
"@actions/http-client": "^3.0.1" "@actions/http-client": "^4.0.0"
} }
}, },
"node_modules/@actions/exec": { "node_modules/@actions/exec": {
"version": "2.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz",
"integrity": "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==", "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/io": "^2.0.0" "@actions/io": "^3.0.2"
} }
}, },
"node_modules/@actions/github": { "node_modules/@actions/github": {
"version": "7.0.0", "version": "9.1.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-7.0.0.tgz", "resolved": "https://registry.npmjs.org/@actions/github/-/github-9.1.1.tgz",
"integrity": "sha512-PyGODO938aoBTZd/IfN/+e+Pd5hUcVpyf+thm4CPESLeqhdSkq5QwMTGX9v84XHE1ifmHWBQ60KB8kIgm96opw==", "integrity": "sha512-tL5JbYOBZHc0ngEnCsaDcryUizIUIlQyIMwy1Wkx93H5HzbBJ7TbiPx2PnFjBwZW0Vh05JmfFZhecE6gglYegA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/http-client": "^3.0.1", "@actions/http-client": "^3.0.2",
"@octokit/core": "^5.0.1", "@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@octokit/request": "^8.4.1", "@octokit/request": "^10.0.7",
"@octokit/request-error": "^5.1.1", "@octokit/request-error": "^7.1.0",
"undici": "^5.28.5" "undici": "^6.23.0"
} }
}, },
"node_modules/@actions/http-client": { "node_modules/@actions/github/node_modules/@actions/http-client": {
"version": "3.0.1", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz",
"integrity": "sha512-SbGS8c/vySbNO3kjFgSW77n83C4MQx/Yoe+b1hAdpuvfHxnkHzDq2pWljUpAA56Si1Gae/7zjeZsV0CYjmLo/w==", "integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"undici": "^5.28.5" "undici": "^6.23.0"
}
},
"node_modules/@actions/http-client": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.1.tgz",
"integrity": "sha512-+Nvd1ImaOZBSoPbsUtEhv+1z99H12xzncCkz0a3RuehINE81FZSe2QTj3uvAPTcJX/SCzUQHQ0D1GrPMbrPitg==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^6.23.0"
} }
}, },
"node_modules/@actions/io": { "node_modules/@actions/io": {
"version": "2.0.0", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz",
"integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==", "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@octokit/auth-token": { "node_modules/@octokit/auth-token": {
"version": "4.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
"integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 18" "node": ">= 20"
} }
}, },
"node_modules/@octokit/core": { "node_modules/@octokit/core": {
"version": "5.2.2", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
"integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/auth-token": "^4.0.0", "@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^7.1.0", "@octokit/graphql": "^9.0.3",
"@octokit/request": "^8.4.1", "@octokit/request": "^10.0.6",
"@octokit/request-error": "^5.1.1", "@octokit/request-error": "^7.0.2",
"@octokit/types": "^13.0.0", "@octokit/types": "^16.0.0",
"before-after-hook": "^2.2.0", "before-after-hook": "^4.0.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^7.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 20"
} }
}, },
"node_modules/@octokit/endpoint": { "node_modules/@octokit/endpoint": {
"version": "9.0.6", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz",
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^13.1.0", "@octokit/types": "^16.0.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^7.0.2"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 20"
} }
}, },
"node_modules/@octokit/graphql": { "node_modules/@octokit/graphql": {
"version": "7.1.1", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
"integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/request": "^8.4.1", "@octokit/request": "^10.0.6",
"@octokit/types": "^13.0.0", "@octokit/types": "^16.0.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^7.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 20"
} }
}, },
"node_modules/@octokit/openapi-types": { "node_modules/@octokit/openapi-types": {
"version": "24.2.0", "version": "27.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@octokit/plugin-paginate-rest": { "node_modules/@octokit/plugin-paginate-rest": {
"version": "9.2.2", "version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
"integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^12.6.0" "@octokit/types": "^16.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 20"
}, },
"peerDependencies": { "peerDependencies": {
"@octokit/core": "5" "@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
"integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
"integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^20.0.0"
} }
}, },
"node_modules/@octokit/plugin-rest-endpoint-methods": { "node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "10.4.1", "version": "17.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
"integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^12.6.0" "@octokit/types": "^16.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 20"
}, },
"peerDependencies": { "peerDependencies": {
"@octokit/core": "5" "@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz",
"integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==",
"license": "MIT"
},
"node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz",
"integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^20.0.0"
} }
}, },
"node_modules/@octokit/request": { "node_modules/@octokit/request": {
"version": "8.4.1", "version": "10.0.8",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz",
"integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/endpoint": "^9.0.6", "@octokit/endpoint": "^11.0.3",
"@octokit/request-error": "^5.1.1", "@octokit/request-error": "^7.0.2",
"@octokit/types": "^13.1.0", "@octokit/types": "^16.0.0",
"universal-user-agent": "^6.0.0" "fast-content-type-parse": "^3.0.0",
"json-with-bigint": "^3.5.3",
"universal-user-agent": "^7.0.2"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 20"
} }
}, },
"node_modules/@octokit/request-error": { "node_modules/@octokit/request-error": {
"version": "5.1.1", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
"integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^13.1.0", "@octokit/types": "^16.0.0"
"deprecation": "^2.0.0",
"once": "^1.4.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 20"
} }
}, },
"node_modules/@octokit/types": { "node_modules/@octokit/types": {
"version": "13.10.0", "version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/openapi-types": "^24.2.0" "@octokit/openapi-types": "^27.0.0"
} }
}, },
"node_modules/before-after-hook": { "node_modules/before-after-hook": {
"version": "2.2.3", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/deprecation": { "node_modules/fast-content-type-parse": {
"version": "2.3.1", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"license": "ISC" "funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
}, },
"node_modules/once": { "node_modules/json-with-bigint": {
"version": "1.4.0", "version": "3.5.8",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==",
"license": "ISC", "license": "MIT"
"dependencies": {
"wrappy": "1"
}
}, },
"node_modules/tunnel": { "node_modules/tunnel": {
"version": "0.0.6", "version": "0.0.6",
@ -260,27 +238,18 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "5.29.0", "version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
"license": "MIT", "license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
"engines": { "engines": {
"node": ">=14.0" "node": ">=18.17"
} }
}, },
"node_modules/universal-user-agent": { "node_modules/universal-user-agent": {
"version": "6.0.1", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"license": "ISC"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
} }
} }

View file

@ -6,7 +6,7 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@actions/core": "^2.0.2", "@actions/core": "^3.0.1",
"@actions/github": "^7.0.0" "@actions/github": "^9.1.1"
} }
} }

View file

@ -133,9 +133,16 @@ parts:
- --prefix=/usr - --prefix=/usr
build-attributes: build-attributes:
- enable-patchelf - enable-patchelf
organize:
# `libgomp1` installs libgomp into the multiarch directory, but the
# `magick` binary's RPATH only searches `$ORIGIN/../lib` (i.e. usr/lib).
# Move it alongside the Magick libraries so the bundled binary can load
# it at runtime; otherwise magick fails with:
# "libgomp.so.1: cannot open shared object file: No such file or directory"
usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libgomp.so*: usr/lib/
prime: prime:
- usr/bin/magick - usr/bin/magick
- usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libgomp.so.1 - usr/lib/libgomp.so*
- usr/lib/libMagickCore-7.Q16HDRI.so* - usr/lib/libMagickCore-7.Q16HDRI.so*
- usr/lib/libMagickWand-7.Q16HDRI.so* - usr/lib/libMagickWand-7.Q16HDRI.so*

View file

@ -1,12 +1,13 @@
[package] [package]
name = "yazi-actor" name = "yazi-actor"
version = "26.1.22" description = "Yazi actor model"
edition = "2024" version.workspace = true
license = "MIT" edition.workspace = true
authors = [ "sxyazi <sxyazi@gmail.com>" ] license.workspace = true
description = "Yazi actor model" authors.workspace = true
homepage = "https://yazi-rs.github.io" homepage.workspace = true
repository = "https://github.com/sxyazi/yazi" repository.workspace = true
rust-version.workspace = true
[lints] [lints]
workspace = true workspace = true
@ -16,37 +17,42 @@ default = [ "vendored-lua" ]
vendored-lua = [ "mlua/vendored" ] vendored-lua = [ "mlua/vendored" ]
[dependencies] [dependencies]
yazi-binding = { path = "../yazi-binding", version = "26.1.22" } yazi-binding = { path = "../yazi-binding", version = "26.5.6" }
yazi-boot = { path = "../yazi-boot", version = "26.1.22" } yazi-boot = { path = "../yazi-boot", version = "26.5.6" }
yazi-config = { path = "../yazi-config", version = "26.1.22" } yazi-config = { path = "../yazi-config", version = "26.5.6" }
yazi-core = { path = "../yazi-core", version = "26.1.22" } yazi-core = { path = "../yazi-core", version = "26.5.6" }
yazi-dds = { path = "../yazi-dds", version = "26.1.22" } yazi-dds = { path = "../yazi-dds", version = "26.5.6" }
yazi-fs = { path = "../yazi-fs", version = "26.1.22" } yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.1.22" } yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-parser = { path = "../yazi-parser", version = "26.1.22" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-plugin = { path = "../yazi-plugin", version = "26.1.22" } yazi-parser = { path = "../yazi-parser", version = "26.5.6" }
yazi-proxy = { path = "../yazi-proxy", version = "26.1.22" } yazi-plugin = { path = "../yazi-plugin", version = "26.5.6" }
yazi-scheduler = { path = "../yazi-scheduler", version = "26.1.22" } yazi-proxy = { path = "../yazi-proxy", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.1.22" } yazi-runner = { path = "../yazi-runner", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.1.22" } yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.1.22" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-watcher = { path = "../yazi-watcher", version = "26.1.22" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.1.22" } yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
yazi-tui = { path = "../yazi-tui", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
crossterm = { workspace = true } either = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
mlua = { workspace = true } indexmap = { workspace = true }
paste = { workspace = true } inventory = { workspace = true }
scopeguard = { workspace = true } mlua = { workspace = true }
tokio = { workspace = true } paste = { workspace = true }
tokio-stream = { workspace = true } ratatui-core = { workspace = true }
tracing = { workspace = true } scopeguard = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
[target."cfg(unix)".dependencies] [target."cfg(unix)".dependencies]
libc = { workspace = true } libc = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -1,15 +1,15 @@
use anyhow::Result; use anyhow::Result;
use yazi_dds::spark::SparkKind; use yazi_parser::spark::SparkKind;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::Ctx; use crate::Ctx;
pub trait Actor { pub trait Actor {
type Options; type Form;
const NAME: &str; const NAME: &str;
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data>; fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data>;
fn hook(_cx: &Ctx, _opt: &Self::Options) -> Option<SparkKind> { None } fn hook(_cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> { None }
} }

View file

@ -0,0 +1,42 @@
use anyhow::Result;
use mlua::IntoLua;
use tracing::error;
use yazi_actor::lives::Lives;
use yazi_binding::runtime_scope;
use yazi_dds::{LOCAL, Payload, REMOTE};
use yazi_macro::succ;
use yazi_plugin::LUA;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct AcceptPayload;
impl Actor for AcceptPayload {
type Form = Payload<'static>;
const NAME: &str = "accept_payload";
fn act(cx: &mut Ctx, payload: Payload) -> Result<Data> {
let kind = payload.body.kind();
let lock = if payload.receiver == 0 || payload.receiver != payload.sender {
REMOTE.read()
} else {
LOCAL.read()
};
let Some(handlers) = lock.get(kind).filter(|&m| !m.is_empty()).cloned() else { succ!() };
drop(lock);
let kind = kind.to_owned();
succ!(Lives::scope(cx.core, |_| {
let body = payload.body.into_lua(&LUA)?;
for (id, cb) in handlers {
if let Err(e) = runtime_scope!(LUA, &id, cb.call::<()>(body.clone())) {
error!("Failed to run `{kind}` event handler in your `{id}` plugin: {e}");
}
}
Ok(())
})?);
}
}

View file

@ -0,0 +1,32 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_boot::BOOT;
use yazi_core::mgr::CdSource;
use yazi_macro::{act, succ};
use yazi_parser::VoidForm;
use yazi_shared::{data::Data, strand::StrandLike, url::UrlLike};
use crate::Actor;
pub struct Bootstrap;
impl Actor for Bootstrap {
type Form = VoidForm;
const NAME: &str = "bootstrap";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
cx.mgr.tabs.resize_with(BOOT.files.len(), Default::default);
for (i, file) in BOOT.files.iter().enumerate().rev() {
cx.tab = i;
if file.is_empty() {
act!(mgr:cd, cx, (BOOT.cwds[i].clone(), CdSource::Tab))?;
} else if let Ok(u) = BOOT.cwds[i].try_join(file) {
act!(mgr:reveal, cx, (u, CdSource::Tab))?;
}
}
succ!();
}
}

View file

@ -0,0 +1,24 @@
use anyhow::Result;
use yazi_core::notify::{MessageLevel, MessageOpt};
use yazi_macro::act;
use yazi_parser::app::DeprecateForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Deprecate;
impl Actor for Deprecate {
type Form = DeprecateForm;
const NAME: &str = "deprecate";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
act!(notify:push, cx, MessageOpt {
title: "Deprecated API".to_owned(),
content: form.content.into_owned(),
level: MessageLevel::Warn,
timeout: std::time::Duration::from_secs(20),
})
}
}

43
yazi-actor/src/app/dnd.rs Normal file
View file

@ -0,0 +1,43 @@
use anyhow::Result;
use mlua::{ObjectLike, Table};
use tracing::error;
use yazi_actor::lives::Lives;
use yazi_binding::runtime_scope;
use yazi_macro::succ;
use yazi_parser::app::DndForm;
use yazi_plugin::LUA;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Dnd;
impl Actor for Dnd {
type Form = DndForm;
const NAME: &str = "dnd";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() };
let area = yazi_binding::elements::Rect::from(size);
let result = Lives::scope(cx.core, move |_| {
runtime_scope!(LUA, "root", {
let root = LUA.globals().raw_get::<Table>("Root")?.call_method::<Table>("new", area)?;
if form.event.is_drag() {
root.call_method::<()>("drag", form.event)?;
} else {
root.call_method::<()>("drop", form.event)?;
}
Ok(())
})
});
if let Err(ref e) = result {
error!("{e}");
}
succ!(result?);
}
}

View file

@ -0,0 +1,17 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::act;
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use crate::Actor;
pub struct Focus;
impl Actor for Focus {
type Form = VoidForm;
const NAME: &str = "focus";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> { act!(mgr:refresh, cx) }
}

24
yazi-actor/src/app/lua.rs Normal file
View file

@ -0,0 +1,24 @@
use anyhow::Result;
use yazi_binding::runtime_scope;
use yazi_macro::succ;
use yazi_parser::app::LuaForm;
use yazi_plugin::LUA;
use yazi_shared::data::{Data, Sendable};
use crate::{Actor, Ctx, lives::Lives};
pub struct Lua;
impl Actor for Lua {
type Form = LuaForm;
const NAME: &str = "lua";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let chunk = LUA.load(&*form.code).set_name("anonymous");
let result = Lives::scope(cx.core, |_| {
runtime_scope!(LUA, "inline", Sendable::value_to_data(&LUA, chunk.eval()?))
});
succ!(result?);
}
}

View file

@ -2,16 +2,18 @@ yazi_macro::mod_flat!(
accept_payload accept_payload
bootstrap bootstrap
deprecate deprecate
dnd
focus focus
lua
mouse mouse
notify
plugin plugin
plugin_do
quit quit
reflow reflow
render
resize resize
resume resume
stop stop
update_notify theme
title
update_progress update_progress
); );

View file

@ -0,0 +1,52 @@
use anyhow::Result;
use mlua::{ObjectLike, Table};
use tracing::error;
use yazi_actor::lives::Lives;
use yazi_binding::runtime_scope;
use yazi_macro::succ;
use yazi_parser::app::MouseForm;
use yazi_plugin::LUA;
use yazi_shared::data::Data;
use yazi_term::event::MouseEventKind;
use crate::{Actor, Ctx};
pub struct Mouse;
impl Actor for Mouse {
type Form = MouseForm;
const NAME: &str = "mouse";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() };
let area = yazi_binding::elements::Rect::from(size);
let result = Lives::scope(cx.core, move |_| {
runtime_scope!(LUA, "root", {
let root = LUA.globals().raw_get::<Table>("Root")?.call_method::<Table>("new", area)?;
match form.event.kind {
MouseEventKind::Down(_) => root.call_method("click", (form.event, false))?,
MouseEventKind::Up(_) => root.call_method("click", (form.event, true))?,
MouseEventKind::ScrollDown => root.call_method("scroll", (form.event, 1))?,
MouseEventKind::ScrollUp => root.call_method("scroll", (form.event, -1))?,
MouseEventKind::ScrollRight => root.call_method("touch", (form.event, 1))?,
MouseEventKind::ScrollLeft => root.call_method("touch", (form.event, -1))?,
MouseEventKind::Moved => root.call_method("move", form.event)?,
MouseEventKind::Drag(_) => root.call_method("drag", form.event)?,
}
Ok(())
})
});
if let Err(ref e) = result {
error!("{e}");
}
succ!(result?);
}
}

View file

@ -0,0 +1,40 @@
use anyhow::Result;
use yazi_core::app::PluginMode;
use yazi_macro::{act, succ};
use yazi_parser::app::PluginForm;
use yazi_proxy::AppProxy;
use yazi_runner::loader::LOADER;
use yazi_scheduler::NotifyProxy;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Plugin;
impl Actor for Plugin {
type Form = PluginForm;
const NAME: &str = "plugin";
fn act(cx: &mut Ctx, Self::Form { mut opt }: Self::Form) -> Result<Data> {
let mut hits = false;
if let Some(chunk) = LOADER.read().get(&*opt.id) {
hits = true;
opt.mode = opt.mode.auto_then(chunk.sync_entry);
}
if opt.mode == PluginMode::Async {
succ!(cx.core.tasks.scheduler.plugin_entry(opt.into()));
} else if opt.mode == PluginMode::Sync && hits {
return act!(app:plugin_do, cx, opt);
}
tokio::spawn(async move {
match LOADER.ensure(&opt.id, |_| ()).await {
Ok(()) => AppProxy::plugin_do(opt),
Err(e) => NotifyProxy::push_error("Plugin load failed", e.to_string()),
}
});
succ!();
}
}

View file

@ -0,0 +1,58 @@
use anyhow::Result;
use mlua::ObjectLike;
use scopeguard::defer;
use tracing::{error, warn};
use yazi_binding::runtime_mut;
use yazi_core::app::PluginMode;
use yazi_macro::succ;
use yazi_parser::app::PluginForm;
use yazi_plugin::LUA;
use yazi_runner::{entry::EntryJob, loader::{LOADER, Loader}};
use yazi_scheduler::NotifyProxy;
use yazi_shared::data::Data;
use crate::{Actor, Ctx, lives::Lives};
pub struct PluginDo;
impl Actor for PluginDo {
type Form = PluginForm;
const NAME: &str = "plugin_do";
fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> {
let loader = LOADER.read();
let Some(chunk) = loader.get(&*opt.id) else {
succ!(warn!("plugin `{}` not found", opt.id));
};
if let Err(e) = Loader::compatible_or_error(&opt.id, chunk) {
succ!(NotifyProxy::push_error("Incompatible plugin", e.to_string()));
}
if opt.mode.auto_then(chunk.sync_entry) != PluginMode::Sync {
succ!(cx.core.tasks.scheduler.plugin_entry(opt.into()));
}
let blocking = runtime_mut!(LUA)?.critical_push(&opt.id, true);
defer! { _ = runtime_mut!(LUA).map(|mut r| r.critical_pop(blocking)) }
let plugin = match LOADER.load_chunk(&LUA, &opt.id, chunk) {
Ok(t) => t,
Err(e) => succ!(warn!("{e}")),
};
drop(loader);
let result = Lives::scope(cx.core, |_| {
if let Some(cb) = opt.callback {
cb(&LUA, plugin)
} else {
plugin.call_method("entry", EntryJob { args: opt.args, ..Default::default() })
}
});
if let Err(ref e) = result {
error!("Sync plugin `{}` failed: {e}", opt.id);
}
succ!(result?);
}
}

View file

@ -0,0 +1,50 @@
use std::process;
use anyhow::Result;
use yazi_boot::ARGS;
use yazi_fs::engine::{Engine, local::Local};
use yazi_parser::app::QuitForm;
use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}};
use yazi_tui::Raterm;
use crate::{Actor, Ctx};
pub struct Quit;
impl Actor for Quit {
type Form = QuitForm;
const NAME: &str = "quit";
fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> {
cx.tasks.shutdown();
cx.mgr.shutdown();
futures::executor::block_on(async {
_ = futures::join!(
yazi_dds::shutdown(),
yazi_dds::STATE.drain(),
Self::cwd_to_file(cx, opt.no_cwd_file),
Self::selected_to_file(opt.selected)
);
});
Raterm::stop();
process::exit(opt.code);
}
}
impl Quit {
async fn cwd_to_file(cx: &Ctx<'_>, no: bool) {
if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) {
let cwd = cx.mgr.cwd().to_strand();
Local::regular(p).write(cwd.encoded_bytes()).await.ok();
}
}
async fn selected_to_file(selected: Option<StrandBuf>) {
if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) {
Local::regular(p).write(s.encoded_bytes()).await.ok();
}
}
}

View file

@ -1,22 +1,28 @@
use anyhow::Result; use anyhow::Result;
use mlua::Value; use mlua::{LuaString, Value};
use ratatui::layout::Position; use ratatui_core::layout::Position;
use tracing::error; use tracing::error;
use yazi_actor::lives::Lives; use yazi_actor::lives::Lives;
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::VoidOpt; use yazi_parser::app::ReflowForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Root, app::App}; use crate::{Actor, Ctx};
impl App { pub struct Reflow;
pub fn reflow(&mut self, _: VoidOpt) -> Result<Data> {
let Some(size) = self.term.as_ref().and_then(|t| t.size().ok()) else { succ!() }; impl Actor for Reflow {
type Form = ReflowForm;
const NAME: &str = "reflow";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some(size) = cx.term.as_ref().and_then(|t| t.size().ok()) else { succ!() };
let mut layout = LAYOUT.get(); let mut layout = LAYOUT.get();
let result = Lives::scope(&self.core, || { let result = Lives::scope(cx.core, |_| {
let comps = Root::reflow((Position::ORIGIN, size).into())?; let comps = (form.reflow)((Position::ORIGIN, size).into())?;
for v in comps.sequence_values::<Value>() { for v in comps.sequence_values::<Value>() {
let Value::Table(t) = v? else { let Value::Table(t) = v? else {
@ -24,7 +30,7 @@ impl App {
continue; continue;
}; };
let id: mlua::String = t.get("_id")?; let id: LuaString = t.get("_id")?;
match &*id.as_bytes() { match &*id.as_bytes() {
b"current" => layout.current = *t.raw_get::<yazi_binding::elements::Rect>("_area")?, b"current" => layout.current = *t.raw_get::<yazi_binding::elements::Rect>("_area")?,
b"preview" => layout.preview = *t.raw_get::<yazi_binding::elements::Rect>("_area")?, b"preview" => layout.preview = *t.raw_get::<yazi_binding::elements::Rect>("_area")?,

View file

@ -0,0 +1,25 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::act;
use yazi_parser::app::ReflowForm;
use yazi_shared::data::Data;
use crate::Actor;
pub struct Resize;
impl Actor for Resize {
type Form = ReflowForm;
const NAME: &str = "resize";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
act!(app:reflow, cx, form)?;
cx.current_mut().arrow(0);
cx.parent_mut().map(|f| f.arrow(0));
cx.current_mut().sync_page(true);
act!(mgr:peek, cx)
}
}

View file

@ -0,0 +1,29 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::app::ReflowForm;
use yazi_shared::data::Data;
use yazi_tui::Raterm;
use crate::{Actor, Ctx};
pub struct Resume;
impl Actor for Resume {
type Form = ReflowForm;
const NAME: &str = "resume";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
cx.active_mut().preview.reset();
drop(cx.term.take());
*cx.term = Some(Raterm::start()?);
// While the app resumes, it's possible that the terminal size has changed.
// We need to trigger a resize, and render the UI based on the resized area.
act!(app:resize, cx, form.reflow)?;
act!(app:title, cx).ok();
succ!(render!());
}
}

View file

@ -0,0 +1,22 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Stop;
impl Actor for Stop {
type Form = VoidForm;
const NAME: &str = "stop";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
cx.active_mut().preview.reset_image();
*cx.term = None;
succ!();
}
}

View file

@ -0,0 +1,25 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_config::{THEME, build_flavor};
use yazi_emulator::EMULATOR;
use yazi_macro::{render, succ};
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use yazi_shim::serde::Overlay;
use crate::Actor;
pub struct Theme;
impl Actor for Theme {
type Form = VoidForm;
const NAME: &str = "theme";
fn act(_cx: &mut Ctx, _: Self::Form) -> Result<Data> {
THEME.overlay(build_flavor(EMULATOR.light, true)?);
yazi_plugin::theme::reset()?;
succ!(render!());
}
}

View file

@ -0,0 +1,32 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::{succ, writef};
use yazi_parser::{app::TitleForm, spark::SparkKind};
use yazi_shared::{Source, data::Data};
use yazi_tty::{TTY, sequence::SetTitle};
use yazi_tui::RatermState;
use crate::Actor;
pub struct Title;
impl Actor for Title {
type Form = TitleForm;
const NAME: &str = "title";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let s = form.value.unwrap_or_else(|| format!("Yazi: {}", cx.tab().name()).into());
writef!(TTY.writer(), "{}", SetTitle(&s))?;
yazi_tui::STATE.set(RatermState { title: !s.is_empty(), ..yazi_tui::STATE.get() });
succ!()
}
fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> {
match cx.source() {
Source::Ind => Some(SparkKind::IndAppTitle),
_ => None,
}
}
}

View file

@ -1,24 +1,29 @@
use anyhow::Result; use anyhow::Result;
use yazi_actor::Ctx; use yazi_actor::Ctx;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, render_partial, succ};
use yazi_parser::app::UpdateProgressOpt; use yazi_parser::app::UpdateProgressForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::app::App; use crate::Actor;
impl App { pub struct UpdateProgress;
pub(crate) fn update_progress(&mut self, opt: UpdateProgressOpt) -> Result<Data> {
impl Actor for UpdateProgress {
type Form = UpdateProgressForm;
const NAME: &str = "update_progress";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
// Update the progress of all tasks. // Update the progress of all tasks.
let tasks = &mut self.core.tasks; let tasks = &mut cx.tasks;
let progressed = tasks.summary != opt.summary; let progressed = tasks.summary != form.summary;
tasks.summary = opt.summary; tasks.summary = form.summary;
// If the task manager is visible, update the snaps with a full render. // If the task manager is visible, update the snaps with a full render.
if tasks.visible { if tasks.visible {
let new = tasks.paginate(); let new = tasks.paginate();
if tasks.snaps != new { if tasks.snaps != new {
tasks.snaps = new; tasks.snaps = new;
let cx = &mut Ctx::active(&mut self.core);
act!(tasks:arrow, cx)?; act!(tasks:arrow, cx)?;
succ!(render!()); succ!(render!());
} }
@ -29,7 +34,7 @@ impl App {
} else if tasks.summary.total == 0 { } else if tasks.summary.total == 0 {
succ!(render!()) succ!(render!())
} else { } else {
act!(render_partially, self) succ!(render_partial!())
} }
} }
} }

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::ArrowOpt; use yazi_parser::ArrowForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_widgets::Scrollable; use yazi_widgets::Scrollable;
@ -9,11 +9,11 @@ use crate::{Actor, Ctx};
pub struct Arrow; pub struct Arrow;
impl Actor for Arrow { impl Actor for Arrow {
type Options = ArrowOpt; type Form = ArrowForm;
const NAME: &str = "arrow"; const NAME: &str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
succ!(render!(cx.cmp.scroll(opt.step))); succ!(render!(cx.cmp.scroll(form.step)));
} }
} }

View file

@ -2,26 +2,46 @@ use std::mem;
use anyhow::Result; use anyhow::Result;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::{cmp::CloseOpt, input::CompleteOpt}; use yazi_parser::cmp::CloseForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_widgets::input::parser::CompleteOpt;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Close; pub struct Close;
impl Actor for Close { impl Actor for Close {
type Options = CloseOpt; type Form = CloseForm;
const NAME: &str = "close"; const NAME: &str = "close";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let cmp = &mut cx.core.cmp; if form.submit && cx.cmp.visible {
if let Some(item) = cmp.selected().filter(|_| opt.submit).cloned() { Self::flush_last_input(cx)?;
return act!(input:complete, cx, CompleteOpt { item, ticket: cmp.ticket }); }
let cmp = &mut cx.cmp;
if let Some(item) = cmp.selected().filter(|_| form.submit).cloned() {
return act!(input:complete, cx, CompleteOpt { name: item.name, is_dir: item.is_dir, ticket: cmp.ticket });
} }
cmp.caches.clear(); cmp.caches.clear();
cmp.ticket = Default::default(); cmp.ticket = Default::default();
cmp.handle.take().map(|h| h.abort());
succ!(render!(mem::replace(&mut cmp.visible, false))); succ!(render!(mem::replace(&mut cmp.visible, false)));
} }
} }
impl Close {
fn flush_last_input(cx: &mut Ctx) -> Result<Data> {
let Some(guard) = cx.input.lock() else { succ!() };
if cx.cmp.ticket == guard.ticket.current() {
succ!();
}
let before = guard.partition().0.to_owned();
drop(guard);
act!(cmp:trigger, cx, before)
}
}

View file

@ -1,9 +1,10 @@
use std::{mem, ops::ControlFlow}; use std::{mem, ops::ControlFlow};
use anyhow::Result; use anyhow::Result;
use yazi_core::cmp::CmpItem;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::cmp::{CmpItem, ShowOpt}; use yazi_parser::cmp::ShowForm;
use yazi_shared::{data::Data, path::{AsPath, PathDyn}, strand::StrandLike}; use yazi_shared::{data::Data, path::{DynPath, PathDyn}, strand::StrandLike};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -12,11 +13,11 @@ const LIMIT: usize = 30;
pub struct Show; pub struct Show;
impl Actor for Show { impl Actor for Show {
type Options = ShowOpt; type Form = ShowForm;
const NAME: &str = "show"; const NAME: &str = "show";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> {
let cmp = &mut cx.cmp; let cmp = &mut cx.cmp;
if cmp.ticket != opt.ticket { if cmp.ticket != opt.ticket {
succ!(); succ!();
@ -29,8 +30,8 @@ impl Actor for Show {
succ!(); succ!();
}; };
cmp.cands = Self::match_candidates(opt.word.as_path(), cache); cmp.matches = Self::match_candidates(opt.word.dyn_path(), cache);
if cmp.cands.is_empty() { if cmp.matches.is_empty() {
succ!(render!(mem::replace(&mut cmp.visible, false))); succ!(render!(mem::replace(&mut cmp.visible, false)));
} }

View file

@ -1,43 +1,42 @@
use std::mem; use std::{io, mem};
use anyhow::Result; use anyhow::Result;
use yazi_fs::{path::clean_url, provider::{DirReader, FileHolder}}; use yazi_core::cmp::{CmpItem, CmpOpt};
use yazi_fs::{engine::{DirReader, FileHolder}, path::clean_url};
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_parser::cmp::TriggerForm;
use yazi_proxy::CmpProxy; use yazi_proxy::CmpProxy;
use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{DynPath, PathBufDyn, PathLike}, spec::Spec, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}};
use yazi_vfs::provider; use yazi_vfs::engine;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Trigger; pub struct Trigger;
impl Actor for Trigger { impl Actor for Trigger {
type Options = TriggerOpt; type Form = TriggerForm;
const NAME: &str = "trigger"; const NAME: &str = "trigger";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let cmp = &mut cx.cmp; if form.ticket.is_some_and(|t| t != cx.cmp.ticket) {
if let Some(t) = opt.ticket { succ!();
if t < cmp.ticket { } else if form.ticket.is_none() {
succ!(); cx.cmp.ticket = cx.input.lock().map(|g| g.ticket.current()).unwrap_or_default();
}
cmp.ticket = t;
} }
let Some((parent, word)) = Self::split_url(&opt.word) else { cx.cmp.handle.take().map(|h| h.abort());
let Some((parent, word)) = Self::split_url(&form.word) else {
return act!(cmp:close, cx, false); return act!(cmp:close, cx, false);
}; };
if cmp.caches.contains_key(&parent) { let ticket = cx.cmp.ticket;
let ticket = cmp.ticket; if cx.cmp.caches.contains_key(&parent) {
return act!(cmp:show, cx, ShowOpt { cache: vec![], cache_name: parent, word, ticket }); return act!(cmp:show, cx, CmpOpt { cache: vec![], cache_name: parent, word, ticket });
} }
let ticket = cmp.ticket; cx.cmp.handle = Some(tokio::spawn(async move {
tokio::spawn(async move { let mut dir = engine::read_dir(&parent).await?;
let mut dir = provider::read_dir(&parent).await?;
let mut cache = vec![]; let mut cache = vec![];
// "/" is both a directory separator and the root directory per se // "/" is both a directory separator and the root directory per se
@ -54,45 +53,39 @@ impl Actor for Trigger {
if !cache.is_empty() { if !cache.is_empty() {
cache cache
.sort_unstable_by(|a, b| natsort(a.name.encoded_bytes(), b.name.encoded_bytes(), false)); .sort_unstable_by(|a, b| natsort(a.name.encoded_bytes(), b.name.encoded_bytes(), true));
CmpProxy::show(ShowOpt { cache, cache_name: parent, word, ticket }); CmpProxy::show(CmpOpt { cache, cache_name: parent, word, ticket });
} }
Ok::<_, anyhow::Error>(()) Ok::<_, io::Error>(())
}); }));
succ!(render!(mem::replace(&mut cmp.visible, false))); succ!(render!(mem::replace(&mut cx.cmp.visible, false)));
} }
} }
impl Trigger { impl Trigger {
fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> { fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> {
let sep = if cfg!(windows) { let (spec, path) = Spec::parse(s.as_bytes()).ok()?;
AnyAsciiChar::new(b"/\\").unwrap() if path.is_empty() && !AnyAsciiChar::SEP.predicate(s.bytes().last()?) {
} else {
AnyAsciiChar::new(b"/").unwrap()
};
let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?;
if path.is_empty() && !sep.predicate(s.bytes().last()?) {
return None; // We don't complete a `sftp://test`, but `sftp://test/` return None; // We don't complete a `sftp://test`, but `sftp://test/`
} }
// Scheme // Spec
let scheme = scheme.zeroed(); let spec = spec.zeroed();
if scheme.is_local() && path.as_strand() == "~" { if spec.kind.is_local() && path.as_strand() == "~" {
return None; // We don't complete a `~`, but `~/` return None; // We don't complete a `~`, but `~/`
} }
// Child // Child
let child = path.rsplit_pred(sep).map_or(path.as_path(), |(_, c)| c).to_owned(); let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.dyn_path(), |(_, c)| c).to_owned();
// Parent // Parent
let url = UrlCow::try_from((scheme.clone().zeroed(), path)).ok()?; let url = UrlCow::try_from((spec.clone().zeroed(), path)).ok()?;
let abs = if let Some(u) = provider::try_absolute(&url) { u } else { url }; let abs = if let Some(u) = engine::try_absolute(&url) { u } else { url };
let parent = abs.loc().try_strip_suffix(&child).ok()?; let parent = abs.loc().try_strip_suffix(&child).ok()?;
Some((clean_url(UrlCow::try_from((scheme, parent)).ok()?), child)) Some((clean_url(UrlCow::try_from((spec, parent)).ok()?), child))
} }
} }
@ -115,10 +108,11 @@ mod tests {
#[test] #[test]
fn test_split() { fn test_split() {
yazi_shared::init_tests(); yazi_shared::init_tests();
yazi_config::init_tests();
yazi_fs::init(); yazi_fs::init();
assert_eq!(Trigger::split_url(""), None); assert_eq!(Trigger::split_url(""), None);
assert_eq!(Trigger::split_url("sftp://test"), None); assert_eq!(Trigger::split_url("sftp://vps"), None);
compare(" ", "", " "); compare(" ", "", " ");
compare("/", "/", ""); compare("/", "/", "");
@ -134,16 +128,18 @@ mod tests {
compare("/foo/bar", "/foo/", "bar"); compare("/foo/bar", "/foo/", "bar");
compare("///foo/bar", "/foo/", "bar"); compare("///foo/bar", "/foo/", "bar");
CWD.set(&"sftp://test".parse::<UrlBuf>().unwrap(), || {}); CWD.set(&"sftp://vps".parse::<UrlBuf>().unwrap(), || {});
compare("sftp://test/a", "sftp://test/.", "a"); compare("sftp://vps/a", "sftp://vps/.", "a");
compare("sftp://test//a", "sftp://test//", "a"); compare("sftp://vps//a", "sftp://vps//", "a");
compare("sftp://test2/a", "sftp://test2/.", "a"); compare("test-scope://aws/a", "test-scope://aws/.", "a");
compare("sftp://test2//a", "sftp://test2//", "a"); compare("test-scope://aws//a", "test-scope://aws//", "a");
} }
#[cfg(windows)] #[cfg(windows)]
#[test] #[test]
fn test_split() { fn test_split() {
yazi_shared::init_tests();
yazi_config::init_tests();
yazi_fs::init(); yazi_fs::init();
compare("foo", "", "foo"); compare("foo", "", "foo");

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::ArrowOpt; use yazi_parser::ArrowForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -8,18 +8,18 @@ use crate::{Actor, Ctx};
pub struct Arrow; pub struct Arrow;
impl Actor for Arrow { impl Actor for Arrow {
type Options = ArrowOpt; type Form = ArrowForm;
const NAME: &str = "arrow"; const NAME: &str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let confirm = &mut cx.core.confirm; let confirm = &mut cx.core.confirm;
let area = cx.core.mgr.area(confirm.position); let area = cx.core.mgr.area(confirm.position);
let len = confirm.list.line_count(area.width); let len = confirm.list.line_count(area.width);
let old = confirm.offset; let old = confirm.offset;
confirm.offset = opt.step.add(confirm.offset, len, area.height as _); confirm.offset = form.step.add(confirm.offset, len, area.height as _, 0, 0);
succ!(render!(old != confirm.offset)); succ!(render!(old != confirm.offset));
} }

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::confirm::CloseOpt; use yazi_parser::confirm::CloseForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -8,15 +8,12 @@ use crate::{Actor, Ctx};
pub struct Close; pub struct Close;
impl Actor for Close { impl Actor for Close {
type Options = CloseOpt; type Form = CloseForm;
const NAME: &str = "close"; const NAME: &str = "close";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
if let Some(cb) = cx.confirm.callback.take() { cx.confirm.token.complete(form.submit);
_ = cb.send(opt.submit);
}
cx.confirm.visible = false; cx.confirm.visible = false;
succ!(render!()); succ!(render!());
} }

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::confirm::ShowOpt; use yazi_parser::confirm::ShowForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -8,22 +8,22 @@ use crate::{Actor, Ctx};
pub struct Show; pub struct Show;
impl Actor for Show { impl Actor for Show {
type Options = ShowOpt; type Form = ShowForm;
const NAME: &str = "show"; const NAME: &str = "show";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
act!(confirm:close, cx)?; act!(confirm:close, cx)?;
let confirm = &mut cx.confirm; let confirm = &mut cx.confirm;
confirm.title = opt.cfg.title; confirm.title = form.cfg.title;
confirm.body = opt.cfg.body; confirm.body = form.cfg.body;
confirm.list = opt.cfg.list; confirm.list = form.cfg.list;
confirm.position = opt.cfg.position; confirm.position = form.cfg.position;
confirm.offset = 0; confirm.offset = 0;
confirm.callback = Some(opt.tx); confirm.token = form.token;
confirm.visible = true; confirm.visible = true;
succ!(render!()); succ!(render!());

View file

@ -1,12 +1,14 @@
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}, tasks::Tasks}; use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}};
use yazi_fs::File; use yazi_fs::file::File;
use yazi_shared::{Id, Source, event::Cmd, url::UrlBuf}; use yazi_shared::{Source, event::Action, id::Id, url::UrlBuf};
use yazi_tui::Raterm;
pub struct Ctx<'a> { pub struct Ctx<'a> {
pub core: &'a mut Core, pub core: &'a mut Core,
pub term: &'a mut Option<Raterm>,
pub tab: usize, pub tab: usize,
pub level: usize, pub level: usize,
pub source: Source, pub source: Source,
@ -25,9 +27,8 @@ impl DerefMut for Ctx<'_> {
} }
impl<'a> Ctx<'a> { impl<'a> Ctx<'a> {
#[inline] pub fn new(action: &Action, core: &'a mut Core, term: &'a mut Option<Raterm>) -> Result<Self> {
pub fn new(core: &'a mut Core, cmd: &Cmd) -> Result<Self> { let tab = if let Ok(id) = action.get::<Id>("tab") {
let tab = if let Ok(id) = cmd.get::<Id>("tab") {
core core
.mgr .mgr
.tabs .tabs
@ -40,19 +41,20 @@ impl<'a> Ctx<'a> {
Ok(Self { Ok(Self {
core, core,
term,
tab, tab,
level: 0, level: 0,
source: cmd.source, source: action.source,
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
backtrace: vec![], backtrace: vec![],
}) })
} }
#[inline]
pub fn renew<'b>(cx: &'a mut Ctx<'b>) -> Self { pub fn renew<'b>(cx: &'a mut Ctx<'b>) -> Self {
let tab = cx.core.mgr.tabs.cursor; let tab = cx.core.mgr.tabs.cursor;
Self { Self {
core: cx.core, core: cx.core,
term: cx.term,
tab, tab,
level: cx.level, level: cx.level,
source: cx.source, source: cx.source,
@ -61,11 +63,11 @@ impl<'a> Ctx<'a> {
} }
} }
#[inline] pub fn active(core: &'a mut Core, term: &'a mut Option<Raterm>) -> Self {
pub fn active(core: &'a mut Core) -> Self {
let tab = core.mgr.tabs.cursor; let tab = core.mgr.tabs.cursor;
Self { Self {
core, core,
term,
tab, tab,
level: 0, level: 0,
source: Source::Unknown, source: Source::Unknown,
@ -106,6 +108,9 @@ impl<'a> Ctx<'a> {
#[inline] #[inline]
pub fn hovered(&self) -> Option<&File> { self.tab().hovered() } pub fn hovered(&self) -> Option<&File> { self.tab().hovered() }
#[inline]
pub fn hovered_url(&self) -> Option<&UrlBuf> { self.tab().hovered_url() }
#[inline] #[inline]
pub fn hovered_folder(&self) -> Option<&Folder> { self.tab().hovered_folder() } pub fn hovered_folder(&self) -> Option<&Folder> { self.tab().hovered_folder() }
@ -114,9 +119,5 @@ impl<'a> Ctx<'a> {
self.tab_mut().hovered_folder_mut() self.tab_mut().hovered_folder_mut()
} }
#[inline]
pub fn tasks(&self) -> &Tasks { &self.tasks }
#[inline]
pub fn source(&self) -> Source { if self.level != 1 { Source::Ind } else { self.source } } pub fn source(&self) -> Source { if self.level != 1 { Source::Ind } else { self.source } }
} }

View file

@ -1,7 +1,8 @@
use anyhow::Result; use anyhow::Result;
use mlua::{ErrorContext, ExternalError, IntoLua, Value}; use mlua::{ErrorContext, ExternalError, IntoLua, Value};
use yazi_binding::runtime_mut; use yazi_binding::runtime_scope;
use yazi_dds::{LOCAL, spark::{Spark, SparkKind}}; use yazi_dds::LOCAL;
use yazi_parser::spark::{Spark, SparkKind};
use yazi_plugin::LUA; use yazi_plugin::LUA;
use crate::{Ctx, lives::Lives}; use crate::{Ctx, lives::Lives};
@ -11,18 +12,14 @@ pub struct Preflight;
impl Preflight { impl Preflight {
pub fn act<'a>(cx: &mut Ctx, opt: (SparkKind, Spark<'a>)) -> Result<Spark<'a>> { pub fn act<'a>(cx: &mut Ctx, opt: (SparkKind, Spark<'a>)) -> Result<Spark<'a>> {
let kind = opt.0; let kind = opt.0;
let Some(handlers) = LOCAL.read().get(kind.as_ref()).filter(|&m| !m.is_empty()).cloned() else { let Some(handlers) = LOCAL.read().get(kind.into()).filter(|&m| !m.is_empty()).cloned() else {
return Ok(opt.1); return Ok(opt.1);
}; };
Ok(Lives::scope(cx.core, || { Ok(Lives::scope(cx.core, |_| {
let mut body = opt.1.into_lua(&LUA)?; let mut body = opt.1.into_lua(&LUA)?;
for (id, cb) in handlers { for (id, cb) in handlers {
runtime_mut!(LUA)?.push(&id); match runtime_scope!(LUA, &id, cb.call::<Value>(&body)) {
let result = cb.call::<Value>(&body);
runtime_mut!(LUA)?.pop();
match result {
Ok(Value::Nil) => { Ok(Value::Nil) => {
Err(format!("`{kind}` event cancelled by `{id}` plugin on preflight").into_lua_err())? Err(format!("`{kind}` event cancelled by `{id}` plugin on preflight").into_lua_err())?
} }

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::ArrowOpt; use yazi_parser::ArrowForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_widgets::Scrollable; use yazi_widgets::Scrollable;
@ -9,11 +9,11 @@ use crate::{Actor, Ctx};
pub struct Arrow; pub struct Arrow;
impl Actor for Arrow { impl Actor for Arrow {
type Options = ArrowOpt; type Form = ArrowForm;
const NAME: &str = "arrow"; const NAME: &str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
succ!(render!(cx.help.scroll(opt.step))); succ!(render!(cx.help.scroll(form.step)));
} }
} }

View file

@ -0,0 +1,25 @@
use anyhow::Result;
use yazi_macro::{emit, render, succ};
use yazi_parser::help::CloseForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Form = CloseForm;
const NAME: &str = "close";
fn act(cx: &mut Ctx, opt: Self::Form) -> Result<Data> {
let help = &mut cx.help;
if let Some(chord) = help.bindings.get(help.cursor).filter(|_| opt.submit) {
emit!(Seq(chord.to_seq(help.layer)));
}
help.visible = false;
succ!(render!());
}
}

View file

@ -1,27 +1,24 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::VoidOpt; use yazi_parser::VoidForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_widgets::input::InputMode;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Escape; pub struct Escape;
impl Actor for Escape { impl Actor for Escape {
type Options = VoidOpt; type Form = VoidForm;
const NAME: &str = "escape"; const NAME: &str = "escape";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
if cx.help.keyword().is_none() { if cx.help.input.mode() == InputMode::Normal {
return act!(help:toggle, cx, cx.help.layer); return act!(help:close, cx);
} }
let help = &mut cx.help; act!(escape, cx.help.input)?;
help.keyword = String::new();
help.in_filter = None;
help.filter_apply();
succ!(render!()); succ!(render!());
} }
} }

View file

@ -1,22 +0,0 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::VoidOpt;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Filter;
impl Actor for Filter {
type Options = VoidOpt;
const NAME: &str = "filter";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let help = &mut cx.help;
help.in_filter = Some(Default::default());
help.filter_apply();
succ!(render!());
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(arrow escape filter toggle); yazi_macro::mod_flat!(arrow close escape toggle);

View file

@ -1,29 +1,39 @@
use anyhow::Result; use anyhow::Result;
use ratatui_core::layout::Margin;
use yazi_config::popup::Help;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::help::ToggleOpt; use yazi_parser::help::ToggleForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_widgets::input::Input;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Toggle; pub struct Toggle;
impl Actor for Toggle { impl Actor for Toggle {
type Options = ToggleOpt; type Form = ToggleForm;
const NAME: &str = "toggle"; const NAME: &str = "toggle";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let position = Help::position();
let area = cx.mgr.area(position);
let input_area = area.inner(Margin::new(1, 1));
let help = &mut cx.help; let help = &mut cx.help;
help.visible = true;
help.layer = form.layer;
help.position = position;
help.height = area.height;
help.visible = !help.visible; help.input = Input::default();
help.layer = opt.layer; help.input.repos(input_area);
help.keyword = String::new();
help.in_filter = None;
help.filter_apply();
help.keyword.clear();
help.offset = 0; help.offset = 0;
help.cursor = 0; help.cursor = 0;
help.filter_apply();
succ!(render!()); succ!(render!());
} }
} }

View file

@ -1,28 +1,47 @@
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::CloseOpt; use yazi_parser::{input::CloseForm, spark::SparkKind};
use yazi_shared::{data::Data, errors::InputError}; use yazi_shared::{Source, data::Data};
use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Close; pub struct Close;
impl Actor for Close { impl Actor for Close {
type Options = CloseOpt; type Form = CloseForm;
const NAME: &str = "close"; const NAME: &str = "close";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let input = &mut cx.input; let Some(mut guard) = cx.input.lock_mut() else {
input.visible = false; succ!();
input.ticket.next(); };
if let Some(tx) = input.tx.take() { guard.ticket.next();
let value = input.snap().value.clone(); if let Some(cb) = guard.cb.take() {
_ = tx.send(if opt.submit { Ok(value) } else { Err(InputError::Canceled(value)) }); let value = guard.value().to_owned();
cb(if form.submit { InputEvent::Submit(value) } else { InputEvent::Cancel(value) });
} }
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)?; act!(cmp:close, cx)?;
succ!(render!()); succ!(render!());
} }
fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> {
match cx.source() {
Source::Key => Some(SparkKind::KeyInputClose),
Source::Ind => Some(SparkKind::IndInputClose),
_ => None,
}
}
} }

View file

@ -1,23 +1,26 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{act, succ}; use yazi_macro::{act, succ};
use yazi_parser::input::CompleteOpt;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_widgets::input::parser::CompleteOpt;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Complete; pub struct Complete;
impl Actor for Complete { impl Actor for Complete {
type Options = CompleteOpt; type Form = CompleteOpt;
const NAME: &str = "complete"; const NAME: &str = "complete";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let input = &mut cx.input; let Some(mut guard) = cx.input.lock_mut() else {
if !input.visible || input.ticket.current() != opt.ticket { succ!();
};
if guard.ticket.current() != form.ticket {
succ!(); succ!();
} }
act!(complete, input, opt) act!(complete, guard, form)
} }
} }

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::VoidOpt; use yazi_parser::VoidForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_widgets::input::InputOp; use yazi_widgets::input::InputOp;
@ -9,22 +9,26 @@ use crate::{Actor, Ctx};
pub struct Escape; pub struct Escape;
impl Actor for Escape { impl Actor for Escape {
type Options = VoidOpt; type Form = VoidForm;
const NAME: &str = "escape"; const NAME: &str = "escape";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
use yazi_widgets::input::InputMode as M; use yazi_widgets::input::InputMode as M;
let input = &mut cx.input; let Some(mut guard) = cx.input.lock_mut() else {
succ!();
};
let mode = input.snap().mode; let (mode, op) = (guard.snap().mode, guard.snap().op);
act!(escape, guard)?;
drop(guard);
match mode { match mode {
M::Normal if input.snap_mut().op == InputOp::None => act!(input:close, cx), M::Normal if op == InputOp::None => act!(input:close, cx),
M::Insert => act!(cmp:close, cx), M::Insert => act!(cmp:close, cx),
M::Normal | M::Replace => Ok(().into()), M::Normal | M::Replace => Ok(().into()),
}?; }?;
act!(escape, cx.input)?;
succ!(render!()); succ!(render!());
} }
} }

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

@ -1,54 +1,35 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use anyhow::Result; use anyhow::Result;
use yazi_config::YAZI; use yazi_config::{THEME, YAZI};
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::input::ShowOpt; use yazi_parser::input::ShowForm;
use yazi_shared::{data::Data, errors::InputError}; use yazi_shared::data::Data;
use yazi_widgets::input::InputCallback; use yazi_shim::ratatui::Padable;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Show; pub struct Show;
impl Actor for Show { impl Actor for Show {
type Options = ShowOpt; type Form = ShowForm;
const NAME: &str = "show"; const NAME: &str = "show";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, Self::Form { mut opt }: Self::Form) -> Result<Data> {
act!(input:close, cx)?; act!(input:close, cx)?;
let area = cx.mgr.area(opt.position).padding(cx.input.padding());
let input = &mut cx.input; let input = &mut cx.input;
input.visible = true; input.main.name = opt.name.clone();
input.title = opt.cfg.title; input.main.title = opt.title.clone();
input.position = opt.cfg.position; input.main.position = opt.position;
input.main.visible = true;
// Typing opt.styles = (&THEME.input).into();
input.tx = Some(opt.tx.clone()); opt.blinking = YAZI.input.cursor_blink;
let ticket = input.ticket.clone(); *input.main.deref_mut() = yazi_widgets::input::Input::new(opt)?;
input.main.repos(area);
// Reset input
let cb: InputCallback = Box::new(move |before, after| {
if opt.cfg.realtime {
opt.tx.send(Err(InputError::Typed(format!("{before}{after}")))).ok();
} else if opt.cfg.completion {
opt.tx.send(Err(InputError::Completed(before.to_owned(), ticket.current()))).ok();
}
});
*input.deref_mut() = yazi_widgets::input::Input::new(
opt.cfg.value,
opt.cfg.position.offset.width.saturating_sub(YAZI.input.border()) as usize,
opt.cfg.obscure,
cb,
);
// Set cursor after reset
// TODO: remove this
if let Some(cursor) = opt.cfg.cursor {
input.snap_mut().cursor = cursor;
act!(r#move, input)?;
}
succ!(render!()); succ!(render!());
} }

View file

@ -1,5 +1,7 @@
extern crate self as yazi_actor; extern crate self as yazi_actor;
yazi_macro::mod_pub!(cmp confirm core help input lives mgr pick spot tasks which); yazi_macro::mod_pub!(app cmp confirm core help input lives mgr notify pick spot tasks which);
yazi_macro::mod_flat!(actor context); yazi_macro::mod_flat!(actor context);
pub fn init() { lives::init(); }

View file

@ -0,0 +1,29 @@
use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataMethods};
use super::{Lives, PtrCell};
pub(super) struct Behavior {
inner: PtrCell<yazi_scheduler::Behavior>,
}
impl Deref for Behavior {
type Target = yazi_scheduler::Behavior;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Behavior {
pub(super) fn make(inner: &yazi_scheduler::Behavior) -> mlua::Result<AnyUserData> {
let inner = PtrCell::from(inner);
Lives::scoped_userdata(Self { inner })
}
}
impl UserData for Behavior {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("reset", |_, me, ()| Ok(me.reset()));
}
}

View file

@ -1,10 +1,12 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, IntoLua, MetaMethod, UserData, Value}; use mlua::{AnyUserData, IntoLua, LuaString, MetaMethod, UserData, UserDataMethods, UserDataRef, Value};
use paste::paste; use paste::paste;
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
pub(super) type CoreRef = UserDataRef<Core>;
pub(super) struct Core { pub(super) struct Core {
inner: PtrCell<yazi_core::Core>, inner: PtrCell<yazi_core::Core>,
@ -12,6 +14,8 @@ pub(super) struct Core {
c_tabs: Option<Value>, c_tabs: Option<Value>,
c_tasks: Option<Value>, c_tasks: Option<Value>,
c_yanked: Option<Value>, c_yanked: Option<Value>,
c_input: Option<Value>,
c_which: Option<Value>,
c_layer: Option<Value>, c_layer: Option<Value>,
} }
@ -30,14 +34,16 @@ impl Core {
c_tabs: None, c_tabs: None,
c_tasks: None, c_tasks: None,
c_yanked: None, c_yanked: None,
c_input: None,
c_which: None,
c_layer: None, c_layer: None,
}) })
} }
} }
impl UserData for Core { impl UserData for Core {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: mlua::String| { methods.add_meta_method_mut(MetaMethod::Index, |lua, me, key: LuaString| {
macro_rules! reuse { macro_rules! reuse {
($key:ident, $value:expr) => { ($key:ident, $value:expr) => {
match paste! { &me.[<c_ $key>] } { match paste! { &me.[<c_ $key>] } {
@ -55,9 +61,9 @@ impl UserData for Core {
b"tabs" => reuse!(tabs, super::Tabs::make(&me.mgr.tabs)), b"tabs" => reuse!(tabs, super::Tabs::make(&me.mgr.tabs)),
b"tasks" => reuse!(tasks, super::Tasks::make(&me.tasks)), b"tasks" => reuse!(tasks, super::Tasks::make(&me.tasks)),
b"yanked" => reuse!(yanked, super::Yanked::make(&me.mgr.yanked)), b"yanked" => reuse!(yanked, super::Yanked::make(&me.mgr.yanked)),
b"layer" => { b"input" => reuse!(input, super::Input::make(&me.input)),
reuse!(layer, Ok::<_, mlua::Error>(yazi_plugin::bindings::Layer::from(me.layer()))) b"which" => reuse!(which, super::Which::make(&me.which)),
} b"layer" => reuse!(layer, Ok::<_, mlua::Error>(me.layer())),
_ => Value::Nil, _ => Value::Nil,
}) })
}); });

View file

@ -1,37 +1,35 @@
use std::ops::{Deref, Range}; use std::ops::{Deref, Range};
use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods};
use yazi_binding::cached_field; use yazi_shim::mlua::UserDataFieldsExt;
use super::{File, Filter, Lives, PtrCell}; use super::{File, Filter, Lives, PtrCell};
pub(super) struct Files { pub(super) struct Entries {
window: Range<usize>, window: Range<usize>,
folder: PtrCell<yazi_core::tab::Folder>, folder: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>, tab: PtrCell<yazi_core::tab::Tab>,
v_filter: Option<Value>,
} }
impl Deref for Files { impl Deref for Entries {
type Target = yazi_fs::Files; type Target = yazi_fs::Entries;
fn deref(&self) -> &Self::Target { &self.folder.files } fn deref(&self) -> &Self::Target { &self.folder.entries }
} }
impl Files { impl Entries {
pub(super) fn make( pub(super) fn make(
window: Range<usize>, window: Range<usize>,
folder: &yazi_core::tab::Folder, folder: &yazi_core::tab::Folder,
tab: &yazi_core::tab::Tab, tab: &yazi_core::tab::Tab,
) -> mlua::Result<AnyUserData> { ) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { window, folder: folder.into(), tab: tab.into(), v_filter: None }) Lives::scoped_userdata(Self { window, folder: folder.into(), tab: tab.into() })
} }
} }
impl UserData for Files { impl UserData for Entries {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, filter, |_, me| me.filter().map(Filter::make).transpose()); fields.add_static_field("filter", |_, me| me.filter().map(Filter::make).transpose());
} }
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {

View file

@ -1,38 +1,28 @@
use std::{ops::Deref, ptr}; use std::{ops::Deref, ptr};
use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods, Value}; use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods};
use yazi_binding::{Style, cached_field}; use yazi_binding::{Range, style::Style};
use yazi_config::THEME; use yazi_config::THEME;
use yazi_plugin::bindings::Range; use yazi_fs::file::FileInventory;
use yazi_shared::{path::AsPath, url::UrlLike}; use yazi_shared::{path::DynPath, url::UrlLike};
use super::Lives; use super::{FILE_CACHE, Lives};
use crate::lives::PtrCell; use crate::lives::{CoreRef, PtrCell};
pub(super) struct File { pub(super) struct File {
idx: usize, idx: usize,
folder: PtrCell<yazi_core::tab::Folder>, folder: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>, tab: PtrCell<yazi_core::tab::Tab>,
v_cha: Option<Value>,
v_url: Option<Value>,
v_link_to: Option<Value>,
v_name: Option<Value>,
v_path: Option<Value>,
v_cache: Option<Value>,
v_bare: Option<Value>,
} }
impl Deref for File { impl Deref for File {
type Target = yazi_fs::File; type Target = yazi_fs::file::File;
fn deref(&self) -> &Self::Target { &self.folder.files[self.idx] } fn deref(&self) -> &Self::Target { &self.folder.entries[self.idx] }
} }
impl AsRef<yazi_fs::File> for File { impl AsRef<yazi_fs::file::File> for File {
fn as_ref(&self) -> &yazi_fs::File { self } fn as_ref(&self) -> &yazi_fs::file::File { self }
} }
impl File { impl File {
@ -43,38 +33,28 @@ impl File {
) -> mlua::Result<AnyUserData> { ) -> mlua::Result<AnyUserData> {
use hashbrown::hash_map::Entry; use hashbrown::hash_map::Entry;
Ok(match super::FILE_CACHE.borrow_mut().entry(PtrCell(&folder.files[idx])) { Ok(
Entry::Occupied(oe) => oe.into_mut().clone(), match unsafe { (*FILE_CACHE.get()).assume_init_mut() }.entry(PtrCell(&folder.entries[idx])) {
Entry::Vacant(ve) => { Entry::Occupied(oe) => oe.into_mut().clone(),
let ud = Lives::scoped_userdata(Self { Entry::Vacant(ve) => {
idx, let ud = Lives::scoped_userdata(Self { idx, folder: folder.into(), tab: tab.into() })?;
folder: folder.into(), ve.insert(ud.clone());
tab: tab.into(), ud
}
v_cha: None, },
v_url: None, )
v_link_to: None,
v_name: None,
v_path: None,
v_cache: None,
v_bare: None,
})?;
ve.insert(ud.clone());
ud
}
})
} }
#[inline]
fn is_hovered(&self) -> bool { self.idx == self.folder.cursor }
} }
impl UserData for File { impl UserData for File {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
yazi_binding::impl_file_fields!(fields); yazi_binding::impl_file_fields!(fields);
cached_field!(fields, bare, |_, me| Ok(yazi_binding::File::new(&**me)));
fields.add_field_method_get("idx", |_, me| Ok(me.idx + 1)); fields.add_field_method_get("idx", |_, me| Ok(me.idx + 1));
fields.add_field_method_get("is_hovered", |_, me| Ok(me.idx == me.folder.cursor)); fields.add_field_method_get("is_hovered", |_, me| Ok(me.is_hovered()));
fields.add_field_method_get("in_current", |_, me| Ok(ptr::eq(&*me.folder, &me.tab.current))); fields.add_field_method_get("in_current", |_, me| Ok(ptr::eq(&*me.folder, &me.tab.current)));
fields.add_field_method_get("in_preview", |_, me| { fields.add_field_method_get("in_preview", |_, me| {
Ok(me.idx == me.folder.cursor && me.tab.hovered().is_some_and(|f| f.url == me.folder.url)) Ok(me.idx == me.folder.cursor && me.tab.hovered().is_some_and(|f| f.url == me.folder.url))
@ -84,13 +64,24 @@ impl UserData for File {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
yazi_binding::impl_file_methods!(methods); yazi_binding::impl_file_methods!(methods);
methods.add_method("icon", |lua, me, ()| {
yazi_binding::deprecate!(
lua,
"{}: `File:icon()` is deprecated, use `th.icon:match(file)` instead"
);
// TODO: use a cache
Ok(yazi_config::THEME.icon.matches(me, me.is_hovered()))
});
methods.add_method("size", |_, me, ()| { methods.add_method("size", |_, me, ()| {
Ok(if me.is_dir() { me.folder.files.sizes.get(&me.urn()).copied() } else { Some(me.len) }) Ok(if me.is_dir() {
me.folder.entries.sizes.get(&me.entry_key()).copied()
} else {
Some(me.len)
})
}); });
methods.add_method("mime", |lua, me, ()| { methods.add_method("mime", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| { let core: CoreRef = lua.named_registry_value("cx")?;
core.mgr.mimetype.get(&me.url).map(|s| lua.create_string(s)).transpose() core.mgr.mimetype.get(&me.url).map(|s| lua.create_string(s)).transpose()
})?
}); });
methods.add_method("prefix", |lua, me, ()| { methods.add_method("prefix", |lua, me, ()| {
if !me.url.has_trail() { if !me.url.has_trail() {
@ -99,64 +90,68 @@ impl UserData for File {
let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components(); let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components();
comp.next_back(); comp.next_back();
Some(lua.create_string(comp.as_path().encoded_bytes())).transpose() Some(lua.create_string(comp.dyn_path().encoded_bytes())).transpose()
}); });
methods.add_method("style", |lua, me, ()| { methods.add_method("style", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| { let core: CoreRef = lua.named_registry_value("cx")?;
let mime = core.mgr.mimetype.get(&me.url).unwrap_or_default(); let mime = core.mgr.mimetype.get(&me.url).unwrap_or_default();
THEME.filetype.iter().find(|&x| x.matches(me, mime)).map(|x| Style::from(x.style)) Ok(THEME.filetype.match_style(me, mime).map(Style::from))
})
}); });
methods.add_method("is_yanked", |lua, me, ()| { methods.add_method("is_yanked", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| { let core: CoreRef = lua.named_registry_value("cx")?;
if !core.mgr.yanked.contains(&me.url) { Ok(if !core.mgr.yanked.contains(&me.url) {
0u8 0u8
} else if core.mgr.yanked.cut { } else if core.mgr.yanked.cut {
2u8 2u8
} else { } else {
1u8 1u8
}
}) })
}); });
methods.add_method("is_marked", |_, me, ()| { methods.add_method("is_marked", |_, me, ()| {
use yazi_core::tab::Mode::*; let Some(visual) = me.tab.mode.visual() else {
if !me.tab.mode.is_visual() || me.folder.url != me.tab.current.url { return Ok(0u8);
};
if !visual.contains(me.idx, me.tab.current.cursor, me.folder.entries.len()) {
return Ok(0u8); return Ok(0u8);
} }
if me.folder.url != me.tab.current.url {
Ok(match &me.tab.mode { return Ok(0u8);
Select(_, indices) if indices.contains(&me.idx) => 1u8, }
Unset(_, indices) if indices.contains(&me.idx) => 2u8, Ok(if me.tab.mode.is_select() { 1u8 } else { 2u8 })
_ => 0u8,
})
}); });
methods.add_method("is_selected", |_, me, ()| Ok(me.tab.selected.contains(&me.url))); methods.add_method("is_selected", |_, me, ()| Ok(me.tab.selected.contains(&me.url)));
methods.add_method("found", |lua, me, ()| { methods.add_method("found", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| { let core: CoreRef = lua.named_registry_value("cx")?;
let Some(finder) = &core.active().finder else { let Some(finder) = &core.active().finder else {
return Ok(None); return Ok(None);
}; };
let Some(idx) = finder.matched_idx(&me.folder, me.urn()) else { let Some(idx) = finder.matched_idx(&me.folder, me.entry_key()) else {
return Ok(None); return Ok(None);
}; };
Some(lua.create_sequence_from([idx.into_lua(lua)?, finder.matched.len().into_lua(lua)?])) lua.create_sequence_from([idx.into_lua(lua)?, finder.matched.len().into_lua(lua)?]).map(Some)
.transpose()
})
}); });
methods.add_method("highlights", |lua, me, ()| { methods.add_method("highlights", |lua, me, ()| {
lua.named_registry_value::<AnyUserData>("cx")?.borrow_scoped(|core: &yazi_core::Core| { let core: CoreRef = lua.named_registry_value("cx")?;
let Some(finder) = &core.active().finder else { let Some(finder) = &core.active().finder else {
return None; return Ok(None);
}; };
if me.folder.url != me.tab.current.url { if me.folder.url != me.tab.current.url {
return None; return Ok(None);
} }
let Some(Some(h)) = me.url.name().map(|s| finder.filter.highlighted(s)) else {
return Ok(None);
};
let h = finder.filter.highlighted(me.url.name()?)?; lua.create_sequence_from(h.into_iter().map(Range::from)).map(Some)
Some(h.into_iter().map(Range::from).collect::<Vec<_>>())
})
}); });
} }
} }
inventory::submit! {
FileInventory {
register: |_| {},
borrow: |ud, f| f(&*ud.borrow::<File>()?),
}
}

View file

@ -1,21 +1,15 @@
use std::ops::{Deref, Range}; use std::ops::{Deref, Range};
use mlua::{AnyUserData, UserData, UserDataFields, Value}; use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_binding::{FolderStage, Url, cached_field};
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
use yazi_shim::mlua::UserDataFieldsExt;
use super::{File, Files, Lives, PtrCell}; use super::{Entries, File, Lives, PtrCell};
pub(super) struct Folder { pub(super) struct Folder {
window: Range<usize>, window: Range<usize>,
inner: PtrCell<yazi_core::tab::Folder>, inner: PtrCell<yazi_core::tab::Folder>,
tab: PtrCell<yazi_core::tab::Tab>, tab: PtrCell<yazi_core::tab::Tab>,
v_cwd: Option<Value>,
v_files: Option<Value>,
v_stage: Option<Value>,
v_window: Option<Value>,
v_hovered: Option<Value>,
} }
impl Deref for Folder { impl Deref for Folder {
@ -34,34 +28,24 @@ impl Folder {
Some(w) => w, Some(w) => w,
None => { None => {
let limit = LAYOUT.get().preview.height as usize; let limit = LAYOUT.get().preview.height as usize;
inner.offset..inner.files.len().min(inner.offset + limit) inner.offset..inner.entries.len().min(inner.offset + limit)
} }
}; };
Lives::scoped_userdata(Self { Lives::scoped_userdata(Self { window, inner: inner.into(), tab: tab.into() })
window,
inner: inner.into(),
tab: tab.into(),
v_cwd: None,
v_files: None,
v_stage: None,
v_window: None,
v_hovered: None,
})
} }
} }
impl UserData for Folder { impl UserData for Folder {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, cwd, |_, me| Ok(Url::new(&me.url))); fields.add_cached_field("cwd", |_, me| Ok(me.url.clone()));
cached_field!(fields, files, |_, me| Files::make(0..me.files.len(), me, &me.tab)); fields.add_static_field("files", |_, me| Entries::make(0..me.entries.len(), me, &me.tab));
cached_field!(fields, stage, |_, me| Ok(FolderStage::new(me.stage.clone()))); fields.add_cached_field("stage", |_, me| Ok(me.stage.clone()));
cached_field!(fields, window, |_, me| Files::make(me.window.clone(), me, &me.tab)); fields.add_static_field("window", |_, me| Entries::make(me.window.clone(), me, &me.tab));
fields.add_field_method_get("offset", |_, me| Ok(me.offset)); fields.add_field_method_get("offset", |_, me| Ok(me.offset));
fields.add_field_method_get("cursor", |_, me| Ok(me.cursor)); fields.add_field_method_get("cursor", |_, me| Ok(me.cursor));
cached_field!(fields, hovered, |_, me| { fields.add_static_field("hovered", |_, me| {
me.hovered().map(|_| File::make(me.cursor, me, &me.tab)).transpose() me.hovered().map(|_| File::make(me.cursor, me, &me.tab)).transpose()
}); });
} }

View file

@ -0,0 +1,29 @@
use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Lives, PtrCell};
use crate::lives::InputAlt;
pub(super) struct Input {
inner: PtrCell<yazi_core::input::Input>,
}
impl Deref for Input {
type Target = yazi_core::input::Input;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Input {
pub(super) fn make(inner: &yazi_core::input::Input) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into() })
}
}
impl UserData for Input {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("alt", |_, me| me.alt.as_ref().map(InputAlt::make).transpose());
}
}

View file

@ -0,0 +1,26 @@
use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Lives, PtrCell};
pub(super) struct InputAlt(PtrCell<yazi_core::input::InputAlt>);
impl Deref for InputAlt {
type Target = yazi_core::input::InputAlt;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl InputAlt {
pub(super) fn make(inner: &yazi_core::input::InputAlt) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self(inner.into()))
}
}
impl UserData for InputAlt {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("value", |lua, me| lua.create_string(me.lock().value()));
}
}

View file

@ -1,36 +1,38 @@
use std::cell::RefCell; use std::mem::MaybeUninit;
use hashbrown::HashMap; use hashbrown::HashMap;
use mlua::{AnyUserData, UserData}; use mlua::{AnyUserData, UserData};
use scopeguard::defer; use scopeguard::defer;
use tracing::error; use tracing::error;
use yazi_plugin::LUA; use yazi_plugin::LUA;
use yazi_shared::RoCell;
use super::{Core, PtrCell}; use super::{Core, PtrCell};
use crate::lives::MutCell;
static TO_DESTROY: RoCell<RefCell<Vec<AnyUserData>>> = RoCell::new_const(RefCell::new(Vec::new())); pub(super) static TO_DESTROY: MutCell<Vec<AnyUserData>> = MutCell::new(Vec::new());
pub(super) static FILE_CACHE: RoCell<RefCell<HashMap<PtrCell<yazi_fs::File>, AnyUserData>>> = pub(super) static FILE_CACHE: MutCell<
RoCell::new(); MaybeUninit<HashMap<PtrCell<yazi_fs::file::File>, AnyUserData>>,
> = MutCell::new(MaybeUninit::uninit());
pub struct Lives; pub struct Lives;
impl Lives { impl Lives {
pub fn scope<T>(core: &yazi_core::Core, f: impl FnOnce() -> mlua::Result<T>) -> mlua::Result<T> { pub fn scope<T, F>(core: &mut yazi_core::Core, f: F) -> mlua::Result<T>
FILE_CACHE.init(Default::default()); where
defer! { FILE_CACHE.drop(); } F: FnOnce(&mut yazi_core::Core) -> mlua::Result<T>,
{
let result = LUA.scope(|scope| { defer! {
scope.add_destructor(|| { unsafe {
for ud in TO_DESTROY.borrow_mut().drain(..) { (*FILE_CACHE.get()).assume_init_mut().clear();
for ud in (*TO_DESTROY.get()).drain(..) {
ud.destroy().expect("failed to destruct scoped userdata"); ud.destroy().expect("failed to destruct scoped userdata");
} }
}); }
}
LUA.set_named_registry_value("cx", scope.create_any_userdata_ref(core)?)?; LUA.set_named_registry_value("cx", Core::make(core)?)?;
LUA.globals().raw_set("cx", Core::make(core)?)?; LUA.globals().raw_set("cx", Core::make(core)?)?;
f() let result = f(core);
});
if let Err(ref e) = result { if let Err(ref e) = result {
error!("{e}"); error!("{e}");
@ -43,7 +45,7 @@ impl Lives {
T: UserData + 'static, T: UserData + 'static,
{ {
let ud = LUA.create_userdata(data)?; let ud = LUA.create_userdata(data)?;
TO_DESTROY.borrow_mut().push(ud.clone()); unsafe { &mut *TO_DESTROY.get() }.push(ud.clone());
Ok(ud) Ok(ud)
} }
} }

View file

@ -1 +1,5 @@
yazi_macro::mod_flat!(core file files filter finder folder lives mode preference preview ptr selected tab tabs task tasks yanked); yazi_macro::mod_flat!(behavior core entries file filter finder folder input input_alt lives mode mut_cell preference preview ptr selected tab tabs task tasks which yanked);
pub(super) fn init() {
unsafe { FILE_CACHE.get().write(std::mem::MaybeUninit::new(<_>::default())) };
}

View file

@ -1,6 +1,7 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods}; use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods};
use yazi_binding::deprecate;
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
@ -22,9 +23,13 @@ impl Mode {
impl UserData for Mode { impl UserData for Mode {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("is_normal", |_, me| Ok(me.is_normal()));
fields.add_field_method_get("is_select", |_, me| Ok(me.is_select())); fields.add_field_method_get("is_select", |_, me| Ok(me.is_select()));
fields.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); fields.add_field_method_get("is_unset", |_, me| Ok(me.is_unset()));
fields.add_field_method_get("is_visual", |_, me| Ok(me.is_visual())); fields.add_field_method_get("is_visual", |lua, me| {
deprecate!(lua, "{}: `mode.is_visual` is deprecated, use `not mode.is_normal` instead. \nSee #4101 for more details: https://github.com/sxyazi/yazi/pull/4101.");
Ok(!me.is_normal())
});
} }
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {

View file

@ -0,0 +1,11 @@
use std::cell::UnsafeCell;
pub(super) struct MutCell<T>(UnsafeCell<T>);
unsafe impl<T> Sync for MutCell<T> {}
impl<T> MutCell<T> {
pub(super) const fn new(value: T) -> Self { Self(UnsafeCell::new(value)) }
pub(super) fn get(&self) -> *mut T { self.0.get() }
}

View file

@ -1,15 +1,12 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields, Value}; use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_binding::cached_field; use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr};
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
pub(super) struct Preference { pub(super) struct Preference {
inner: PtrCell<yazi_core::tab::Preference>, inner: PtrCell<yazi_core::tab::Preference>,
v_sort_by: Option<Value>,
v_linemode: Option<Value>,
} }
impl Deref for Preference { impl Deref for Preference {
@ -20,19 +17,23 @@ impl Deref for Preference {
impl Preference { impl Preference {
pub(super) fn make(inner: &yazi_core::tab::Preference) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_core::tab::Preference) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into(), v_sort_by: None, v_linemode: None }) Lives::scoped_userdata(Self { inner: inner.into() })
} }
} }
impl UserData for Preference { impl UserData for Preference {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, sort_by, |_, me| Ok(me.sort_by.to_string())); // Display
fields.add_cached_field("name", |lua, me| lua.create_string(&me.name));
fields.add_cached_field("linemode", |lua, me| lua.create_string(&*me.linemode));
fields.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden));
// Sorting
fields.add_cached_field("sort_by", |_, me| Ok(me.sort_by.into_str()));
fields.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive)); fields.add_field_method_get("sort_sensitive", |_, me| Ok(me.sort_sensitive));
fields.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse)); fields.add_field_method_get("sort_reverse", |_, me| Ok(me.sort_reverse));
fields.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first)); fields.add_field_method_get("sort_dir_first", |_, me| Ok(me.sort_dir_first));
fields.add_field_method_get("sort_translit", |_, me| Ok(me.sort_translit)); fields.add_field_method_get("sort_translit", |_, me| Ok(me.sort_translit));
fields.add_field_method_get("sort_fallback", |_, me| Ok(me.sort_fallback.into_str()));
cached_field!(fields, linemode, |lua, me| lua.create_string(&me.linemode));
fields.add_field_method_get("show_hidden", |_, me| Ok(me.show_hidden));
} }
} }

View file

@ -1,15 +1,13 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields, Value}; use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_binding::cached_field;
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Folder, Lives, PtrCell}; use super::{Folder, Lives, PtrCell};
pub(super) struct Preview { pub(super) struct Preview {
tab: PtrCell<yazi_core::tab::Tab>, tab: PtrCell<yazi_core::tab::Tab>,
v_folder: Option<Value>,
} }
impl Deref for Preview { impl Deref for Preview {
@ -20,19 +18,19 @@ impl Deref for Preview {
impl Preview { impl Preview {
pub(super) fn make(tab: &yazi_core::tab::Tab) -> mlua::Result<AnyUserData> { pub(super) fn make(tab: &yazi_core::tab::Tab) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { tab: tab.into(), v_folder: None }) Lives::scoped_userdata(Self { tab: tab.into() })
} }
} }
impl UserData for Preview { impl UserData for Preview {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("skip", |_, me| Ok(me.skip)); fields.add_field_method_get("skip", |_, me| Ok(me.skip));
cached_field!(fields, folder, |_, me| { fields.add_static_field("folder", |_, me| {
me.tab me.tab
.hovered_folder() .hovered_folder()
.map(|f| { .map(|f| {
let limit = LAYOUT.get().preview.height as usize; let limit = LAYOUT.get().preview.height as usize;
Folder::make(Some(me.skip..f.files.len().min(me.skip + limit)), f, &me.tab) Folder::make(Some(me.skip..f.entries.len().min(me.skip + limit)), f, &me.tab)
}) })
.transpose() .transpose()
}); });

View file

@ -1,7 +1,6 @@
use mlua::AnyUserData; use mlua::AnyUserData;
use super::Lives; use super::{Lives, PtrCell};
use crate::lives::PtrCell;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(super) struct Selected; pub(super) struct Selected;
@ -11,7 +10,7 @@ impl Selected {
let inner = PtrCell::from(inner); let inner = PtrCell::from(inner);
Lives::scoped_userdata(yazi_binding::Iter::new( Lives::scoped_userdata(yazi_binding::Iter::new(
inner.as_static().values().map(yazi_binding::Url::new), inner.as_static().files().cloned(),
Some(inner.len()), Some(inner.len()),
)) ))
} }

View file

@ -1,22 +1,13 @@
use std::ops::Deref; use std::{borrow::Cow, ops::Deref};
use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods, Value}; use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods};
use yazi_binding::{Id, UrlRef, cached_field}; use yazi_shared::url::UrlRef;
use yazi_shared::url::UrlLike; use yazi_shim::mlua::UserDataFieldsExt;
use super::{Finder, Folder, Lives, Mode, Preference, Preview, PtrCell, Selected}; use super::{Finder, Folder, Lives, Mode, Preference, Preview, PtrCell, Selected};
pub(super) struct Tab { pub(super) struct Tab {
inner: PtrCell<yazi_core::tab::Tab>, inner: PtrCell<yazi_core::tab::Tab>,
v_name: Option<Value>,
v_mode: Option<Value>,
v_pref: Option<Value>,
v_current: Option<Value>,
v_parent: Option<Value>,
v_selected: Option<Value>,
v_preview: Option<Value>,
v_finder: Option<Value>,
} }
impl Deref for Tab { impl Deref for Tab {
@ -27,40 +18,29 @@ impl Deref for Tab {
impl Tab { impl Tab {
pub(super) fn make(inner: &yazi_core::tab::Tab) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_core::tab::Tab) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { Lives::scoped_userdata(Self { inner: inner.into() })
inner: inner.into(),
v_name: None,
v_mode: None,
v_pref: None,
v_current: None,
v_parent: None,
v_selected: None,
v_preview: None,
v_finder: None,
})
} }
} }
impl UserData for Tab { impl UserData for Tab {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(Id(me.id))); fields.add_field_method_get("id", |_, me| Ok(me.id));
cached_field!(fields, name, |lua, me| { fields.add_cached_field("name", |lua, me| match me.name() {
let url = &me.current.url; Cow::Borrowed(s) => lua.create_string(s),
lua.create_string(url.name().map_or_else(|| url.loc().encoded_bytes(), |n| n.encoded_bytes())) Cow::Owned(s) => lua.create_external_string(s),
}); });
cached_field!(fields, mode, |_, me| Mode::make(&me.mode)); fields.add_static_field("mode", |_, me| Mode::make(&me.mode));
cached_field!(fields, pref, |_, me| Preference::make(&me.pref)); fields.add_static_field("pref", |_, me| Preference::make(&me.pref));
cached_field!(fields, current, |_, me| Folder::make(None, &me.current, me)); fields.add_static_field("current", |_, me| Folder::make(None, &me.current, me));
cached_field!(fields, parent, |_, me| { fields.add_static_field("parent", |_, me| {
me.parent.as_ref().map(|f| Folder::make(None, f, me)).transpose() me.parent.as_ref().map(|f| Folder::make(None, f, me)).transpose()
}); });
cached_field!(fields, selected, |_, me| Selected::make(&me.selected)); fields.add_static_field("selected", |_, me| Selected::make(&me.selected));
cached_field!(fields, preview, |_, me| Preview::make(me)); fields.add_static_field("preview", |_, me| Preview::make(me));
cached_field!(fields, finder, |_, me| me.finder.as_ref().map(Finder::make).transpose()); fields.add_static_field("finder", |_, me| me.finder.as_ref().map(Finder::make).transpose());
} }
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {

View file

@ -1,15 +1,13 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields, Value}; use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields};
use yazi_binding::{SER_OPT, cached_field}; use yazi_scheduler::Progress;
use yazi_shim::mlua::{SER_OPT, UserDataFieldsExt};
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
pub(super) struct TaskSnap { pub(super) struct TaskSnap {
inner: PtrCell<yazi_scheduler::TaskSnap>, inner: PtrCell<yazi_scheduler::TaskSnap>,
v_name: Option<Value>,
v_prog: Option<Value>,
} }
impl Deref for TaskSnap { impl Deref for TaskSnap {
@ -20,17 +18,17 @@ impl Deref for TaskSnap {
impl TaskSnap { impl TaskSnap {
pub(super) fn make(inner: &yazi_scheduler::TaskSnap) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_scheduler::TaskSnap) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into(), v_name: None, v_prog: None }) Lives::scoped_userdata(Self { inner: inner.into() })
} }
} }
impl UserData for TaskSnap { impl UserData for TaskSnap {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, name, |lua, me| lua.create_string(&me.name)); fields.add_cached_field("title", |lua, me| lua.create_string(&me.title));
cached_field!(fields, prog, |lua, me| lua.to_value_with(&me.prog, SER_OPT)); fields.add_cached_field("prog", |lua, me| lua.to_value_with(&me.prog, SER_OPT));
fields.add_field_method_get("cooked", |_, me| Ok(me.prog.cooked()));
fields.add_field_method_get("running", |_, me| Ok(me.prog.running())); fields.add_field_method_get("running", |_, me| Ok(me.prog.running()));
fields.add_field_method_get("cooked", |_, me| Ok(me.prog.cooked()));
fields.add_field_method_get("success", |_, me| Ok(me.prog.success())); fields.add_field_method_get("success", |_, me| Ok(me.prog.success()));
fields.add_field_method_get("failed", |_, me| Ok(me.prog.failed())); fields.add_field_method_get("failed", |_, me| Ok(me.prog.failed()));
fields.add_field_method_get("percent", |_, me| Ok(me.prog.percent())); fields.add_field_method_get("percent", |_, me| Ok(me.prog.percent()));

View file

@ -1,17 +1,13 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields, Value}; use mlua::{AnyUserData, LuaSerdeExt, UserData, UserDataFields};
use yazi_binding::{SER_OPT, cached_field, deprecate}; use yazi_shim::mlua::{SER_OPT, UserDataFieldsExt};
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
use crate::lives::TaskSnap; use crate::lives::{Behavior, TaskSnap};
pub(super) struct Tasks { pub(super) struct Tasks {
inner: PtrCell<yazi_core::tasks::Tasks>, inner: PtrCell<yazi_core::tasks::Tasks>,
v_snaps: Option<Value>,
v_summary: Option<Value>,
v_progress: Option<Value>,
} }
impl Deref for Tasks { impl Deref for Tasks {
@ -22,13 +18,7 @@ impl Deref for Tasks {
impl Tasks { impl Tasks {
pub(super) fn make(inner: &yazi_core::tasks::Tasks) -> mlua::Result<AnyUserData> { pub(super) fn make(inner: &yazi_core::tasks::Tasks) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { Lives::scoped_userdata(Self { inner: inner.into() })
inner: inner.into(),
v_snaps: None,
v_summary: None,
v_progress: None,
})
} }
} }
@ -36,7 +26,9 @@ impl UserData for Tasks {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("cursor", |_, me| Ok(me.cursor)); fields.add_field_method_get("cursor", |_, me| Ok(me.cursor));
cached_field!(fields, snaps, |lua, me| { fields.add_static_field("behavior", |_, me| Behavior::make(&me.scheduler.behavior));
fields.add_static_field("snaps", |lua, me| {
let tbl = lua.create_table_with_capacity(me.snaps.len(), 0)?; let tbl = lua.create_table_with_capacity(me.snaps.len(), 0)?;
for snap in &me.snaps { for snap in &me.snaps {
tbl.raw_push(TaskSnap::make(snap)?)?; tbl.raw_push(TaskSnap::make(snap)?)?;
@ -44,20 +36,6 @@ impl UserData for Tasks {
Ok(tbl) Ok(tbl)
}); });
cached_field!(fields, summary, |lua, me| lua.to_value_with(&me.summary, SER_OPT)); fields.add_cached_field("summary", |lua, me| lua.to_value_with(&me.summary, SER_OPT));
cached_field!(fields, progress, |lua, me| {
deprecate!(
lua,
"`cx.tasks.progress` is deprecated, use `cx.tasks.summary` instead, in your {}"
);
lua.create_table_from([
("total", me.summary.total),
("succ", me.summary.success),
("fail", me.summary.failed),
("found", 0),
("processed", 0),
])
});
} }
} }

View file

@ -0,0 +1,35 @@
use std::ops::Deref;
use mlua::{AnyUserData, UserData, UserDataFields};
use yazi_shim::mlua::UserDataFieldsExt;
use super::{Lives, PtrCell};
pub(super) struct Which {
inner: PtrCell<yazi_core::which::Which>,
}
impl Deref for Which {
type Target = yazi_core::which::Which;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl Which {
pub(super) fn make(inner: &yazi_core::which::Which) -> mlua::Result<AnyUserData> {
Lives::scoped_userdata(Self { inner: inner.into() })
}
}
impl UserData for Which {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("tx", |_, me| Ok(me.tx.clone().map(yazi_binding::MpscUnboundedTx)));
fields.add_cached_field("cands", |lua, me| {
lua.create_sequence_from(me.inner.cands.iter().cloned())
});
fields.add_field_method_get("times", |_, me| Ok(me.inner.times));
fields.add_field_method_get("active", |_, me| Ok(me.inner.active));
fields.add_field_method_get("silent", |_, me| Ok(me.inner.silent));
}
}

View file

@ -1,7 +1,9 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{AnyUserData, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods}; use mlua::{AnyUserData, MetaMethod, MultiValue, ObjectLike, UserData, UserDataFields, UserDataMethods};
use yazi_binding::{Iter, get_metatable}; use yazi_binding::Iter;
use yazi_fs::file::File;
use yazi_shim::mlua::get_metatable;
use super::{Lives, PtrCell}; use super::{Lives, PtrCell};
@ -23,7 +25,7 @@ impl Yanked {
Lives::scoped_userdata(Self { Lives::scoped_userdata(Self {
inner, inner,
iter: Lives::scoped_userdata(Iter::new( iter: Lives::scoped_userdata(Iter::new(
inner.as_static().iter().map(yazi_binding::Url::new), inner.as_static().iter().map(File::from),
Some(inner.len()), Some(inner.len()),
))?, ))?,
}) })

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::ArrowOpt; use yazi_parser::ArrowForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -8,26 +8,30 @@ use crate::{Actor, Ctx};
pub struct Arrow; pub struct Arrow;
impl Actor for Arrow { impl Actor for Arrow {
type Options = ArrowOpt; type Form = ArrowForm;
const NAME: &str = "arrow"; const NAME: &str = "arrow";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let tab = cx.tab_mut(); let tab = cx.tab_mut();
if !tab.current.arrow(opt.step) { let old = tab.current.cursor;
if !tab.current.arrow(form.step) {
succ!(); succ!();
} }
// Retrace
tab.current.retrace();
// Visual selection // Visual selection
if let Some((start, items)) = tab.mode.visual_mut() { if let Some(visual) = tab.mode.visual_mut() {
let end = tab.current.cursor; visual.arrow(form.step, old, tab.current.cursor);
*items = (start.min(end)..=end.max(start)).collect();
} }
act!(mgr:hover, cx)?; act!(mgr:hover, cx)?;
act!(mgr:peek, cx)?; act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?; act!(mgr:watch, cx)?;
cx.tasks.scheduler.behavior.reset();
succ!(render!()); succ!(render!());
} }
} }

View file

@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use yazi_core::mgr::CdSource;
use yazi_macro::{act, succ}; use yazi_macro::{act, succ};
use yazi_parser::{VoidOpt, mgr::CdSource}; use yazi_parser::VoidForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -8,11 +9,11 @@ use crate::{Actor, Ctx};
pub struct Back; pub struct Back;
impl Actor for Back { impl Actor for Back {
type Options = VoidOpt; type Form = VoidForm;
const NAME: &str = "back"; const NAME: &str = "back";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
if let Some(u) = cx.tab_mut().backstack.shift_backward().cloned() { if let Some(u) = cx.tab_mut().backstack.shift_backward().cloned() {
act!(mgr:cd, cx, (u, CdSource::Back))?; act!(mgr:cd, cx, (u, CdSource::Back))?;
} }

View file

@ -0,0 +1,175 @@
use std::{fmt::{self, Display}, io::{self, Read, Write}, path::{MAIN_SEPARATOR, Path}};
use anyhow::{Result, anyhow};
use scopeguard::defer;
use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRuleArc};
use yazi_fs::{FilesOp, Splatter, engine::{Engine, local::Local}};
use yazi_macro::{succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt};
use yazi_shared::{data::Data, strand::Strand, url::{UrlBuf, UrlLike}};
use yazi_shim::path::CROSS_SEPARATOR;
use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_tty::{TTY, sequence::EraseScreen};
use yazi_vfs::engine;
use yazi_watcher::WATCHER;
use crate::{Actor, Ctx};
pub struct BulkCreate;
impl Actor for BulkCreate {
type Form = VoidForm;
const NAME: &str = "bulk_create";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let Some(opener) = Self::opener() else {
succ!(NotifyProxy::push_warn("Bulk create", "No text opener found"));
};
let cwd = cx.cwd().clone();
tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk-create");
let file = engine::create_new(&tmp).await?.file().await?;
defer! {
let tmp = tmp.clone();
tokio::spawn(async move {
Local::regular(&tmp).remove_file().await
});
}
TasksProxy::process_exec(ShellOpt {
cwd: cwd.clone(),
cmd: Splatter::new(&[file]).splat(&opener.run),
block: opener.block,
orphan: opener.orphan,
})
.await;
let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume());
AppProxy::stop().await;
let content = Local::regular(&tmp).read_to_string().await?;
Self::r#do(cwd, content.lines().filter_map(Entry::parse).collect()).await
});
succ!()
}
}
impl BulkCreate {
async fn r#do(cwd: UrlBuf, todo: Vec<Entry<'_>>) -> Result<()> {
writef!(TTY.writer(), "{EraseScreen}\n")?;
if todo.is_empty() {
return Ok(());
} else if !Self::ask_continue(&todo, None)? {
return Ok(()); // TODO: support `bulk_exit`?
}
let _permit = WATCHER.acquire().await.unwrap();
let (mut failed, mut succeeded) = (vec![], Vec::with_capacity(todo.len()));
for entry in todo {
let Ok(dist) = cwd.try_join(entry.path) else {
failed.push((entry, anyhow!("Invalid path")));
continue;
};
let result: io::Result<()> = if entry.is_dir {
engine::create_dir_all(&dist).await
} else if let Some(parent) = dist.parent() {
engine::create_dir_all(parent).await.ok();
engine::create_new(&dist).await.map(|_| ())
} else {
Err(io::Error::other("No parent directory"))
};
if let Err(e) = result {
failed.push((entry, e.into()));
} else if let Ok(f) = engine::file(dist).await {
succeeded.push(f);
} else {
failed.push((entry, anyhow!("Failed to retrieve file info")));
}
}
if !succeeded.is_empty() {
// err!(Pubsub::pub_after_bulk_create(it)); // FIXME
FilesOp::create(succeeded);
}
drop(_permit);
if !failed.is_empty() {
Self::output_failed(failed).await?;
}
Ok(())
}
fn opener() -> Option<OpenerRuleArc> {
YAZI
.open
.match_dummy(Path::new("bulk-create.txt"), "text/plain")
.and_then(|r| YAZI.opener.block(&r))
}
fn ask_continue(todo: &[Entry], decision: Option<bool>) -> Result<bool> {
if let Some(decision) = decision {
return Ok(decision);
}
{
let mut w = TTY.lockout();
for entry in todo {
writeln!(w, "{entry}")?;
}
write!(w, "Continue to create? (y/N): ")?;
w.flush()?;
}
let mut buf = [0; 10];
_ = TTY.reader().read(&mut buf)?;
Ok(buf[0] == b'y' || buf[0] == b'Y')
}
async fn output_failed(failed: Vec<(Entry<'_>, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout();
writeln!(stdout, "{EraseScreen}")?;
writeln!(stdout, "Failed to create:")?;
for (entry, err) in failed {
writeln!(stdout, "{entry}: {err}")?;
}
writeln!(stdout, "\nPress ENTER to exit")?;
stdout.flush()?;
TTY.reader().read_exact(&mut [0])?;
Ok(())
}
}
// --- Entry
struct Entry<'a> {
path: Strand<'a>,
is_dir: bool,
}
impl<'a> Entry<'a> {
fn parse(s: &'a str) -> Option<Self> {
let (path, is_dir) = match s.strip_suffix(CROSS_SEPARATOR) {
Some(p) => (p, true),
None => (s, false),
};
(!path.is_empty()).then_some(Self { path: path.into(), is_dir })
}
}
impl Display for Entry<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_dir {
write!(f, "{}{MAIN_SEPARATOR}", self.path.display())
} else {
self.path.display().fmt(f)
}
}
}

View file

@ -0,0 +1,19 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::mgr::BulkExitForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct BulkExit;
impl Actor for BulkExit {
type Form = BulkExitForm;
const NAME: &str = "bulk_exit";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
cx.mgr.batcher.decide(form.target, form.accept);
succ!();
}
}

View file

@ -1,20 +1,21 @@
use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path}; use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use crossterm::{execute, style::Print};
use hashbrown::HashMap; use hashbrown::HashMap;
use scopeguard::defer; use scopeguard::defer;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use yazi_binding::Permit; use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRule}; use yazi_config::{YAZI, opener::OpenerRuleArc};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider::{FileBuilder, Provider, local::{Gate, Local}}}; use yazi_fs::{FilesOp, Splatter, engine::{Engine, FileBuilder, local::Local}, max_common_root, path::skip_url};
use yazi_macro::{err, succ}; use yazi_macro::{err, succ, writef};
use yazi_parser::VoidOpt; use yazi_parser::VoidForm;
use yazi_proxy::{AppProxy, HIDER, TasksProxy}; use yazi_proxy::TasksProxy;
use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_scheduler::{AppProxy, NotifyProxy, process::ShellOpt};
use yazi_term::tty::TTY; use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_tty::{TTY, sequence::EraseScreen};
use yazi_vfs::{engine::{self, Demand}, maybe_exists};
use yazi_watcher::WATCHER; use yazi_watcher::WATCHER;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -22,18 +23,18 @@ use crate::{Actor, Ctx};
pub struct BulkRename; pub struct BulkRename;
impl Actor for BulkRename { impl Actor for BulkRename {
type Options = VoidOpt; type Form = VoidForm;
const NAME: &str = "bulk_rename"; const NAME: &str = "bulk_rename";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let Some(opener) = Self::opener() else { let Some(opener) = Self::opener() else {
succ!(AppProxy::notify_warn("Bulk rename", "No text opener found")); succ!(NotifyProxy::push_warn("Bulk rename", "No text opener found"));
}; };
let selected: Vec<_> = cx.tab().selected_or_hovered().cloned().collect(); let selected: Vec<_> = cx.tab().selected_or_hovered_urls().cloned().collect();
if selected.is_empty() { if selected.is_empty() {
succ!(AppProxy::notify_warn("Bulk rename", "No files selected")); succ!(NotifyProxy::push_warn("Bulk rename", "No files selected"));
} }
let root = max_common_root(&selected); let root = max_common_root(&selected);
@ -41,32 +42,31 @@ impl Actor for BulkRename {
selected.iter().enumerate().map(|(i, u)| Tuple::new(i, skip_url(u, root))).collect(); selected.iter().enumerate().map(|(i, u)| Tuple::new(i, skip_url(u, root))).collect();
let cwd = cx.cwd().clone(); let cwd = cx.cwd().clone();
let batcher = cx.core.mgr.batcher.clone();
tokio::spawn(async move { tokio::spawn(async move {
let tmp = YAZI.preview.tmpfile("bulk"); let tmp = YAZI.preview.tmpfile("bulk-rename");
Gate::default()
.write(true) let mut rw = Demand::default().write(true).create_new(true).open(&tmp).await?;
.create_new(true) rw.write_all(old.join(Strand::Utf8("\n")).encoded_bytes()).await?;
.open(&tmp)
.await?
.write_all(old.join(Strand::Utf8("\n")).encoded_bytes())
.await?;
defer! { defer! {
let tmp = tmp.clone(); let tmp = tmp.clone();
batcher.drain(&tmp);
tokio::spawn(async move { tokio::spawn(async move {
Local::regular(&tmp).remove_file().await Local::regular(&tmp).remove_file().await
}); });
} }
TasksProxy::process_exec(
cwd.into(), batcher.prime(&tmp);
Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run), TasksProxy::process_exec(ShellOpt {
vec![UrlCow::default(), UrlBuf::from(&tmp).into()], cwd,
opener.block, cmd: Splatter::new(&[rw.into_file().await?]).splat(&opener.run),
opener.orphan, block: opener.block,
) orphan: opener.orphan,
})
.await; .await;
let _permit = Permit::new(HIDER.acquire().await.unwrap(), AppProxy::resume()); let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume());
AppProxy::stop().await; AppProxy::stop().await;
let new: Vec<_> = Local::regular(&tmp) let new: Vec<_> = Local::regular(&tmp)
@ -78,7 +78,8 @@ impl Actor for BulkRename {
.map(|(i, s)| Tuple::new(i, s)) .map(|(i, s)| Tuple::new(i, s))
.collect(); .collect();
Self::r#do(root, old, new, selected).await let decision = batcher.drain(&tmp);
Self::r#do(root, old, new, selected, decision).await
}); });
succ!(); succ!();
} }
@ -90,12 +91,12 @@ impl BulkRename {
old: Vec<Tuple>, old: Vec<Tuple>,
new: Vec<Tuple>, new: Vec<Tuple>,
selected: Vec<UrlBuf>, selected: Vec<UrlBuf>,
decision: Option<bool>,
) -> Result<()> { ) -> Result<()> {
terminal_clear(TTY.writer())?; writef!(TTY.writer(), "{EraseScreen}\n")?;
if old.len() != new.len() { if old.len() != new.len() {
#[rustfmt::skip] #[rustfmt::skip]
let s = format!("Number of new and old file names mismatch (New: {}, Old: {}).\nPress <Enter> to exit...", new.len(), old.len()); writef!(TTY.writer(), "Number of new and old file names mismatch (New: {}, Old: {}).\nPress <Enter> to exit...", new.len(), old.len())?;
execute!(TTY.writer(), Print(s))?;
TTY.reader().read_exact(&mut [0])?; TTY.reader().read_exact(&mut [0])?;
return Ok(()); return Ok(());
@ -107,18 +108,7 @@ impl BulkRename {
return Ok(()); return Ok(());
} }
{ if !Self::ask_continue(&todo, decision)? {
let mut w = TTY.lockout();
for (old, new) in &todo {
writeln!(w, "{} -> {}", old.display(), new.display())?;
}
write!(w, "Continue to rename? (y/N): ")?;
w.flush()?;
}
let mut buf = [0; 10];
_ = TTY.reader().read(&mut buf)?;
if buf[0] != b'y' && buf[0] != b'Y' {
return Ok(()); return Ok(());
} }
@ -132,11 +122,11 @@ impl BulkRename {
continue; continue;
}; };
if maybe_exists(&new).await && !provider::must_identical(&old, &new).await { if maybe_exists(&new).await && !engine::must_identical(&old, &new).await {
failed.push((o, n, anyhow!("Destination already exists"))); failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = provider::rename(&old, &new).await { } else if let Err(e) = engine::rename(&old, &new).await {
failed.push((o, n, e.into())); failed.push((o, n, e.into()));
} else if let Ok(f) = File::new(new).await { } else if let Ok(f) = engine::file(new).await {
succeeded.insert(old, f); succeeded.insert(old, f);
} else { } else {
failed.push((o, n, anyhow!("Failed to retrieve file info"))); failed.push((o, n, anyhow!("Failed to retrieve file info")));
@ -145,7 +135,7 @@ impl BulkRename {
if !succeeded.is_empty() { if !succeeded.is_empty() {
let it = succeeded.iter().map(|(o, n)| (o.as_url(), n.url.as_url())); let it = succeeded.iter().map(|(o, n)| (o.as_url(), n.url.as_url()));
err!(Pubsub::pub_after_bulk(it)); err!(Pubsub::pub_after_bulk_rename(it));
FilesOp::rename(succeeded); FilesOp::rename(succeeded);
} }
drop(permit); drop(permit);
@ -156,17 +146,39 @@ impl BulkRename {
Ok(()) Ok(())
} }
fn opener() -> Option<&'static OpenerRule> { fn opener() -> Option<OpenerRuleArc> {
YAZI.opener.block(YAZI.open.all(Path::new("bulk-rename.txt"), "text/plain")) YAZI
.open
.match_dummy(Path::new("bulk-rename.txt"), "text/plain")
.and_then(|r| YAZI.opener.block(&r))
} }
fn replace_url(url: &UrlBuf, take: usize, rep: &StrandBuf) -> Result<UrlBuf> { fn replace_url(url: &UrlBuf, take: usize, rep: &StrandBuf) -> Result<UrlBuf> {
Ok(url.try_replace(take, PathDyn::with(url.kind(), rep)?)?.into_owned()) Ok(url.try_replace(take, PathDyn::with(url.kind(), rep)?)?.into_owned())
} }
fn ask_continue(todo: &[(Tuple, Tuple)], decision: Option<bool>) -> Result<bool> {
if let Some(decision) = decision {
return Ok(decision);
}
{
let mut w = TTY.lockout();
for (old, new) in todo {
writeln!(w, "{} -> {}", old.display(), new.display())?;
}
write!(w, "Continue to rename? (y/N): ")?;
w.flush()?;
}
let mut buf = [0; 10];
_ = TTY.reader().read(&mut buf)?;
Ok(buf[0] == b'y' || buf[0] == b'Y')
}
async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> { async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout(); let mut stdout = TTY.lockout();
terminal_clear(&mut *stdout)?; writeln!(stdout, "{EraseScreen}")?;
writeln!(stdout, "Failed to rename:")?; writeln!(stdout, "Failed to rename:")?;
for (old, new, err) in failed { for (old, new, err) in failed {

View file

@ -3,64 +3,67 @@ use std::{mem, time::Duration};
use anyhow::Result; use anyhow::Result;
use tokio::pin; use tokio::pin;
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::popup::InputCfg; use yazi_config::YAZI;
use yazi_core::mgr::CdSource;
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, path::{clean_url, expand_url}}; use yazi_fs::{FilesOp, path::{clean_url, expand_url}};
use yazi_macro::{act, err, render, succ}; use yazi_macro::{act, err, input, render, succ};
use yazi_parser::mgr::CdOpt; use yazi_parser::mgr::CdForm;
use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; use yazi_proxy::{CmpProxy, MgrProxy};
use yazi_shared::{Debounce, data::Data, errors::InputError, url::{AsUrl, UrlBuf, UrlLike}}; use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_vfs::{VfsFile, provider}; use yazi_vfs::engine;
use yazi_widgets::input::InputEvent;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Cd; pub struct Cd;
impl Actor for Cd { impl Actor for Cd {
type Options = CdOpt; type Form = CdForm;
const NAME: &str = "cd"; const NAME: &str = "cd";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
act!(mgr:escape_visual, cx)?; act!(mgr:escape_visual, cx)?;
if opt.interactive { if form.interactive {
return Self::cd_interactive(cx); return Self::cd_interactive(cx);
} } else if form.target == *cx.cwd() {
let tab = cx.tab_mut();
if opt.target == *tab.cwd() {
succ!(); succ!();
} }
// Stash first so it's possible to access the original cwd in hooks
act!(mgr:stash, cx, &form).ok();
// Take parent to history // Take parent to history
let tab = cx.tab_mut();
if let Some(t) = tab.parent.take() { if let Some(t) = tab.parent.take() {
tab.history.insert(t.url.clone(), t); tab.history.insert(t.url.clone(), t);
} }
// Current // Current
let rep = tab.history.remove_or(&opt.target); let rep = tab.history.remove_or(&form.target);
let rep = mem::replace(&mut tab.current, rep); let rep = mem::replace(&mut tab.current, rep);
tab.history.insert(rep.url.clone(), rep); tab.history.insert(rep.url.clone(), rep);
// Parent // Parent
if let Some(parent) = opt.target.parent() { if let Some(parent) = form.target.parent() {
tab.parent = Some(tab.history.remove_or(parent)); tab.parent = Some(tab.history.remove_or(parent));
} }
err!(Pubsub::pub_after_cd(tab.id, tab.cwd())); err!(Pubsub::pub_after_cd(tab.id, tab.cwd()));
act!(mgr:displace, cx)?; act!(mgr:displace, cx)?;
act!(mgr:hidden, cx)?; act!(mgr:hidden, cx).ok();
act!(mgr:sort, cx).ok(); act!(mgr:sort, cx).ok();
act!(mgr:hover, cx)?; act!(mgr:hover, cx)?;
act!(mgr:refresh, cx)?; act!(mgr:refresh, cx)?;
act!(mgr:stash, cx, opt).ok(); act!(app:title, cx).ok();
succ!(render!()); succ!(render!());
} }
} }
impl Cd { impl Cd {
fn cd_interactive(cx: &Ctx) -> Result<Data> { fn cd_interactive(cx: &mut Ctx) -> Result<Data> {
let input = InputProxy::show(InputCfg::cd(cx.cwd().as_url())); let input = input!(cx, YAZI.input.cd(cx.cwd().as_url()))?;
tokio::spawn(async move { tokio::spawn(async move {
let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50)); let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50));
@ -68,22 +71,22 @@ impl Cd {
while let Some(result) = rx.next().await { while let Some(result) = rx.next().await {
match result { match result {
Ok(s) => { InputEvent::Submit(s) => {
let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return }; let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return };
let Ok(url) = provider::absolute(&url).await else { return }; let Ok(url) = engine::absolute(&url).await else { return };
let url = clean_url(url); let url = clean_url(url);
let Ok(file) = File::new(&url).await else { return }; let Ok(file) = engine::file(&url).await else { return };
if file.is_dir() { if file.is_dir() {
return MgrProxy::cd(&url); return MgrProxy::cd(&url, CdSource::Cd);
} }
if let Some(p) = url.parent() { if let Some((p, k)) = url.pair2() {
FilesOp::Upserting(p.into(), [(url.urn().into(), file)].into()).emit(); FilesOp::Upserting(p.into(), [(k.into(), file)].into()).emit();
} }
MgrProxy::reveal(url); MgrProxy::reveal(url);
} }
Err(InputError::Completed(before, ticket)) => { InputEvent::Trigger(before, ticket) => {
CmpProxy::trigger(before, ticket); CmpProxy::trigger(before, ticket);
} }
_ => break, _ => break,

Some files were not shown because too many files have changed in this diff Show more